怎样实现javascript深拷贝_javascript中如何处理循环引用

JSON.parse(JSON.stringify()) 不能处理循环引用,会报错 TypeError;structuredClone() 原生支持循环引用及多种类型但不支持函数等,兼容性需注意;手写 deepClone 需用 WeakMap 解决循环引用并适配各类原生对象。

JSON.parse(JSON.stringify()) 为什么不能处理循环引用

这个方法会直接报错 TypeError: Converting circular structure to JSON,因为 JSON.stringify() 在序列化时遇到对象的某个属性指向自身(或形成闭环引用)就会中断。它不支持任何函数、undefinedSymbolDateRegExpMapSet 等非可序列化值,更不会记录已遍历过的对象。

用 WeakMap 记录已拷贝对象来解决循环引用

核心思路是:在递归拷贝过程中,用 WeakMap 缓存原始对象 → 拷贝后对象的映射。每次进入新对象前先查表,命中就直接返回对应拷贝体,避免无限递归。

适用场景:需要保留函数、DateRegExpMapSet 等原生类型,且必须支持任意嵌套与循环引用。

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 (obj instanceof Map) {
    cloned = new Map();
    seen.set(obj, cloned);
    obj.forEach((value, key) => cloned.set(deepClone(key, seen), deepClone(value, seen)));
  } else if (obj instanceof Set) {
    cloned = new Set();
    seen.set(obj, cloned);
    obj.forEach(value => cloned.add(deepClone(value, seen)));
  } else {
    cloned = Array.isArray(obj) ? [] : {};
    seen.set(obj, cloned);
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        cloned[key] = deepClone(obj[key], seen);
      }
    }
  }
  return cloned;
}

structuredClone() 是什么,能替代手写吗

structuredClone() 是现代浏览器(Chrome 98+、Firefox 94+、Safari 16.4+)和 Node.js 18.13+ 原生提供的深拷贝 API,它原生支持循环引用、MapSetDateRegExpArrayBuffer 等,但不支持函数、undefinedSymbol 和带有原型链的自定义类实例(只保留数据属性)。

  • 能用就优先用,性能好、无 bug 风险
  • 注意兼容性:旧环境需降级到 deepClone 函数或引入 polyfill(如 core-js 提供的 structuredClone 模拟)
  • Node.js 中需启用 --enable-structured-clone 标志(v18.13–v20.0),v20.1+ 默认开启

哪些情况会让深拷贝“看似成功”却出问题

最容易被忽略的是原型链丢失和不可枚举属性处理:

  • Object.create(null) 创建的对象没有 constructor__proto__,但多数手写 clone 会误当作普通 plain object 处理,导致后续 instanceof 判定失效
  • 使用 for...inObject.keys() 遍历时,会跳过不可枚举属性(如 Object.defineProperty(obj, 'x', { value: 1, enumerable: false })
  • 某些库(如 Vue 的响应式对象、JQuery 封装对象)内部有特殊属性或方法,盲目深拷贝可能破坏其行为,应优先使用其官方克隆方法(如 Vue reactive() 不应被深拷贝,而应重新创建)

循环引用只是深拷贝的显性门槛;真正难的是语义一致性——拷出来的对象是否在业务逻辑中“等价”。这点没有银弹,得看具体用在哪。