前端知库

怎样手写深拷贝?和 structuredClone 有什么区别?

深拷贝要递归复制对象,用 WeakMap 记录已复制的对象来处理循环引用;Date、RegExp、Map、Set 要按类型单独构造;用 Reflect.ownKeys 才能拷到 Symbol 键。structuredClone 原生支持循环引用和多数内置类型,但会丢弃 Symbol 键和原型,遇到函数会抛出 DataCloneError。

参考答案

简答

  • 原始值和函数直接返回;对象先查 WeakMap,复制过就返回已有副本,这样循环引用和共享引用都能保持原来的结构。
  • 副本要在递归子属性之前写入 WeakMap,否则子属性指回自身时会无限递归。
  • Date、RegExp、Map、Set 的数据不在普通属性上,需要按类型新建;Map 的键和值都要递归复制。
  • Object.keys 和 for...in 拿不到 Symbol 键,要用 Reflect.ownKeys 再筛选可枚举属性。用 Object.create(原型) 创建副本可以保留类实例的方法。
  • structuredClone 能处理循环引用、Date、Map、Set 等,但不复制 Symbol 键,类实例会变成普通对象,函数和 DOM 节点会抛出 DataCloneError。

实现

JavaScript
function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== 'object') return value;
  if (seen.has(value)) return seen.get(value);

  if (value instanceof Date) return new Date(value.getTime());
  if (value instanceof RegExp) {
    const copy = new RegExp(value.source, value.flags);
    copy.lastIndex = value.lastIndex;
    return copy;
  }
  if (value instanceof Map) {
    const copy = new Map();
    seen.set(value, copy);
    value.forEach((v, k) => copy.set(deepClone(k, seen), deepClone(v, seen)));
    return copy;
  }
  if (value instanceof Set) {
    const copy = new Set();
    seen.set(value, copy);
    value.forEach((v) => copy.add(deepClone(v, seen)));
    return copy;
  }

  const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
  // 先登记再递归,子属性引用自身时能拿到这个副本
  seen.set(value, copy);
  for (const key of Reflect.ownKeys(value)) {
    const descriptor = Object.getOwnPropertyDescriptor(value, key);
    if (descriptor.enumerable) copy[key] = deepClone(value[key], seen);
  }
  return copy;
}

用 WeakMap 而不是 Map,是因为它只在本次拷贝期间使用,键是对象,不会阻止原对象被回收。

验证

JavaScript
const id = Symbol('id');
const source = {
  date: new Date(0),
  map: new Map([['k', { n: 1 }]]),
  set: new Set([1]),
  [id]: 'sym',
  list: [1, { x: 2 }],
};
source.self = source;

const copy = deepClone(source);
console.log(copy.self === copy); // true
console.log(copy.date !== source.date && copy.date instanceof Date); // true
console.log(copy.map.get('k') !== source.map.get('k')); // true
console.log(copy[id]); // 'sym'

const native = structuredClone(source);
console.log(native.self === native); // true
console.log(native[id]); // undefined

structuredClone 的边界

情况deepClone(上面实现)structuredClone
循环引用支持支持
Date、RegExp、Map、Set支持支持
Symbol 键复制丢弃
类实例的原型保留变成普通对象
函数共享原函数抛出 DataCloneError
ArrayBuffer、TypedArray、Blob 等未处理支持

实际项目里复制纯数据优先用 structuredClone;它和 JSON 方案、浅拷贝的取舍见《浅拷贝、深拷贝和 structuredClone 如何选择?》。

常见追问

  • 为什么函数不复制?函数可能引用闭包里的变量,无法可靠地复制这些变量,通常直接共享。
  • getter 和不可枚举属性怎么办?上面的实现读取 getter 的当前值并跳过不可枚举属性。需要保留完整属性描述符时,改用 Object.defineProperty 写入复制后的描述符。
  • 数据很深时会不会栈溢出?递归实现有调用栈深度限制,极深的结构可以改用显式栈迭代。