JavaScript 中的浅拷贝和深拷贝

63 阅读6分钟

在 JavaScript 中复制和修改对象从来都不像看起来那么简单。理解对象和引用在此过程中的工作方式对于网页开发人员来说至关重要,并且可以节省大量调试时间。当您使用像 React 或 Vue 这样的大型状态应用程序时,这变得越来越重要。

浅拷贝和深拷贝是指在JavaScript中如何复制对象以及在‘复制’中创建的数据。在本文中,我们将深入探讨这些方法之间的区别,探索它们在现实世界中的应用,并揭示在使用它们时可能出现的潜在陷阱。

什么是“浅”复制?

浅拷贝是指创建一个新对象,该对象是现有对象的副本,其属性与原始对象引用相同的值或对象。在JavaScript中,可以使用Object.assign()或展开运算符({...originalObject})等方法来实现浅拷贝。浅拷贝只是创建了对现有对象或值的新引用,而没有创建深层次的副本,这意味着嵌套对象仍然是引用而非副本。

让我们看下面的代码示例。通过展开运算符,新创建的对象shallowCopyZoo作为zoo的副本被创建,但却导致了一些意外的后果。

let zoo = {
  name: "Amazing Zoo",
  location: "Melbourne, Australia",
  animals: [
    {
      species: "Lion",
      favoriteTreat: "🥩",
    },
    {
      species: "Panda",
      favoriteTreat: "🎋",
    },
  ],
};

let shallowCopyZoo = { ...zoo };
shallowCopyZoo.animals[0].favoriteTreat = "🍖";
console.log(zoo.animals[0].favoriteTreat); 
// "🍖", not "🥩"

但是让我们看看浅拷贝实际上拷贝了什么。属性name和location是原始值(字符串),因此它们的值被拷贝了。然而,属性animals是一个对象数组,因此它的引用被拷贝,而不是数组本身。

你可以快速测试这一点(如果你不相信我)使用严格相等运算符(===)。一个对象只有在引用相同的对象时才等于另一个对象(参见原始数据类型 vs. 引用数据类型)。请注意,属性animals在两个对象上是相等的,但对象本身不相等。

console.log(zoo.animals === shallowCopyZoo.animals)
// true

console.log(zoo === shallowCopyZoo)
// false

这可能会在代码库中引起潜在的问题,并且在处理大型的、有状态的应用程序(如React或Vue)时会使开发人员的生活变得更加困难。在浅拷贝中修改嵌套对象也会影响原始对象和所有浅拷贝,因为它们都共享同一引用。

深拷贝

深拷贝是一种创建新对象的技术,该对象是现有对象的精确副本,包括所有属性和任何嵌套对象,而不是引用。深拷贝非常有用,因为它能够创建两个不共享引用的独立对象,确保对一个对象所做的更改不会影响另一个对象。

当程序员在复杂的应用程序中使用应用程序状态对象时,通常会使用深拷贝。创建一个新的状态对象而不影响先前的状态对于保持应用程序的稳定性和正确实现撤消/重做功能非常重要。

如何使用JSON.stringify()和JSON.parse()进行深拷贝

一种流行且无需使用库的深复制方式是使用内置的JSON stringify()和parse()方法。

parse(stringify())方法并非完美。例如,特殊的数据类型如Date将被字符串化,未定义的值将被忽略。和本文介绍的所有方法一样,应根据你的具体情况进行选择。

在下面的代码中,我们将创建一个deepCopy函数,使用这些方法来深度克隆一个对象。然后,我们将复制playerProfile对象并修改复制的对象,而不影响原始对象。这展示了深复制在维护不共享引用的分离对象方面的价值。

const playerProfile = {
  name: 'Alice',
  level: 10,
  achievements: [
    {
      title: 'Fast Learner',
      emoji: '🚀'
    },
    {
      title: 'Treasure Hunter',
      emoji: '💰'
    }
  ]
};

function deepCopy(obj) {
  return JSON.parse(JSON.stringify(obj));
}

const clonedProfile = deepCopy(playerProfile);

console.log(clonedProfile);
/* Output:
{
  name: 'Alice',
  level: 10,
  achievements: [
    {
      title: 'Fast Learner',
      emoji: '🚀'
    },
    {
      title: 'Treasure Hunter',
      emoji: '💰'
    }
  ]
}
*/

// Modify the cloned profile without affecting the original profile
clonedProfile.achievements.push({ title: 'Marathon Runner', emoji: '🏃' });
console.log(playerProfile.achievements.length); // Output: 2
console.log(clonedProfile.achievements.length); // Output: 3

深拷贝库

还有许多第三方库提供深拷贝解决方案。

  • Lodash库的cloneDeep()函数可以正确处理循环引用、函数和特殊对象。
  • jQuery库的extend()函数可以选择深拷贝[deep=true]。
  • immer库是专为React-Redux开发人员设计的,提供了方便的对象变异工具。

一个 Vanilla JS 的深拷贝函数

如果出于某种原因您不想使用 JSON 对象或第三方库,您也可以在纯 JavaScript 中创建一个自定义深拷贝函数。该函数会递归地遍历对象属性,并创建具有相同属性和值的新对象。

const deepCopy = (obj) => {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }

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

    for (const key in obj) {
        newObj[key] = deepCopy(obj[key]);
    }

    return newObj;
}

const deepCopiedObject = deepCopy(originalObject);

深拷贝的缺点

虽然深度拷贝为数据准确性提供了很大的好处,但建议在每个特定的使用情况下评估是否需要深度拷贝。在某些情况下,浅拷贝或其他管理对象引用的技术可能更适合,提供更好的性能和降低复杂性。

  • 性能影响:深度拷贝可能具有计算上的昂贵性,特别是在处理大型或复杂对象时。由于深度拷贝过程需要迭代所有嵌套属性,因此可能需要大量时间,从而对应用程序的性能产生负面影响。
  • 内存消耗:创建深层副本会导致整个对象层次结构(包括所有嵌套对象)的复制。这可能导致内存使用量增加,这可能在内存受限环境或处理大型数据集时会出现问题。
  • 循环引用:深度拷贝可能会在对象包含循环引用时(即对象具有指向自身的属性,直接或间接地)导致问题。循环引用可能导致在深度拷贝过程中出现无限循环或堆栈溢出错误,处理它们需要额外的逻辑以避免这些问题。
  • 函数和特殊对象处理:深度拷贝可能无法正确处理具有特殊特征的函数或对象(例如,Date、RegExp、DOM元素)。例如,当深度拷贝包含函数的对象时,函数的引用可能会被复制,但函数的闭包和绑定的上下文将不会被复制。类似地,具有特殊特征的对象在深度拷贝时可能会失去其独特的属性和行为。
  • 实现复杂性:编写自定义的深度拷贝函数可能会很复杂,而内置方法如JSON.parse(JSON.stringify(obj))具有局限性,例如无法正确处理函数、循环引用或特殊对象。虽然有第三方库如Lodash的_.cloneDeep()可以更有效地处理深度拷贝,但添加外部依赖项以进行深度拷贝可能并不总是理想的。