对象深拷贝的原理和代码实现【热度: 892】

关键词:对象什拷贝原理、避免循环引用和栈溢出

在 JavaScript 中,对象深拷贝指的是创建一个对象的副本,使得这个副本与原始对象不共享任何一个引用。这意味着,如果你修改了副本的属性,原始对象不会受到任何影响,反之亦然。

原理

在实现深拷贝时,有几个关键的概念需要理解:

  1. 值类型与引用类型:值类型(如数字、字符串和布尔值)直接存储数据的值,而引用类型(如对象、数组等)存储的是对一个内存地址的引用。

  2. 复制引用:如果你将一个对象赋值给一个新的变量,那么这个变量仅复制了对象的引用,而不是对象本身。因此,两个变量都指向同一个对象。

  3. 深度克隆:深拷贝则需要递归地复制对象中的每个属性,确保每个属性都是独立的副本,而不共享引用。

实现

实现对象的深拷贝有多种方式,以下是几种常见的实现方法:

1. JSON 方法

最简单的深拷贝方法之一是使用 JSON.stringify()JSON.parse()

function deepClone(value) {
  return JSON.parse(JSON.stringify(value));
}

但是,这种方法有局限性:

  • 它无法复制函数。
  • 它无法复制循环引用。
  • 它不会拷贝 undefined
  • 它无法处理特定属性(如 Symbol 属性、属性名为 Symbol 类型的属性等)。

2. 递归方法

你可以编写一个递归函数来复制每个属性:

function deepClone(value) {
  if (typeof value !== "object" || value === null) {
    return value; // 返回原始值类型
  }

  let result = Array.isArray(value) ? [] : {};
  for (let key in value) {
    // 使用 hasOwnProperty 检查以避免原型链中的键
    if (value.hasOwnProperty(key)) {
      // 递归复制每个属性值
      result[key] = deepClone(value[key]);
    }
  }
  return result;
}

这种方法的优点是它可以处理循环引用的拷贝,并且能够处理函数以外的所有类型的值。但它仍然有局限性,比如它不会拷贝对象的原型链。

补充进阶:避免循环引用和栈溢出的问题且支持拷贝原型链上的属性

为了避免循环引用和栈溢出的问题,我们可以在递归函数中加入一个缓存(通常是对象或 Map),来存储已经被拷贝过的引用类型对象。这样,当遇到一个已经被拷贝的引用类型时,我们可以使用缓存中的数据而不是再次进行拷贝。

下面是实现该思想的深拷贝函数示例:

function deepClone(value, map = new WeakMap()) {
  if (typeof value !== "object" || value === null) {
    return value; // 返回基本数据类型的值
  }

  // 检查是否为 Date、RegExp、Function 或循环引用
  if (value instanceof Date || value instanceof RegExp) {
    return value; // Created with built-in constructors, directly returned
  }

  // 如果 map 中已存在,则返回之前拷贝的对象,避免循环引用
  if (map.has(value)) {
    return map.get(value);
  }

  let result;
  if (Array.isArray(value)) {
    result = [];
    map.set(value, result);
    for (let i = 0; i < value.length; i++) {
      result[i] = deepClone(value[i], map); // 处理数组循环引用
    }
  } else {
    result = {};
    map.set(value, result);
    for (let key in value) {
      if (value.hasOwnProperty(key)) {
        result[key] = deepClone(value[key], map); // 递归复制每个属性
      }
    }
  }

  // 拷贝原型链上的属性
  // 根据需要可以取消以下注释
  // result.__proto__ = Object.getPrototypeOf(value);

  return result;
}

3. 使用第三方库

另一个选择是使用第三方库,如 Lodash,它提供了 _.cloneDeep 方法来实现深拷贝:

const _ = require("lodash");
const clone = _.cloneDeep(yourObject);

使用第三方库通常是最简单且最健壮的解决方案,因为它们已经考虑到了各种边缘情况,并包含了更高级的拷贝功能。

注意

无论选择哪种方法,都需要注意的是,深拷贝可能无法复制具有特定属性的对象,如:

  • Function 对象
  • Map 和 Set 对象
  • React 组件
  • 日期对象
  • 正则表达式对象
  • 以及一些其他通过构造函数创建的对象,可能会丢失它们的框架或库特定的属性或方法。

在实现深拷贝时,需要根据实际情况调整和选择使用的方法。