解决深拷贝循环引用痛点:一篇看懂 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,而不是 SetMap

  • 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 是实现缓存的最优选择(弱引用+键为对象)。

相关推荐
han_2 小时前
前端性能优化之性能指标篇
前端·javascript·性能优化
爱生活的苏苏2 小时前
修改默认滚动条样式
开发语言·javascript·ecmascript
大布布将军2 小时前
⚡部署的通行证:Docker 容器化基础
运维·前端·学习·程序人生·docker·容器·node.js
0思必得02 小时前
[Web自动化] JavaScriptAJAX与Fetch API
运维·前端·javascript·python·自动化·html·web自动化
爱上妖精的尾巴2 小时前
7-1 WPS JS宏 Object对象创建的几种方法
开发语言·前端·javascript
孙_华鹏2 小时前
高德地图与Three.js结合实现3D大屏可视化
前端·数据可视化
卸载引擎3 小时前
vue3+vite如何兼容低版本浏览器的白屏问题(安卓7/ios11)
android·javascript
秋雨雁南飞3 小时前
WaferMap.HTML
前端·css·html