深拷贝:怎么定义“深”?structuredClone、JSON、手写递归各有什么坑?
面试速答(30 秒版 TL;DR)
- 浅拷贝只复制第一层引用;深拷贝要递归复制可达子对象,使新旧对象互不影响。
- 最推荐的通用方案是
structuredClone(现代浏览器/Node 支持较好),能处理循环引用、Map/Set、Date、ArrayBuffer等,但不能克隆函数、DOM 节点等。 JSON.parse(JSON.stringify(obj))只适合“纯数据对象”,会丢失undefined、函数、Symbol,并把Date变字符串,RegExp变空对象等。- 手写深拷贝要处理:循环引用(
WeakMap)、特殊对象类型(Date/RegExp/Map/Set)、原型链、属性描述符与不可枚举属性(取舍需说明)。
什么叫“深拷贝”
面试一句话:
深拷贝的目标是:拷贝后修改任意层级都不影响原对象,且引用图结构被复制出来(必要时保留循环结构)。
首选:structuredClone
const a = { x: 1, d: new Date() };
a.self = a;
const b = structuredClone(a);
console.log(b !== a); // true
console.log(b.self === b); // true(循环结构被保留)
面试补充:
- 如果你要克隆的是“可结构化克隆”的数据,这是最省心的方案。
- 不支持函数是设计选择:函数带闭包环境与可执行语义,不是纯数据。
JSON 方案:为什么面试一般不推荐当“通用深拷贝”
const a = {
d: new Date(),
u: undefined,
f: () => 1,
r: /a/g,
};
const b = JSON.parse(JSON.stringify(a));
console.log(typeof b.d); // "string"
console.log("u" in b); // false
console.log("f" in b); // false
console.log(b.r); // {}
手写:一个可用的深拷贝(面试可写)
取舍说明(建议在面试里说出来):这个版本覆盖常见类型并处理循环引用,不复制属性描述符,不处理 DOM/函数。
function deepClone(value, cache = new WeakMap()) {
if (typeof value !== "object" || value === null) return value;
if (cache.has(value)) return cache.get(value);
if (value instanceof Date) return new Date(value.getTime());
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
if (value instanceof Map) {
const m = new Map();
cache.set(value, m);
for (const [k, v] of value) m.set(deepClone(k, cache), deepClone(v, cache));
return m;
}
if (value instanceof Set) {
const s = new Set();
cache.set(value, s);
for (const v of value) s.add(deepClone(v, cache));
return s;
}
const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
cache.set(value, out);
for (const key of Reflect.ownKeys(value)) {
const desc = Object.getOwnPropertyDescriptor(value, key);
if (!desc) continue;
if ("value" in desc) desc.value = deepClone(desc.value, cache);
Object.defineProperty(out, key, desc);
}
return out;
}
典型题 & 标准答法
Q1:为什么需要 WeakMap?
为了解决循环引用和重复引用:
- 循环引用:避免无限递归
- 重复引用:保证引用关系一致(同一对象在拷贝后仍指向同一拷贝实例)
Q2:深拷贝能不能拷贝函数?
一般不做。函数不仅是代码,还包含闭包环境、原型、可执行语义;“复制函数”通常等价于保持同一个函数引用,或通过源码重建(不可靠且不安全)。
易错点/坑
Object.assign、展开运算符{...obj}、Array.prototype.slice都是浅拷贝。structuredClone抛错并不代表你的数据“不能拷贝”,而是有不可结构化类型;要么换策略,要么先做数据化。
速记要点(可背诵)
- 通用首选:
structuredClone。 - JSON 只适合纯数据;丢类型、丢值、丢符号。
- 手写要点:循环引用用
WeakMap,特殊类型要分支处理。