首页 > web前端 > js教程 > 浅与深层复制的JavaScript

浅与深层复制的JavaScript

Jennifer Aniston
发布: 2025-02-09 08:28:10
原创
930 人浏览过

Shallow vs. Deep Copying in JavaScript

JavaScript 对象的复制并非表面看起来那么简单。理解此过程中对象和引用的工作方式对于 Web 开发人员至关重要,可以节省数小时的调试时间。当您使用大型有状态应用程序(例如在 React 或 Vue 中构建的应用程序)时,这一点变得越来越重要。

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

关键要点

  • 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); 
// "?",而不是 "?"
登录后复制
登录后复制

但是让我们看看 shallowCopyZoo 中究竟是什么。属性 namelocation 是原始值(字符串),因此它们的数值被复制。但是,animals 属性是对象的数组,因此复制的是对该数组的引用,而不是数组本身。

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

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); 
// "?",而不是 "?"
登录后复制
登录后复制

这可能导致代码库中出现潜在问题,并且在处理大型修改时尤其困难。修改浅副本中的嵌套对象也会影响原始对象和任何其他浅副本,因为它们都共享相同的引用。

深复制

深复制是一种创建新对象的技巧,该对象是现有对象的精确副本。这包括复制其所有属性和任何嵌套对象,而不是引用。当您需要两个不共享引用的独立对象时,深克隆很有用,确保对一个对象的更改不会影响另一个对象。

程序员在处理复杂应用程序中的应用程序状态对象时经常使用深克隆。创建新的状态对象而不影响先前状态对于维护应用程序的稳定性和正确实现撤消/重做功能至关重要。

如何使用 JSON.stringify()JSON.parse() 进行深复制

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

parse(stringify()) 方法并不完美。例如,Date 等特殊数据类型将被转换为字符串,未定义的值将被忽略。与本文中的所有选项一样,应根据您的具体用例进行考虑。

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

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

console.log(zoo === shallowCopyZoo)
// false
登录后复制

用于深复制的库

还有一些各种第三方库提供了深复制解决方案。

  • Lodash 库的 cloneDeep() 函数可以正确处理循环引用、函数和特殊对象。
  • jQuery 库的 extend() [deep = true] 函数
  • immer 库是为 React-Redux 开发人员构建的,并提供用于修改对象的便捷工具。

Vanilla JS 深复制函数

如果由于某种原因您不想使用 JSON 对象或第三方库,您也可以在 Vanilla JavaScript 中创建一个自定义深复制函数。该函数递归迭代对象属性并创建一个具有相同属性和值的新对象。

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);
// 输出与playerProfile相同

// 修改克隆的配置文件而不影响原始配置文件
clonedProfile.achievements.push({ title: 'Marathon Runner', emoji: '?' });
console.log(playerProfile.achievements.length); // 输出:2
console.log(clonedProfile.achievements.length); // 输出:3
登录后复制

深复制的缺点

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

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

结论

感谢您抽出时间阅读本文。浅复制与深复制比任何初学者想象的都要复杂得多。尽管每种方法都有很多陷阱,但花时间审查和考虑这些选项将确保您的应用程序和数据保持您想要的样子。

关于 JavaScript 中浅复制与深复制的常见问题解答 (FAQ)

JavaScript 中浅复制和深复制的主要区别是什么?

浅复制和深复制之间的主要区别在于它们处理作为对象的属性的方式。在浅复制中,复制的对象与原始对象共享对嵌套对象的相同引用。这意味着对嵌套对象的更改将反映在原始对象和复制对象中。另一方面,深复制会创建嵌套对象的新实例,这意味着对复制对象中嵌套对象的更改不会影响原始对象。

展开运算符如何在浅复制中工作?

JavaScript 中的展开运算符(…)通常用于浅复制。它将一个对象的所有可枚举自身属性复制到另一个对象。但是,它只复制第一级的属性和嵌套对象的引用。因此,对嵌套对象的更改将影响原始对象和复制的对象。

我可以使用 JSON 方法进行深复制吗?

是的,您可以使用 JSON 方法在 JavaScript 中进行深复制。JSON.stringify()JSON.parse() 方法的组合可以创建对象的深复制。JSON.stringify() 将对象转换为字符串,JSON.parse() 将字符串解析回新对象。但是,此方法有一些限制,因为它不会复制方法,并且不适用于 DateRegExpMapSet 等特殊 JavaScript 对象。

浅复制的局限性是什么?

浅复制只复制第一级的属性和嵌套对象的引用。因此,如果原始对象包含嵌套对象,则对这些嵌套对象的更改将影响原始对象和复制的对象。这可能导致代码中出现意外的结果和错误。

Object.assign() 方法如何在浅复制中工作?

Object.assign() 方法用于将一个或多个源对象的所有可枚举自身属性的值复制到目标对象。它返回目标对象。但是,它执行浅复制,这意味着它只复制第一级的属性和嵌套对象的引用。

在 JavaScript 中深复制对象的最佳方法是什么?

在 JavaScript 中深复制对象的最佳方法取决于代码的具体要求。如果您的对象不包含方法或特殊的 JavaScript 对象,您可以使用 JSON.stringify()JSON.parse() 方法的组合。对于更复杂的对象,您可能需要使用 Lodash 等库,该库提供深克隆函数。

我可以使用展开运算符进行深复制吗?

不可以,JavaScript 中的展开运算符只执行浅复制。它复制第一级的属性和嵌套对象的引用。要执行深复制,您需要使用其他方法或库。

深复制的性能影响是什么?

深复制可能比浅复制更消耗资源,尤其对于大型对象而言。这是因为深复制为所有嵌套对象创建新实例,这可能会占用更多内存和处理能力。

深复制如何处理循环引用?

JSON.stringify()JSON.parse() 等深复制方法不处理循环引用,并将抛出错误。循环引用发生在一个对象的属性引用该对象本身时。要处理循环引用,您需要使用支持它的库,例如 Lodash。

为什么我应该关心浅复制和深复制之间的区别?

了解浅复制和深复制之间的区别对于管理 JavaScript 中的数据至关重要。它会影响您的对象如何相互交互。如果您不小心,浅复制会导致意外的结果和错误,因为对嵌套对象的更改会影响原始对象和复制的对象。另一方面,深复制确保您的复制对象与原始对象完全独立。

以上是浅与深层复制的JavaScript的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板