关键不是检测循环而是提前拦截重复访问:用weakmap缓存原对象→副本映射,查到即返回;weakmap因弱引用不阻gc、键只能是对象,故唯一合适;必须先set再递归,仅缓存引用类型,cache不可作默认参数。

用 WeakMap 解决深拷贝中的循环引用,关键不是“检测循环”,而是“提前拦截重复访问”——每次准备拷贝一个对象前,先查它是否已被处理过;如果已有副本,就直接返回,跳过所有递归逻辑。
为什么 WeakMap 是唯一合适的选择
WeakMap 的键必须是对象,且是弱引用:它不会阻止垃圾回收。当原始对象被释放后,对应条目会自动消失,避免内存泄漏。Map 虽支持对象键,但强持有引用,长期运行容易堆积无效映射;普通对象无法用对象作键,更不可靠。
缓存操作必须放在递归之前
顺序错了,WeakMap 就失效。正确流程是:
- 检查 cache.has(obj),命中则直接 return cache.get(obj)
- 未命中时,先创建新对象({} 或 [] 等)
- 立刻执行 cache.set(obj, cloned),把映射登记进去
- 再遍历属性、递归拷贝子项
如果等到所有子项拷贝完才 set,那在多条引用路径交汇时(比如 obj.a 和 obj.b 都指向 target),target 会被多次进入递归,缓存完全不起作用。
哪些类型要进 WeakMap,哪些不用
只对可能形成循环的引用类型做缓存判断和记录:
- 需要缓存的:普通对象、数组、Date、RegExp、Map、Set、TypedArray、ArrayBuffer、DataView
- 无需缓存的:string / number / boolean / null / undefined / Symbol / BigInt —— 这些基础类型直接返回,不参与递归
- 函数通常也不进缓存:一般原样返回(浅拷贝),因为 JS 中没有标准方式深拷贝函数内部状态
WeakMap 实例不能写成默认参数
常见错误写法:function deepClone(obj, cache = new WeakMap())。这会导致每次调用都新建一个空 WeakMap,循环检测彻底失效。
正确做法有两种:
- 由调用方显式创建并复用:const cache = new WeakMap(); deepClone(obj, cache);
- 封装为工厂函数:const createCloner = () => { const cache = new WeakMap(); return (obj) => deepClone(obj, cache); };
不复杂但容易忽略。











