js深拷贝循环引用解决
JS 深拷贝撞上“循环引用”?别慌,WeakMap 来救场
做前端开发久了,谁没在复制粘贴对象时遇到过浏览器突然卡死的尴尬时刻?大概率是撞上了循环引用。比如对象 A 里有个属性指向 B,而 B 的属性又指回 A。这时候如果你用常规的递归函数去深拷贝,程序就会像陷入迷宫一样无限跑下去,直到栈溢出。
很多人第一反应是直接用 JSON.parse(JSON.stringify()) 凑合一下。但这个方法不仅会丢失函数和 undefined,最致命的是它遇到循环结构同样会报错。想要真正解决这个问题,核心思路其实就一条:给递归过程装上一个“记忆本”。
为什么要用 WeakMap?
在实现自定义深拷贝时,我们需要记录每个已经处理过的源对象。如果再次遍历到同一个地址,就直接返回之前生成的副本,而不是继续深入。
这里最容易忽略的细节是存储容器。普通 Map 虽然能存键值对,但它会让键保持强引用,导致垃圾回收机制无法清理不再使用的对象引用,容易造成内存泄漏。而 WeakMap 的键是弱引用的,当源对象被销毁时,WeakMap 里的对应条目也会自动消失,完美契合临时记录的需求,也不会给内存压力添堵。
动手写一个健壮的 clone 函数
逻辑理顺后,代码实现就不难了。我们定义一个函数,传入源数据和用于记录的 cache(默认新建 WeakMap)。在递归开始前,先查表:如果有缓存,直接取出来;如果没有,创建新对象并存入缓存,再开始填充属性。
function deepClone(obj, cache = new WeakMap()) {
// 非对象或 null 直接返回
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 检查是否有循环引用记录
if (cache.has(obj)) {
return cache.get(obj);
}
// 创建新容器,区分 Array 和 Object
const cloneObj = Array.isArray(obj) ? [] : {};
// 【关键步骤】先将映射存入 cache,防止后续递归时死循环
cache.set(obj, cloneObj);
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// 递归处理嵌套属性
cloneObj[key] = deepClone(obj[key], cache);
}
}
return cloneObj;
}
这段代码的核心在于 cache.set(obj, cloneObj) 必须在递归遍历属性之前执行。这一点至关重要,因为一旦属性里又包含当前对象的引用,后续的递归调用就能立刻从缓存中拿到正在构建的目标对象,从而切断无限循环的路径。
那些容易被忽视的内置类型
上面的基础版解决了结构循环问题,但实际业务中还会遇到 Date、RegExp、Map 等内置对象。它们不能单纯当作普通对象处理。比如在递归判断前,增加一层针对特殊类型的识别逻辑。如果是日期对象,直接 new Date(obj.getTime());如果是正则,则需保留标志位和源字符串重新构建。
现代浏览器其实提供了一个更省力的方案:structuredClone()。它是原生支持深拷贝且内置处理循环引用的 API,性能通常优于手写方案。不过要注意兼容性,如果需要兼容旧版环境,或者需要精细控制哪些字段不参与拷贝,手写基于 WeakMap 的版本依然是基本功。
写在最后
理解循环引用的破解之道,不仅仅是为了解决一个 bug,更能帮你理清内存管理的脉络。在处理复杂状态管理或持久化数据时,清晰的数据流比单纯的代码复制更重要。下次再遇到深拷贝需求,先评估是否真的需要全量复制,有时候浅拷贝加上按需懒加载,才是更优雅的性能解法。


还没有评论,来说两句吧...