深拷贝 vs 浅拷贝:值复制还是引用穿透?递归与循环引用的破解之道
“拷贝的本质,不是数据的复制,而是引用关系的解构与重建——浅拷贝是‘表面复制’,深拷贝是‘递归解构’。”
当你写下:
const copy = Object.assign({}, obj);你可能以为 copy 与 obj 完全独立。
但真相是:
如果 obj 包含嵌套对象或数组,copy 中的这些属性仍然指向原对象的引用——
一次对 copy.nested.x 的修改,可能悄然改变 obj.nested.x。
我们来从 拷贝的语义定义、浅拷贝的局限、深拷贝的递归实现、循环引用的破解、现代解决方案 五个维度,彻底拆解:
一、拷贝的两种语义:浅拷贝 vs 深拷贝
浅拷贝(Shallow Copy)
只复制对象的第一层属性,
对于引用类型(对象、数组),复制的是指针(引用),而非其内容。
const obj = { a: 1, nested: { x: 1 } };
const shallow = Object.assign({}, obj);
shallow.nested.x = 2;
console.log(obj.nested.x); // 2 —— 被意外修改!shallow.a是值复制(原始值)shallow.nested是引用复制(指向同一对象)
深拷贝(Deep Copy)
递归复制对象的所有层级,
所有嵌套的引用类型都被完全重建,形成独立的副本。
const deep = deepClone(obj);
deep.nested.x = 3;
console.log(obj.nested.x); // 1 —— 未受影响深拷贝的目标是:完全切断新旧对象之间的引用联系。
二、浅拷贝的实现方式与局限
常见浅拷贝方法
| 方法 | 说明 |
|---|---|
Object.assign({}, obj) | 标准方式,仅复制可枚举自有属性 |
{...obj} | 展开语法,同 Object.assign |
Array.prototype.slice() | 数组浅拷贝 |
Array.from(arr) | 数组浅拷贝 |
局限性:引用穿透(Reference Tunneling)
const user = {
name: 'Alice',
settings: {
theme: 'dark',
prefs: ['a', 'b']
}
};
const copy = { ...user };
copy.settings.theme = 'light';
copy.settings.prefs.push('c');
console.log(user.settings.theme); // 'light' —— 被污染
console.log(user.settings.prefs); // ['a', 'b', 'c'] —— 被污染浅拷贝只“复制表皮”,不“解剖内脏”。
三、深拷贝的递归实现:基础版本
function simpleDeepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj; // 原始值直接返回
}
if (Array.isArray(obj)) {
return obj.map(item => simpleDeepClone(item));
}
const cloned = {};
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
cloned[key] = simpleDeepClone(obj[key]);
}
}
return cloned;
}测试
const obj = { a: 1, nested: { x: 1 } };
const copy = simpleDeepClone(obj);
copy.nested.x = 2;
console.log(obj.nested.x); // 1 —— 成功隔离四、挑战 1:循环引用(Circular References)
const obj = { name: 'Alice' };
obj.self = obj; // 自引用
obj.friend = obj; // 循环引用
const copy = simpleDeepClone(obj); // ❌ 栈溢出!因为递归会无限进行:
clone(obj)
→ clone(obj.self)
→ clone(obj.self.self)
→ ...破解之道:使用 WeakMap 缓存已克隆对象
function deepClone(obj, cache = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 如果已克隆过,直接返回缓存副本
if (cache.has(obj)) {
return cache.get(obj);
}
let cloned;
if (Array.isArray(obj)) {
cloned = [];
cache.set(obj, cloned); // 先缓存,防止循环
for (let item of obj) {
cloned.push(deepClone(item, cache));
}
} else {
cloned = {};
cache.set(obj, cloned);
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
cloned[key] = deepClone(obj[key], cache);
}
}
}
return cloned;
}测试循环引用
const obj = { name: 'Alice' };
obj.self = obj;
const copy = deepClone(obj);
console.log(copy === copy.self); // true
console.log(copy === obj); // false —— 独立对象WeakMap 的优势: 键是对象,自动垃圾回收 不阻止原对象被回收 避免内存泄漏
五、挑战 2:特殊对象类型
基础实现无法处理:
1. Date 对象
if (obj instanceof Date) {
return new Date(obj);
}2. RegExp 对象
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}3. 函数
函数通常不应“拷贝”,但可选择:
- 返回原函数(共享)
- 返回
undefined(忽略) - 抛出错误
if (typeof obj === 'function') {
return obj; // 或 throw new Error('Cannot clone function');
}4. Map 和 Set
if (obj instanceof Map) {
const cloned = new Map();
cache.set(obj, cloned);
for (let [k, v] of obj) {
cloned.set(deepClone(k, cache), deepClone(v, cache));
}
return cloned;
}
if (obj instanceof Set) {
const cloned = new Set();
cache.set(obj, cloned);
for (let v of obj) {
cloned.add(deepClone(v, cache));
}
return cloned;
}六、现代解决方案:structuredClone
来自 HTML Standard:
structuredClone(value)是浏览器原生的深拷贝方法,
支持Date、RegExp、Map、Set、ArrayBuffer等,
并自动处理循环引用。
const obj = { date: new Date(), map: new Map([['a', 1]]) };
obj.self = obj;
const copy = structuredClone(obj);
console.log(copy.date instanceof Date); // true
console.log(copy.map instanceof Map); // true
console.log(copy === copy.self); // true限制:
- 不支持函数、
Error、DOM节点 - Node.js 支持较晚(v17+)
七、JSON.parse(JSON.stringify()) 的陷阱
常被误用为深拷贝方案:
const copy = JSON.parse(JSON.stringify(obj));问题:
| 类型 | 行为 |
|---|---|
undefined | 丢失 |
Symbol | 丢失 |
| 函数 | 丢失 |
Date | 变为字符串 |
RegExp | 变为 {} |
Map/Set | 变为 {} 或 [] |
| 循环引用 | 报错 TypeError |
BigInt | 报错 |
仅适用于纯 JSON 数据(原始值、数组、普通对象)。
一句话总结
浅拷贝只复制第一层,嵌套引用仍共享;深拷贝需递归重建所有层级,并用 WeakMap 缓存解决循环引用。
- 浅拷贝:
Object.assign、{...}—— 快速但不彻底 - 深拷贝:递归 +
WeakMap缓存 —— 完整但复杂 - 现代方案:
structuredClone—— 推荐使用 - 避免:
JSON.parse(JSON.stringify())—— 陷阱众多
结语:理解拷贝,就是理解“引用与值的边界”
大多数人认为“复制对象”是简单的数据搬运,
而你理解的是:
“JavaScript 中的‘拷贝’本质是引用图的遍历与重建;
浅拷贝是‘浅层遍历’,深拷贝是‘深度遍历+状态缓存’,
而循环引用是这张图中的‘环’,必须用缓存来打破。”
你不再只是“复制数据”,
而是在操作对象图的拓扑结构。