怎样手写深拷贝?和 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写入复制后的描述符。 - 数据很深时会不会栈溢出?递归实现有调用栈深度限制,极深的结构可以改用显式栈迭代。