解决深拷贝循环引用痛点:一篇看懂 WeakMap 实现方案

解决深拷贝循环引用痛点:一篇看懂 WeakMap 实现方案

在 JavaScript 开发中,深拷贝是高频需求------无论是处理复杂的业务数据,还是封装工具函数,我们都需要确保拷贝后的对象与原对象完全独立,不会互相干扰。但深拷贝藏着一个容易踩坑的痛点:循环引用。

比如一个对象的属性引用了自身,或者引用了父级对象,用普通的递归深拷贝实现会直接触发死循环,最终导致栈溢出错误。今天就带大家彻底搞懂循环引用的问题根源,以及如何用 WeakMap 优雅解决,最终实现一个健壮的深拷贝函数。

一、先搞懂:什么是循环引用?

循环引用就是对象内部存在"自引用"或"互引用"的情况,简单说就是对象自己指向了自己(或关联对象形成闭环)。举个最直观的例子:

ini 复制代码
// 自引用:obj 的 self 属性引用了自身
const obj = { a: 1 };
obj.self = obj; 

// 互引用:obj1 和 obj2 互相引用
const obj1 = { b: 2 };
const obj2 = { c: 3 };
obj1.target = obj2;
obj2.target = obj1;

这种结构很常见,比如 DOM 元素的 parentNode 属性(子元素引用父元素,父元素又包含子元素)、链表数据结构等。如果用普通递归深拷贝处理这类对象,递归会无限循环下去,直到浏览器触发 Maximum call stack size exceeded 栈溢出错误。

二、普通递归深拷贝的问题所在

先看一个简单的递归深拷贝实现(未处理循环引用):

javascript 复制代码
// 普通递归深拷贝(存在循环引用漏洞)
function simpleDeepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  const cloneObj = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 递归拷贝属性
      cloneObj[key] = simpleDeepClone(obj[key]);
    }
  }
  return cloneObj;
}

当我们用这个函数拷贝前面的循环引用对象 obj 时:

ini 复制代码
const obj = { a: 1 };
obj.self = obj;
simpleDeepClone(obj); // 直接栈溢出!

原因很简单:递归过程中,会不断解析 obj.self,而 obj.self 又指向 obj,导致递归永远无法终止,栈内存被持续占用直到溢出。

三、核心思路:用"缓存"记录已拷贝对象

要解决循环引用,关键在于避免对同一个对象重复递归拷贝。我们需要一个"缓存容器",在每次拷贝对象前,先检查这个对象是否已经被拷贝过:

  1. 如果没拷贝过,就正常拷贝,同时把"原对象"和"拷贝后的新对象"存入缓存;
  2. 如果已经拷贝过,直接从缓存中取出新对象返回,不再重复递归。

这里的核心是「缓存键值对」------键存原对象,值存对应的拷贝对象,这样才能精准找到已拷贝的结果。那用什么数据结构做缓存最合适呢?

推荐用 WeakMap,而不是 Set 或 Map:

  • WeakMap 的键只能是对象,正好匹配我们"存原对象"的需求;
  • WeakMap 是弱引用,当原对象没有其他引用时,会被垃圾回收机制回收,不会造成内存泄漏(如果用 Map 或 Set,会一直强引用原对象,导致内存无法释放)。

四、最终实现:带循环引用检测的深拷贝

结合上面的思路,实现代码如下,每一步都加了详细注释,新手也能看懂:

javascript 复制代码
function deepClone(obj, seen = new WeakMap()) {
  // 1. 基本类型或函数直接返回(基本类型按值传递,函数一般不需要拷贝)
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 2. 检查缓存:如果已拷贝过该对象,直接返回缓存中的新对象
  if (seen.has(obj)) {
    return seen.get(obj);
  }

  // 3. 创建新容器:根据原对象类型(数组/对象)创建对应的空容器
  const cloneObj = Array.isArray(obj) ? [] : {};

  // 4. 存入缓存:将原对象和新对象的映射关系存入 WeakMap
  seen.set(obj, cloneObj);

  // 5. 递归拷贝属性:遍历原对象的自有属性,递归拷贝到新对象
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], seen); // 传递缓存容器
    }
  }

  // 6. 返回拷贝后的新对象
  return cloneObj;
}

五、测试验证:循环引用是否被正确处理?

我们用前面的循环引用案例测试一下,看看效果:

ini 复制代码
// 测试1:自引用对象
const obj1 = { a: 1, b: [1, 2, 3] };
obj1.self = obj1; // 自引用

const cloned1 = deepClone(obj1);
console.log(cloned1); 
// 输出:{ a: 1, b: [1,2,3], self: [Circular] }([Circular] 表示循环引用)
console.log(cloned1.self === cloned1); // true(新对象的 self 引用自身,正确)
console.log(cloned1.b === obj1.b); // false(数组被深拷贝,完全独立)

// 测试2:互引用对象
const obj2 = { x: 'hello' };
const obj3 = { y: 'world' };
obj2.target = obj3;
obj3.target = obj2; // 互引用

const cloned2 = deepClone(obj2);
const cloned3 = deepClone(obj3);
console.log(cloned2.target === cloned3); // true(拷贝后的互引用关系正确)
console.log(cloned3.target === cloned2); // true

从测试结果可以看出:

  • 循环引用被正确识别,没有出现栈溢出;
  • 拷贝后的对象保持了原有的循环引用关系;
  • 普通属性和数组都被深拷贝,与原对象完全独立。

六、总结

深拷贝的核心难点是循环引用,解决的关键是「缓存已拷贝对象」,而 WeakMap 是实现缓存的最优选择(弱引用+键为对象)。

相关推荐
卸任7 小时前
Vibe coding工具使用心得
前端·ai编程
brien98 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance8 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M8 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写8 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李8 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易8 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding8 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马8 小时前
前沿视界
前端
kyriewen9 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员