深拷贝遇循环引用会无限递归致栈溢出,须用WeakMap缓存已拷贝对象并提前复用;WeakMap键为对象、自动垃圾回收、无需遍历,优于Map和普通对象;rfdc、structuredClone等现成方案已内置防护。

深拷贝遇到循环引用会无限递归,最终触发 RangeError: Maximum call stack size exceeded。核心解法不是“阻止报错”,而是提前识别并复用已拷贝的对象,打断递归链。
用 WeakMap 缓存已处理对象
WeakMap 是最常用且稳妥的选择,它以原始对象为键、拷贝结果为值,支持对象身份精准比对(===),且不阻止垃圾回收。
- 每次递归进入前,先查 WeakMap:若已有对应副本,直接返回,跳过后续递归
- 若无缓存,先新建目标对象({} 或 []),立即存入 WeakMap,再遍历属性递归拷贝
- 这样即使
obj.self = obj,第二次碰到obj时也会命中缓存,不会继续向下钻
注意 Map 和 WeakMap 的适用差异
WeakMap 更适合深拷贝场景,因为它的键只能是对象,天然过滤掉 null/undefined 等非对象类型,避免手动判断;而 Map 虽支持任意键,但需额外处理边界值,且可能意外保留对已销毁对象的强引用。
- WeakMap 无法遍历,但深拷贝不需要遍历缓存,只做“查-存”操作,正合适
- 不要用普通对象(如
{})模拟哈希表——对象键会被强制转成字符串,{a:1}和{b:2}都变成"[object Object]",导致误判
避免常见实现陷阱
很多手写深拷贝函数崩溃,不是逻辑错,而是漏掉了关键防护步骤。
立即学习“Java免费学习笔记(深入)”;
- 缓存必须在创建新对象后、遍历属性前就写入 WeakMap,否则同一对象在多处被引用时仍可能重复进入递归
- 不能只对顶层对象做缓存,嵌套中的每个对象都要独立记录
- 特殊类型(Date、RegExp、Map、Set、ArrayBuffer 等)要单独处理,否则它们的原型方法或内部结构可能绕过缓存逻辑
考虑现成方案更省心
业务中不必每次都手写。rfdc、lodash.cloneDeep、structuredClone(现代环境)等都内置了循环引用防护。
- rfdc 启用
{ circles: true }即可自动追踪引用,连copy.self === copy这样的自引用关系也能保持 -
structuredClone是浏览器原生 API,直接支持循环引用和多种内置类型,但暂不支持函数、undefined、BigInt(视环境而定) - 自己造轮子前,先确认是否真有定制需求——多数情况下,开箱即用的方案更安全可靠


















