深浅拷贝
考点:浅拷贝 vs 深拷贝(JS 引用类型的核心)
面试官会结合“不可变数据”和“性能”来问,特别是你的 Redux/Vuex 场景。
1. 基础认知(概念区分)
- 浅拷贝(Shallow Copy):只复制第一层引用。新对象的
基本类型独立,但对象类型依然指向同一块堆内存。方法:Object.assign({}, obj)、{ ...obj }、Array.slice()。 - 深拷贝(Deep Copy):递归复制所有层级,新对象和原对象在堆内存中完全独立。方法:
JSON.parse(JSON.stringify(obj))(有缺陷)、structuredClone()(原生API)、递归遍历。
2. 进阶原理(三大死穴与终极实现)
JSON方法的致命缺陷(面试必问):- 无法处理
undefined、Function、Symbol(会被忽略或变成null)。 - 无法处理循环引用(
obj.self = obj会报错Converting circular structure to JSON)。 - 无法处理
Date、RegExp、Map、Set等特殊对象(会被转成空对象或字符串)。
- 无法处理
终极实现(你已经有了
WeakMap版,这里再强化一个structuredClone现代 API):“如果浏览器环境支持(Chrome 98+),
structuredClone(obj)是最快的原生深拷贝,天然支持Date、RegExp、Map、Set、ArrayBuffer,且支持循环引用!如果需兼容 IE 或复杂自定义类,我才会手写递归 +WeakMap。”
3. 结合你的简历(Redux / 点云数据处理)
“在 WeLink(React + Redux) 中,Reducer 要求 必须返回新对象才能触发更新。我大量使用浅拷贝(
{ ...state, messages: [...state.messages, newMsg] }),因为这足够快且符合 Redux 规范。但绝不用JSON方式深拷贝 Redux State,因为它会丢掉函数方法(比如action里的Symbol类型)。在 IAMP 点云平台,处理
Float32Array大块二进制数据时,我刻意避免深拷贝,因为百万级数组深拷贝会直接导致内存爆炸(卡死)。我采用TypedArray.slice()(浅拷贝视图) + 不可变指针 来管理数据块,用transfer转移所有权给 Worker 线程,零拷贝传输。”
重点防坑补充:
“如果面试官问我‘如何实现深拷贝的终极版’,我会说:生产环境优先用
lodash.cloneDeep,它已经处理了 99% 的边缘场景,不要重复造轮子。除非我要做极致性能优化,才会针对Float32Array写专门的拷贝函数。”
第一部分:浅拷贝(Shallow Copy)—— 基础与代码
- 基础认知:只复制第一层引用。新对象里的基本类型(
string/number)独立,但对象类型(object/array)依然指向同一块堆内存。修改拷贝后的对象里的数组,原对象也会变。 - 4种实现方式(你务必会写):
const original = {
name: 'Tom',
age: 25,
address: { city: '西安', zone: '高新' }
};
// 方法1:展开运算符(最常用)
const clone1 = { ...original };
// 方法2:Object.assign
const clone2 = Object.assign({}, original);
// 方法3:数组的 slice(针对数组)
const arr = [1, 2, { a: 3 }];
const arrClone = arr.slice(); // 或 [...arr]
// 方法4:手写极简浅拷贝(面试有时会让手写)
function shallowClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
const newObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]; // 直接赋值(只拷一层)
}
}
return newObj;
}
// 验证:clone1.address.city = '北京',original.address.city 也跟着变(说明浅拷贝没断开引用)第二部分:深拷贝(Deep Copy)—— 基础方法与致命缺陷
方法1:JSON.parse(JSON.stringify(obj))(最常见,但全是坑)
const original = {
name: 'Tom',
age: 25,
address: { city: '西安' },
birthday: new Date('1995-01-01'),
sayHi: function() { console.log('hi'); },
undef: undefined,
symbolKey: Symbol('id')
};
const clone = JSON.parse(JSON.stringify(original));
// ❌ 缺陷:
// 1. birthday 变成了字符串 "1995-01-01T00:00:00.000Z",不再是 Date 对象
// 2. sayHi 函数直接被忽略(没了)
// 3. undef: undefined 被忽略(没了)
// 4. symbolKey 被忽略(没了)
// 5. 遇到循环引用(obj.self = obj)直接报错:Converting circular structure to JSON方法2:structuredClone()(现代浏览器原生 API,Chrome 98+)
const clone = structuredClone(original);
// ✅ 优点:支持 Date、RegExp、Map、Set、ArrayBuffer、循环引用
// ❌ 缺陷:依然不能拷贝函数、Symbol、DOM 节点
// 面试时补一句:“如果我的项目用户都是 Chrome 现代浏览器,我会优先用这个,性能最好。”第三部分:手写终极深拷贝(面试必背完整版)
面试官潜台词:我要看到
WeakMap解决循环引用、Reflect.ownKeys遍历 Symbol、Object.getPrototypeOf保留原型链、以及Date/RegExp/Map/Set的特殊处理。
完整可运行代码(逐行带注释,背下来直接默写):
function deepClone(obj, hash = new WeakMap()) {
// ---------- 第一步:处理基本类型 / null / 函数 ----------
// 如果是基本类型(string/number/boolean/symbol)或 null,直接返回
// 注意:函数我们直接返回原引用(因为拷贝函数没有实际意义,且难以序列化)
if (obj === null || typeof obj !== 'object') {
return obj;
}
// ---------- 第二步:解决循环引用(核心!) ----------
// 如果 hash 中已经存过这个对象,直接返回之前存的那个拷贝,避免死递归
if (hash.has(obj)) {
return hash.get(obj);
}
// ---------- 第三步:处理特殊对象类型 ----------
// 1. Date:创建新 Date 实例
if (obj instanceof Date) {
return new Date(obj);
}
// 2. RegExp:创建新 RegExp 实例(注意要带上 flags)
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 3. Map:递归拷贝每个键值对(注意键也可能是对象,也要深拷贝)
if (obj instanceof Map) {
const cloneMap = new Map();
hash.set(obj, cloneMap); // 先存入 hash,防止 Map 内部自引用
obj.forEach((value, key) => {
// key 和 value 都可能引用复杂类型,都要递归深拷贝
cloneMap.set(deepClone(key, hash), deepClone(value, hash));
});
return cloneMap;
}
// 4. Set:递归拷贝每个值
if (obj instanceof Set) {
const cloneSet = new Set();
hash.set(obj, cloneSet);
obj.forEach(value => {
cloneSet.add(deepClone(value, hash));
});
return cloneSet;
}
// ---------- 第四步:处理数组 / 普通对象(保留原型链) ----------
// 关键点:用 Object.getPrototypeOf(obj) 保留 obj 的原型链(比如它是某个类的实例)
const cloneObj = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
// 存入 hash,后面如果子属性引用到 obj,直接返回 cloneObj
hash.set(obj, cloneObj);
// ---------- 第五步:遍历所有属性(包括 Symbol 和不可枚举属性) ----------
// Reflect.ownKeys 返回所有自有属性(包括 Symbol 类型)
Reflect.ownKeys(obj).forEach(key => {
// 递归拷贝每一个属性的值
cloneObj[key] = deepClone(obj[key], hash);
});
return cloneObj;
}
// ========== 测试用例(面试完可以自己验证) ==========
const obj = {
name: 'Tom',
age: 25,
birthday: new Date(),
reg: /test/i,
map: new Map([['key1', { val: 1 }]]),
set: new Set([{ val: 2 }]),
symbolKey: Symbol('id'),
nested: { a: 1, b: { c: 2 } }
};
obj.self = obj; // 制造循环引用
const cloned = deepClone(obj);
console.log(cloned);
console.log(cloned.birthday instanceof Date); // true
console.log(cloned.reg instanceof RegExp); // true
console.log(cloned.map instanceof Map); // true
console.log(cloned.self === cloned); // true(循环引用保持)深拷贝最终版代码(背这个就够了):
function deepClone(obj, map = new WeakMap()) {
// 基础类型 / null 直接返回
if (obj === null || typeof obj !== 'object') return obj;
// 处理特殊对象
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 解决循环引用
if (map.has(obj)) return map.get(obj);
// 保持原型链
const clone = new obj.constructor();
map.set(obj, clone);
// 遍历所有属性(包括 Symbol)
Reflect.ownKeys(obj).forEach(key => {
clone[key] = deepClone(obj[key], map);
});
return clone;
}第四部分:面试官“变态”追问与你的回答
追问1:“为什么不直接用 for...in 遍历属性,要用 Reflect.ownKeys?”
你的回答:“
for...in只能遍历可枚举的字符串属性,会漏掉 Symbol 类型的属性,而且会顺着原型链往上找(虽然可以用hasOwnProperty过滤)。Reflect.ownKeys能一次性拿到所有自有属性(字符串 + Symbol,包括不可枚举的),更彻底。比如我们 WeLink 项目里 Redux 的 Action 有时会带 Symbol 属性,如果用for...in就丢了。”
追问2:“为什么要用 WeakMap 而不是 Map 来做缓存?”
你的回答:“
WeakMap的键是弱引用。当obj被垃圾回收时,这个WeakMap里的条目会自动被清理,不会阻止垃圾回收,防止内存泄漏。如果用Map,即使obj不用了,它还被Map强引用着,内存永远释放不了。这在 IAMP 处理大量临时点云数据时极其重要。”
追问3:“如果拷贝 Float32Array(二进制数组)怎么办?”
你的回答(结合你的 IAMP 项目,封神!):“在实际工程中,对于
TypedArray(如Float32Array),我不会使用手写的递归深拷贝!因为数据量可能达到几十 MB,深拷贝会直接内存翻倍导致卡死。我会用buffer.slice()拷贝底层ArrayBuffer,或者利用postMessage的transfer转移所有权,实现零拷贝(Zero-Copy)的数据共享。这也是我在 IAMP 点云平台处理百万级坐标数据的核心优化手段。”
第五部分:结合你简历的“业务选择题”(面试主动抛出来)
面试官可能会问:“你在项目中怎么区分用浅拷贝还是深拷贝?”
你的回答(直接复制背):
“面试官,我有一套清晰的决策逻辑:
1. 优先用浅拷贝(高性能场景):在 WeLink Redux 和 IRMP Pinia 中,所有状态更新我都用浅拷贝(
{ ...state, list: [...state.list, newItem] })。因为状态管理工具(Redux/Vuex)内部只做浅层引用对比来判断数据是否变化,深拷贝反而会破坏性能。而且我们项目数据层级不深,浅拷贝足够安全。2. 特定场景用
structuredClone(最优解):在 MMMP 大屏仪表板,我需要深拷贝一份配置对象(含Date和嵌套对象)来做历史快照(撤销回滚)。因为用户都是 Chrome,我用structuredClone原生实现,比手写递归快得多。3. 慎用或禁用深拷贝(大内存场景):在 IAMP 点云渲染,面对 100 万个
Float32Array点,任何形式的深拷贝都是灾难。我通过transferable对象 + Worker 线程 实现主线程和 Worker 共享内存,避免拷贝。盲目深拷贝在数据密集型场景是性能红线。”
面试现场组合技(终极总结陈词)
“面试官,关于拷贝,我的核心理念是 ‘浅拷贝是常态,深拷贝是特例,手写递归是兜底方案’。
在日常业务中(表单、列表、Redux),我 90% 的场景用浅拷贝(展开运算符) 足以应对。只有遇到配置快照、深层次复杂对象的克隆时,我才会考虑深拷贝。在 IAMP 项目中,我甚至刻意避免深拷贝来保护内存,改用
structuredClone或Transferable对象。如果面试官要求手写,我会迅速掏出这套
WeakMap + Reflect.ownKeys + 类型分支的完整模版,并主动说出Map/Set/Date/RegExp的特殊处理。”