浅拷贝与深拷贝

1. 概念与区别

(1) 浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

  • 如果属性是基本类型,拷贝的就是基本类型的值。
  • 如果属性是引用类型,拷贝的就是内存地址 (引用)。如果其中一个对象改变了这个地址,就会影响到另一个对象。

实现方式

  • Object.assign
  • 展开运算符 ...
  • Array.prototype.slice() / concat() (针对数组)

(2) 深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

实现方式

  • JSON.parse(JSON.stringify()) (最简单,有缺陷)
  • structuredClone (现代浏览器 API)
  • 手写递归 (最稳妥)
  • 第三方库 lodash.cloneDeep

2. 浅拷贝示例 (面试手写概率较低)

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1);

obj1.a = 3;  // obj2.a 不变 (1)
obj1.b.c = 4; // obj2.b.c 变为 4 (引用共享)

3. 深拷贝实现方案 (面试重点)

(1) JSON 序列化

const obj2 = JSON.parse(JSON.stringify(obj1));

缺陷 (高频考点)

  1. 忽略 undefined 属性。
  2. 忽略 Symbol 属性。
  3. 忽略 function (会将函数变为 undefined 或者抛错)。
  4. Date 对象变为字符串。
  5. RegExp 对象变为 {}
  6. 循环引用 会报错 (Converting circular structure to JSON)。

(2) structuredClone (ES2022)

const obj2 = structuredClone(obj1);

优点:解决了 JSON 方案的循环引用和大部分特殊类型丢失问题 (支持 Date, RegExp, Map, Set, ArrayBuffer 等)。 缺陷

  1. 不支持 function (无法克隆函数)。
  2. 不支持 DOM 节点
  3. 如果不兼容旧环境,需 polyfill。

(3) 手写递归 DeepClone

能够处理循环引用是一个关键加分项 (使用 WeakMap)。

function deepClone(obj, hash = new WeakMap()) {
  // 1. 处理 null 和 基本类型
  if (obj === null || typeof obj !== 'object') return obj;

  // 2. 处理 Date 和 RegExp
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);

  // 3. 处理循环引用 (如果之前拷贝过,直接返回引用)
  if (hash.has(obj)) return hash.get(obj);

  // 4. 创建新对象 (保持原型链,兼容 Array 和 Object)
  // 或者 const cloneObj = Array.isArray(obj) ? [] : {};
  const cloneObj = new obj.constructor();
  
  // 5. 将当前对象存入 hash,防止循环引用死循环
  hash.set(obj, cloneObj);

  // 6. 递归拷贝属性 (使用 Reflect.ownKeys 可遍历 Symbol)
  // for...in 只遍历可枚举属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  return cloneObj;
}

4. 面试总结

  • 浅拷贝:一层复制,引用共享。(Object.assign, ...)
  • 深拷贝:完全独立。(JSON, structuredClone, 递归)
  • JSON 缺陷:忽略 undefined/symbol/function,不支持循环引用。
  • 手写要点:递归 + 类型判断 + WeakMap 防循环引用。