必须用WeakMap而非Map或普通对象,因其键为弱引用对象,可避免内存泄漏且天然支持对象键;Map强持有导致GC无法回收,普通对象键会转为字符串而失效;仅引用类型需缓存,基础类型和函数无需缓存;缓存须“先存后递归”,且WeakMap实例必须复用。

用 WeakMap 缓存已拷贝对象,核心是让它在递归过程中“记住”原对象和副本的对应关系,遇到重复引用时直接返回已有副本,从而避开死循环和内存泄漏。
为什么必须用 WeakMap,不能用 Map 或普通对象?
WeakMap 的键只能是对象,且是弱引用——只要原始对象外部没有其他变量引用它,垃圾回收器就能立刻清理掉 WeakMap 中对应的条目。而 Map 会强持有原对象,导致即使你不再需要那个对象,它也一直卡在内存里;普通对象更不行,根本没法用对象当键。
- WeakMap:适合临时映射,“用完即走”,不干扰 GC
- Map:缓存长期滞留,可能引发内存缓慢上涨
- 普通对象:objKey 会被转成字符串,所有对象都变成 "[object Object]",完全失效
哪些对象要进 WeakMap,哪些不用管?
只对可能形成循环引用的引用类型做缓存判断和登记。基础类型和函数不参与递归,也不需要缓存。
- 需要缓存的:普通对象、数组、Date、RegExp、Map、Set、TypedArray、ArrayBuffer、DataView
- 无需缓存的:string / number / boolean / null / undefined / Symbol / BigInt
- 函数通常不缓存:JS 没有标准方式深拷贝函数内部状态,一般直接返回原函数(浅拷贝)
缓存时机必须是“先存后递归”
这是最容易出错的关键点。必须在新建副本之后、还没开始遍历属性之前,立刻执行 cache.set(original, clone)。如果等到所有子属性都处理完再存,多条引用路径交汇时(比如 obj.a = obj.b = target),target 就会被多次进入递归,缓存彻底失效。
- ✅ 正确顺序:查缓存 → 未命中 → 创建新对象 → 立即 cache.set() → 再递归处理属性
- ❌ 错误顺序:查缓存 → 未命中 → 创建新对象 → 先递归子项 → 最后才 cache.set()
WeakMap 实例不能写成参数默认值
每次调用 deepClone 都新建一个 WeakMap,等于没缓存。必须复用同一个实例。
- ❌ 错误写法:
function deepClone(obj, cache = new WeakMap()) { ... } - ✅ 推荐方式一:外部创建,显式传入
const cache = new WeakMap(); deepClone(obj, cache); - ✅ 推荐方式二:闭包封装,内部复用
const createCloner = () => { const cache = new WeakMap(); return (obj) => { ... }; };

















