深拷贝循环引用的处理深度解析
一句话概括
深拷贝遇到对象属性指回自身时,用 WeakMap 建立「原对象→副本」的映射表,既能检测循环防止无限递归,又不会因为强引用阻止垃圾回收。
核心知识点
1. 为什么 JSON 方案直接挂掉?
因为 JSON.stringify 内部遍历对象图时发现自己回到了起点,V8 直接抛错——它没有「记录已路过节点」的能力。
1
2
3
4
5
const obj = { name: 'Alice' };
obj.self = obj;
JSON.parse(JSON.stringify(obj));
// ❌ TypeError: Converting circular structure to JSON
2. WeakMap 的检测逻辑:先查表再克隆
在递归进入一个对象之前,先查 WeakMap 里有没有「已克隆版本」。有就直接返回,没有才新建并登记。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj); // 👈 灵魂一行
const copy = Array.isArray(obj) ? [] : {};
hash.set(obj, copy);
for (const key of Reflect.ownKeys(obj)) {
copy[key] = deepClone(obj[key], hash);
}
return copy;
}
// 验证
const a = { v: 1 }; a.self = a;
const b = deepClone(a);
console.log(b.self === b); // true,循环结构被保留
3. WeakMap vs Map:面试杀招
Map 是强引用,用完深拷贝函数后,hash 里仍然拽着原对象不让 GC 回收。WeakMap 的键是弱引用,函数执行完、外部没有引用原对象时,GC 可以正常干活。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = { huge: new Array(1e7) };
// --- Map 版本(内存泄漏)---
(function () {
const hash = new Map();
hash.set(obj, deepClone(obj));
})(); // hash 消失了,但 obj 被 Map 强引用的期间 GC 无法回收
// --- WeakMap 版本(安全)---
let obj2 = { huge: new Array(1e7) };
(function () {
const hash = new WeakMap();
hash.set(obj2, deepClone(obj2));
})(); // WeakMap 不阻止 GC,函数结束意味着 obj 的外部强引用只有 obj2 自身
obj2 = null; // 此时可以回收
问自己:如果只做一次拷贝就扔掉 hash,Map 也没泄漏?——对,但面试问的是「为什么标准做法是 WeakMap」。答案:WeakMap 的语义就是「关联数据但别挡 GC」,用它天然正确。
4. 完整版:Date / RegExp / Map / Set 一个不漏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function deepClone(source, hash = new WeakMap()) {
if (source === null || typeof source !== 'object') return source;
if (hash.has(source)) return hash.get(source);
const type = Object.prototype.toString.call(source);
let clone;
switch (type) {
case '[object Date]': return new Date(source);
case '[object RegExp]': return new RegExp(source.source, source.flags);
case '[object Map]':
clone = new Map(); hash.set(source, clone);
source.forEach((v, k) => clone.set(deepClone(k, hash), deepClone(v, hash)));
return clone;
case '[object Set]':
clone = new Set(); hash.set(source, clone);
source.forEach(v => clone.add(deepClone(v, hash)));
return clone;
case '[object Array]':
clone = []; hash.set(source, clone);
source.forEach((v, i) => (clone[i] = deepClone(v, hash)));
return clone;
default:
clone = Object.create(Object.getPrototypeOf(source));
hash.set(source, clone);
Reflect.ownKeys(source).forEach(k => (clone[k] = deepClone(source[k], hash)));
return clone;
}
}
5. 小心递归爆栈:迭代 + 栈模拟
万层级嵌套时递归版直接 RangeError,改成显式栈即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function deepCloneIterative(source) {
const root = Array.isArray(source) ? [] : {};
const stack = [{ source, target: root }];
const hash = new WeakMap().set(source, root);
while (stack.length) {
const { source, target } = stack.pop();
for (const key of Reflect.ownKeys(source)) {
const val = source[key];
if (val === null || typeof val !== 'object') {
target[key] = val;
} else if (hash.has(val)) {
target[key] = hash.get(val);
} else {
const copy = Array.isArray(val) ? [] : {};
hash.set(val, copy);
target[key] = copy;
stack.push({ source: val, target: copy });
}
}
}
return root;
}
其实你每天都在用
- Redux / Zustand 状态更新:reducer 必须返回新对象,immer 内部就在做「记录已拷贝节点」防止重复克隆共享结构。
- Vue / React 的 undo / redo:每次保存状态快照都是深拷贝。如果状态里有 parent↔child 的双向引用,WeakMap 机制就在背后兜底。
- 表单草稿暂存:编辑到一半点击「暂存」,把当前 formData 深拷贝存到 localStorage。
structuredClone就是浏览器自带的循环引用安全拷贝。 - 组件树可视化:递归组件渲染用到的 tree 数据里,常常有
node.children+child.parent的双向引用。 - PostMessage / Web Worker 传数据:
structuredClone做序列化,天然支持循环引用,面试官问你用它还是 JSON——这就是考点。
常见误解
❌ 误区1:”用 try-catch 包住 JSON.stringify 就行”
try-catch 只会让你「不崩溃」,但丢失了整个对象。面试官要的是「正确拷贝循环结构」,而不是「绕过报错」。
❌ 误区2:”Map 和 WeakMap 效果一样,面试这么写也没事”
语法上能跑,但深拷贝函数执行完后 hash 本该被 GC,Map 却会因为强引用让原对象活得更久。尤其在大对象或循环拷贝频繁的场景里,这是真实的内存泄漏点。
❌ 误区3:”我自己写的对象里不可能有循环引用”
DOM 节点的 parentNode / childNodes、双向链表、树形菜单的 children + parent、关系型数据(A 引用 B,B 引用 C,C 引用 A)——这些在生产代码里到处都是,不是你「刻意写」才会出现。
❌ 误区4:”用 lodash.cloneDeep 就高枕无忧了”
lodash 内部用 WeakMap 吗?老版本用的是 Map + 自定义 hash,仍有强引用问题。知道原理才能评估工具链,而不是无脑 npm install。
一句话总结
循环引用的深拷贝本质是「图的遍历 + 访问记录」问题——WeakMap 就是那张记录表,弱引用是它不拖累 GC 的精髓。
面试官想听的不是你背出
if (hash.has(obj)),而是你能说清楚:「哈希表记录映射 → 弱引用不挡 GC → 图遍历里保留共享关系」这条链路。