Skip to content

深拷贝 vs 浅拷贝:值复制还是引用穿透?递归与循环引用的破解之道

“拷贝的本质,不是数据的复制,而是引用关系的解构与重建——浅拷贝是‘表面复制’,深拷贝是‘递归解构’。”

当你写下:

js
const copy = Object.assign({}, obj);

你可能以为 copyobj 完全独立。

但真相是:
如果 obj 包含嵌套对象或数组,
copy 中的这些属性仍然指向原对象的引用——
一次对 copy.nested.x 的修改,可能悄然改变 obj.nested.x

我们来从 拷贝的语义定义、浅拷贝的局限、深拷贝的递归实现、循环引用的破解、现代解决方案 五个维度,彻底拆解:

一、拷贝的两种语义:浅拷贝 vs 深拷贝

浅拷贝(Shallow Copy)

只复制对象的第一层属性,
对于引用类型(对象、数组),复制的是指针(引用),而非其内容。

js
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)

递归复制对象的所有层级,
所有嵌套的引用类型都被完全重建,形成独立的副本。

js
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)

js
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'] —— 被污染

浅拷贝只“复制表皮”,不“解剖内脏”。

三、深拷贝的递归实现:基础版本

js
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;
}

测试

js
const obj = { a: 1, nested: { x: 1 } };
const copy = simpleDeepClone(obj);
copy.nested.x = 2;
console.log(obj.nested.x); // 1 —— 成功隔离

四、挑战 1:循环引用(Circular References)

js
const obj = { name: 'Alice' };
obj.self = obj; // 自引用
obj.friend = obj; // 循环引用

const copy = simpleDeepClone(obj); // ❌ 栈溢出!

因为递归会无限进行:

clone(obj)
  → clone(obj.self)
     → clone(obj.self.self)
        → ...

破解之道:使用 WeakMap 缓存已克隆对象

js
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;
}

测试循环引用

js
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 对象

js
if (obj instanceof Date) {
  return new Date(obj);
}

2. RegExp 对象

js
if (obj instanceof RegExp) {
  return new RegExp(obj.source, obj.flags);
}

3. 函数

函数通常不应“拷贝”,但可选择:

  • 返回原函数(共享)
  • 返回 undefined(忽略)
  • 抛出错误
js
if (typeof obj === 'function') {
  return obj; // 或 throw new Error('Cannot clone function');
}

4. MapSet

js
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) 是浏览器原生的深拷贝方法,
支持 DateRegExpMapSetArrayBuffer 等,
并自动处理循环引用。

js
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

限制:

  • 不支持函数、ErrorDOM 节点
  • Node.js 支持较晚(v17+)

七、JSON.parse(JSON.stringify()) 的陷阱

常被误用为深拷贝方案:

js
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 中的‘拷贝’本质是引用图的遍历与重建
浅拷贝是‘浅层遍历’,深拷贝是‘深度遍历+状态缓存’,
而循环引用是这张图中的‘环’,必须用缓存来打破。”

你不再只是“复制数据”,
而是在操作对象图的拓扑结构