递归深拷贝在循环引用时栈溢出,因无终止条件导致无限递归;解决方法是用weakmap缓存已遍历对象以跳过重复访问,避免内存泄漏且支持对象作键,需在递归前存入映射。

递归深拷贝为什么会在循环引用时栈溢出
因为对象内部存在 self 或互相引用的属性(比如 a.b = b,b.a = a),递归无终止条件,会无限深入。浏览器或 Node.js 会抛出 RangeError: Maximum call stack size exceeded。
解决思路不是“避免递归”,而是“记录已遍历的对象并跳过重复访问”。关键在于用一个弱映射(WeakMap)缓存源对象到克隆对象的映射关系——它既能避免内存泄漏,又支持对象作为键。
-
WeakMap是唯一能用对象作键且不阻止垃圾回收的内置结构;用Map也可行,但需手动清理,否则可能泄露 - 必须在进入递归前就存入映射,而不是在递归返回后,否则重复路径仍会触发新递归
- 只对对象(
typeof obj === 'object'且非null)和数组做缓存判断;基本类型直接返回,无需缓存
用 WeakMap 实现带循环检测的递归拷贝函数
下面是一个最小可用实现,覆盖普通对象、数组、Date、RegExp,并跳过函数、undefined、Symbol 等不可序列化值:
function deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return seen.get(obj);
let cloned;
if (obj instanceof Date) {
cloned = new Date(obj.getTime());
} else if (obj instanceof RegExp) {
cloned = new RegExp(obj);
} else if (Array.isArray(obj)) {
cloned = [];
} else {
cloned = {};
}
seen.set(obj, cloned);
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key], seen);
}
}
return cloned;
}
注意:seen 参数默认值写成 new WeakMap() 会导致每次调用都新建映射,失去循环检测能力。所以实际使用时应显式传入,或改用闭包封装(如导出为工厂函数)。
哪些值无法被 this 方法正确拷贝
这个实现明确不处理以下类型,遇到时原样返回(即浅拷贝语义):
-
function:不会执行deepClone,直接返回原函数引用 -
undefined、symbol、bigint:属于基本类型分支,直接返回 -
Map、Set、TypedArray、Promise:未在判断分支中覆盖,会走else分支误当作普通对象处理,结果错误
如果业务中用到 Map,需单独加判断:if (obj instanceof Map) { cloned = new Map([...obj].map(([k, v]) => [k, deepClone(v, seen)])); }。其他类型同理,但每加一种都增加维护成本。
性能与兼容性取舍点
WeakMap 在 IE11 中不可用,若需兼容,只能降级为 Map + 手动管理键数组,或改用唯一符号(Symbol.for)挂载到源对象上(有污染风险)。现代项目可放心使用 WeakMap。
递归方式天然比迭代(如基于栈模拟)更易读,但深度极大时仍有栈限制风险(即使有循环检测,极端嵌套仍可能超限)。真正对深度敏感的场景,建议改用 structuredClone(Node.js 17.0+ / Chrome 98+),它原生支持循环引用且不占 JS 调用栈。
循环引用不是“异常情况”,而是真实存在的数据形态(比如树节点的 parent 字段、响应式框架的依赖追踪结构)。漏掉这层检测的深拷贝,在某些输入下会静默失败或卡死,很难定位。










