深拷贝的实现思路深度解析
一句话概括
深拷贝本质上是递归遍历对象的每一层、对每种数据类型做针对性重建,同时用 WeakMap 解决循环引用——JSON.stringify 只是应试时的快捷答案,真正面试拼的是类型边界覆盖和循环引用处理。
核心知识点
1. 类型判断是地基
抄对象之前,先知道手里拿的是什么。typeof 只能区分基本类型和 object,数组、Date、null 全都混在一起。面试官想听的是 Object.prototype.toString。
1
2
3
4
5
6
7
8
9
10
const getType = (val) => {
if (val === null) return 'null';
const raw = Object.prototype.toString.call(val);
return raw.slice(8, -1).toLowerCase();
};
console.log(getType([])); // 'array'
console.log(getType(new Date())); // 'date'
console.log(getType(new Map())); // 'map'
console.log(getType(null)); // 'null'
2. 递归骨架:理解分治
闭上眼睛想:深拷贝 = 基本类型直接还 + 引用类型造个壳再装填。这就是递归的终止条件和递推关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const deepClone = (obj) => {
if (obj === null || typeof obj !== 'object') return obj;
const copy = Array.isArray(obj) ? [] : {};
for (const key of Reflect.ownKeys(obj)) {
copy[key] = deepClone(obj[key]);
}
return copy;
};
// 测试
const a = { x: [1, { y: 2 }] };
const b = deepClone(a);
b.x[1].y = 99;
console.log(a.x[1].y); // 2 —— 互不影响
3. 循环引用:WeakMap 的面试高光
一个对象引用自己,递归进去就死循环。面试官等的就是「用 WeakMap 记路」——来过的地方不再走第二次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const deepClone = (obj, memo = new WeakMap()) => {
if (obj === null || typeof obj !== 'object') return obj;
if (memo.has(obj)) return memo.get(obj); // 回头路
const copy = Array.isArray(obj) ? [] : {};
memo.set(obj, copy);
for (const key of Reflect.ownKeys(obj)) {
copy[key] = deepClone(obj[key], memo);
}
return copy;
};
// 循环引用测试
const circle = { a: 1 };
circle.self = circle;
const cloned = deepClone(circle);
console.log(cloned.self === cloned); // true
追问:为什么是 WeakMap 而不是 Map? 因为 WeakMap 对键是弱引用——原始对象被销毁后,WeakMap 里的记录不会阻止 GC。如果用 Map,拷贝历史上每一份临时对象都会被地图死死抱住,内存泄露只是时间问题。
4. 特殊类型:Date / RegExp / Map / Set
纯递归只能复制 {} 和 []。Date 拷贝引用后改副本会连带改原对象;RegExp 丢 lastIndex 会翻车;Map/Set 的内部结构不是可枚举属性,必须走专用 API。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const deepClone = (obj, memo = new WeakMap()) => {
if (obj === null || typeof obj !== 'object') return obj;
if (memo.has(obj)) return memo.get(obj);
// 特殊类型优先拦截
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (obj instanceof Map) {
const copy = new Map(); memo.set(obj, copy);
obj.forEach((v, k) => copy.set(deepClone(k, memo), deepClone(v, memo)));
return copy;
}
if (obj instanceof Set) {
const copy = new Set(); memo.set(obj, copy);
obj.forEach(v => copy.add(deepClone(v, memo)));
return copy;
}
// 普通对象 / 数组
const copy = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
memo.set(obj, copy);
Reflect.ownKeys(obj).forEach(k => copy[k] = deepClone(obj[k], memo));
return copy;
};
5. structuredClone:写进标准的答案
如果面试官说「还有更省事的吗」,别犹豫——
1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
date: new Date(),
map: new Map([['k', 'v']]),
set: new Set([1, 2, 3]),
nested: { deep: true }
};
const copy = structuredClone(obj);
// ✅ 原生支持循环引用
// ✅ Date / RegExp / Map / Set / ArrayBuffer 全搞定
// ❌ Function、Symbol、DOM 节点不行(会直接抛错)
说完立刻补一句:「structuredClone 底层走的是结构化克隆算法,和 postMessage 共享同一套引擎,所以可转移对象也能传。」这个补充加分。
其实你每天都在用
Redux / Zustand 状态更新
1
2
3
4
// reducer 里绝不能 mutate 原 state —— 深拷贝是基础操作
const newState = structuredClone(oldState);
newState.user.age += 1;
return newState;
表单草稿 vs 提交数据
1
2
3
4
// 编辑中途取消:扔掉副本就行,原数据毫发无损
const draft = deepClone(originalForm);
// ...用户一顿改...
if (cancel) resetTo(originalForm);
缓存计算结果
1
2
3
4
5
6
7
8
// 缓存里存引用会互相污染,必须拷贝一份冻住
const cache = new Map();
const getReport = (params) => {
if (cache.has(params.id)) return deepClone(cache.get(params.id));
const result = heavyComputation(params);
cache.set(params.id, deepClone(result));
return result;
};
Vue/React 的不可变更更
1
2
3
4
5
// Vue 3 reactive 自动深拷贝包装的对象,手改内部值会丢响应式追踪
// React setState 必须传新引用,浅拷贝套娃是新手常见 bug 来源
const [list, setList] = useState(original);
setList(prev => [...prev, newItem]); // 浅的可以
setList(prev => { prev[0].name = 'X'; return prev; }); // 🔥 原地修改,不触发渲染
常见误解
❌ 误区一:「JSON.parse(JSON.stringify(obj)) 就是深拷贝。」
这是面试翻车率最高的回答,没有之一。它丢 Function、丢 Symbol、丢 undefined,Date 变字符串,NaN/Infinity 变 null,循环引用直接炸。只适用于把纯数据对象传给后端,离「深拷贝」还差一个宇宙的距离。
1
2
3
const obj = { fn() {}, s: Symbol('x'), d: new Date(), n: NaN };
console.log(JSON.parse(JSON.stringify(obj)));
// { d: "2026-07-02T...Z", n: null } —— fn、Symbol 蒸发,NaN 变 null
❌ 误区二:「用 { ...obj } 展开就是深拷贝。」
展开运算符——和 Object.assign 一样——只拷贝第一层。嵌套对象、数组内部的引用纹丝不动。面试官拿这个坑人一坑一个准。
1
2
3
4
const obj = { user: { name: 'Alice' } };
const copy = { ...obj };
copy.user.name = 'Bob';
console.log(obj.user.name); // 'Bob' —— 原对象被连带修改
❌ 误区三:「深拷贝函数是必须的——它们也是数据。」
函数在绝大多数场景下不需要拷贝。它们是行为,不是状态。你很难找到两个函数「长得一样但应该是不同实例」的合理需求。如果硬要拷,new Function('return ' + fn.toString())() 能凑合,但闭包环境丢干净了,得不偿失。面试时说「函数直接复用引用」是正确答案。
❌ 误区四:「lodash.cloneDeep 就是万能的。」
它确实强,但它也拷不了 WeakMap、WeakSet(弱引用天生不可枚举),面对 Proxy 对象会穿透代理拿到原始 target。lodash 是好工具,但理解边界比记住函数名重要十倍。
一句话总结
深拷贝的面试核心只有三件事:用什么判断类型、怎么掐断循环引用、每种内置类型怎么重建——把这三条逻辑串成一条链路,比背 structuredClone 名字管用一百倍。