WeakMap是解决循环引用深拷贝的唯一合适选择,因其键为弱引用对象,既避免内存泄漏又天然拦截重复递归;必须先缓存新对象再递归处理属性,且仅对引用类型缓存。

用 WeakMap 解决循环引用对象的深拷贝,核心不是“判定”循环是否存在,而是**在递归过程中主动拦截重复访问,从而天然绕过死循环**。它不依赖事后检测或结构分析,而是在第一次创建副本时就登记,后续再遇到同一对象直接返回已有的副本——这样既保证了拓扑一致性,又彻底避免了栈溢出。
WeakMap 为什么是唯一合适的选择
普通对象不能用对象作键;Map 虽支持对象键,但会强持有引用,导致原始对象即使不再使用也无法被垃圾回收,长期运行可能内存泄漏;WeakMap 的键只能是对象,且是弱引用,只要外部没有其他强引用指向该对象,它就会自动从 WeakMap 中消失,不影响 GC。这使得 WeakMap 成为临时缓存“原对象→副本”映射的理想容器。
关键操作顺序不能错:先存后递归
必须在新建副本之后、开始遍历属性之前,立刻执行 cache.set(obj, cloned)。如果等到所有子属性都拷贝完再存,那么在多条引用路径交汇时(比如 obj.a = obj.b = target),target 会被多次进入递归,缓存完全失效。
- 正确流程:检查缓存 → 未命中 → 创建新对象 → 立即缓存 → 再递归处理属性
- 错误流程:检查缓存 → 未命中 → 创建新对象 → 递归处理属性 → 最后才缓存
哪些值要进 WeakMap,哪些不用管
只对可能形成循环的引用类型做缓存判断和记录:
- 需要缓存的:普通对象、数组、Date、RegExp、Map、Set、TypedArray、ArrayBuffer、DataView
- 无需缓存的:string / number / boolean / null / undefined / Symbol / BigInt —— 这些基础类型直接返回,不参与递归
- 函数通常也不进缓存:一般选择浅拷贝(原样返回),因为深拷贝函数内部状态在 JS 中不可靠且无标准方式
实际代码中容易踩的坑
WeakMap 实例绝不能写成函数参数默认值,否则每次调用 deepClone 都会新建一个空 WeakMap,缓存形同虚设。
- ❌ 错误写法:function deepClone(obj, cache = new WeakMap()) { ... }
- ✅ 正确写法一:const cache = new WeakMap(); deepClone(obj, cache);
- ✅ 正确写法二(闭包封装):const createCloner = () => { const cache = new WeakMap(); return (obj) => { ... }; };










