如何避免对象相互引用时深拷贝出现无限递归?
如何避免深拷贝递归中的循环引用无限递归问题
这个问题绝对是手写深拷贝时最容易踩的“深坑”之一!循环引用(比如A引用B、B又引用A)导致无限递归的本质,就是我们的递归逻辑没记住已经处理过哪些对象,一直在反复处理相互引用的节点,直到栈溢出。
解决这个问题的核心思路非常清晰:用一个缓存容器来记录已经完成拷贝的对象,每次递归处理前先检查当前对象是否已经在缓存里——如果有,直接返回缓存的拷贝结果;如果没有,就先创建拷贝对象并存入缓存,再递归处理它的属性。
最优实现:用WeakMap做缓存
我最推荐用WeakMap作为缓存容器,因为它的键是对象的弱引用,不会阻止垃圾回收器回收那些不再被其他地方引用的原对象,能避免内存泄漏。下面是完整的实现代码:
function deepClone(obj, cache = new WeakMap()) { // 处理基本类型、null和undefined:直接返回原值 if (obj === null || typeof obj !== 'object') { return obj; } // 先检查缓存:如果当前对象已经拷贝过,直接返回缓存的副本 if (cache.has(obj)) { return cache.get(obj); } // 根据原对象的类型,创建对应的拷贝实例(保留原型链) let cloneObj; if (obj instanceof Array) { cloneObj = []; } else { // 普通对象用Object.create继承原对象的原型,确保拷贝后的对象原型链正确 cloneObj = Object.create(Object.getPrototypeOf(obj)); } // 将原对象和它的拷贝存入缓存,后续遇到相同引用直接用缓存 cache.set(obj, cloneObj); // 遍历所有属性(包括不可枚举属性和Symbol类型的键),递归拷贝 Reflect.ownKeys(obj).forEach(key => { cloneObj[key] = deepClone(obj[key], cache); }); return cloneObj; }
关键细节解释
- WeakMap的优势:如果用普通
Map的话,键是强引用,即使原对象已经没有其他引用,缓存里的强引用也会让它无法被垃圾回收,长期运行可能导致内存泄漏。WeakMap完美解决了这个问题。 - 保留原型链:用
Object.create(Object.getPrototypeOf(obj))创建普通对象的拷贝,而不是直接{},这样能保留原对象的原型链,拷贝出来的对象和原对象是同一个构造函数的实例。 - 遍历所有属性:
Reflect.ownKeys会返回对象的所有自有键,包括不可枚举属性和Symbol类型的键,比Object.keys或for...in更全面。
测试循环引用场景
我们用你提到的Person A和Person B的例子来测试:
const personA = { name: 'Alice' }; const personB = { name: 'Bob' }; personA.bestFriend = personB; personB.bestFriend = personA; const clonedA = deepClone(personA); console.log(clonedA.bestFriend.bestFriend === clonedA); // 输出true,说明循环引用处理成功,没有无限递归
备选方案:用普通Map
如果你的运行环境不支持WeakMap(现在这种情况很少见了),也可以用普通Map替代,但要注意在拷贝完成后如果不需要缓存了,最好手动清空它,避免内存泄漏:
function deepCloneWithMap(obj, cache = new Map()) { // 逻辑和WeakMap版本几乎一致,只是把WeakMap换成Map if (obj === null || typeof obj !== 'object') { return obj; } if (cache.has(obj)) { return cache.get(obj); } let cloneObj = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj)); cache.set(obj, cloneObj); Reflect.ownKeys(obj).forEach(key => { cloneObj[key] = deepCloneWithMap(obj[key], cache); }); return cloneObj; }
总的来说,只要记住“用缓存记录已拷贝对象”这个核心,就能轻松解决循环引用导致的无限递归问题,同时还能避免重复拷贝同一个对象,提升深拷贝的性能。
内容的提问来源于stack exchange,提问作者melkors_lammoth
相关产品推荐
相关产品推荐

