浅拷贝| 深拷贝

77 阅读5分钟

浅拷贝

浅拷贝是创建一个新对象,这个新对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。

常见的浅拷贝方法

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

console.log(obj2.b === obj1.b); // true (引用的是同一个对象)

obj2.a = 10;
obj2.b.c = 20;

console.log(obj1.a); // 1 (基本类型不受影响)
console.log(obj1.b.c); // 20 (引用类型被修改了)
2. 展开语法 (...)
const arr1 = [1, 2, { a: 3 }];
const arr2 = [...arr1];

console.log(arr2[2] === arr1[2]); // true

arr2[0] = 10;
arr2[2].a = 30;

console.log(arr1[0]); // 1
console.log(arr1[2].a); // 30
3. Array.prototype.slice() / Array.prototype.concat()

这些数组方法返回的都是一个新的浅拷贝数组。

深拷贝

深拷贝就是将一个对象在内存中在拷贝一份,两个对象相互不独立,不影响

1. JSON.parse(JSON.stringify(obj)) - 简单但有缺陷

这是最简单、最常用的“抖机灵”式深拷贝方法。

const obj1 = { 
  a: 1, 
  b: { c: 2 },
  d: new Date(),
  e: function() {},
  f: undefined,
  g: Symbol('g')
};
const obj2 = JSON.parse(JSON.stringify(obj1));

console.log(obj2.b === obj1.b); // false (已经是不同的对象了)

obj2.b.c = 20;
console.log(obj1.b.c); // 2 (未受影响)

优点

  • 代码极其简单,一行搞定。
  • 能处理绝大多数只包含 JSON 安全数据(字符串、数字、布尔、数组、普通对象)的场景。

致命缺陷

  1. 会忽略 undefined、Symbol 和函数
  2. 无法处理循环引用(一个对象的属性直接或间接引用了自身),会抛出 TypeError。
  3. 日期对象 (Date)  会被转换为字符串。
  4. 正则表达式 (RegExp)  会被转换为空对象。
2. structuredClone() - 现代浏览器的“官方答案”

这是一个新的、内置在浏览器和 Node.js 中的全局函数,专门用于深拷贝。

const obj1 = { 
  a: 1, 
  b: { c: 2 },
  d: new Date(),
  e: /abc/g
};
const obj2 = structuredClone(obj1);

console.log(obj2.b === obj1.b); // false
console.log(obj2.d); // Date 对象被正确拷贝
console.log(obj2.e); // RegExp 对象被正确拷贝

优点

  • 官方标准,语义清晰。
  • 功能强大:支持循环引用、支持多种内置类型(Date, RegExp, Map, Set, Blob, File 等)。
  • 性能优秀:底层由浏览器 C++ 实现,通常比手写的 JS 递归拷贝快得多。

缺点

  • 不能拷贝函数:会抛出 DataCloneError。
  • 不能拷贝原型链
  • 兼容性:是一个较新的 API,不支持一些老旧浏览器。
3. 使用第三方库,如 lodash.cloneDeep

这是生产环境中最可靠、最常用的选择。

import { cloneDeep } from 'lodash-es';

const obj1 = { a: 1, b: { c: 2 }, e: function() {} };
const obj2 = cloneDeep(obj1);

console.log(obj2.b === obj1.b); // false
console.log(typeof obj2.e); // "function" (函数也被正确处理)

优点

  • 功能最全面:处理了几乎所有的边缘情况,包括循环引用、各种内置类型、函数、原型链等。
  • 久经考验:在无数项目中被验证过,非常稳定。 面试官让你手写,是想考察你的递归思想、类型判断能力和对边缘情况的处理

下面是一个逐步完善的手写版本,你可以根据面试的时间和要求,展示不同层次的实现。

版本一:基础递归版 (只考虑普通对象和数组)

codeJavaScript

function deepClone(target) {
  // 1. 处理基本类型和 null
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 2. 判断是数组还是对象
  const newObj = Array.isArray(target) ? [] : {};

  // 3. 遍历 target 的所有属性
  for (const key in target) {
    // 4. 只拷贝自有属性,避免拷贝原型链上的属性
    if (Object.prototype.hasOwnProperty.call(target, key)) {
      // 5. 递归调用 deepClone
      newObj[key] = deepClone(target[key]);
    }
  }

  return newObj;
}

讲解要点

  1. 递归出口:首先处理非对象的情况,直接返回值。
  2. 类型判断:根据 Array.isArray 创建新的空数组或空对象。
  3. 遍历:使用 for...in 遍历所有可枚举属性。
  4. hasOwnProperty:关键一步!确保我们只拷贝对象自己的属性,而不是继承来的。
  5. 递归:对每个属性的值,再次调用 deepClone,实现深层拷贝。
版本二:处理循环引用 (进阶版)

上面的版本如果遇到循环引用,会陷入无限递归导致栈溢出。我们需要一个方法来“记住”已经拷贝过的对象。

codeJavaScript

function deepClone(target, map = new WeakMap()) {
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 1. 解决循环引用
  // 检查 map 中是否已经有当前对象的拷贝记录
  if (map.has(target)) {
    return map.get(target);
  }

  const newObj = Array.isArray(target) ? [] : {};

  // 2. 将新创建的对象存入 map,key 是原始对象,value 是新对象
  map.set(target, newObj);

  for (const key in target) {
    if (Object.prototype.hasOwnProperty.call(target, key)) {
      newObj[key] = deepClone(target[key], map); // 3. 递归时传递 map
    }
  }

  return newObj;
}

讲解要点

  1. 使用 WeakMap:WeakMap 的键是弱引用,当原始对象没有其他引用时,GC 可以回收它,避免了因拷贝而产生的内存泄漏。这是处理循环引用的最佳实践。
  2. 存取记录:在拷贝一个对象之前,先检查 map 里有没有。如果有,直接返回之前创建的拷贝。如果没有,就创建一个新的,并立即存入 map,然后再进行递归。
版本三:处理更多类型 (完整版)

一个更完整的实现,还需要考虑 Date, RegExp 等内置类型。

codeJavaScript

function deepClone(target, map = new WeakMap()) {
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 处理 Date
  if (target instanceof Date) {
    return new Date(target);
  }
  // 处理 RegExp
  if (target instanceof RegExp) {
    return new RegExp(target);
  }

  if (map.has(target)) {
    return map.get(target);
  }

  const newObj = Array.isArray(target) ? [] : {};
  map.set(target, newObj);

  // 使用 Reflect.ownKeys 可以拷贝 Symbol 类型的 key
  for (const key of Reflect.ownKeys(target)) {
    newObj[key] = deepClone(target[key], map);
  }

  return newObj;
}

讲解要点

  1. 类型检查:在递归前,增加对 Date, RegExp 等特殊对象的 instanceof 判断,并调用它们各自的构造函数来创建新实例。
  2. 遍历优化:使用 Reflect.ownKeys 代替 for...in + hasOwnProperty,可以拷贝包括 Symbol 类型在内的所有自有属性。