在 JavaScript 中复制和修改对象从来都不像看起来那么简单。了解对象和引用在此过程中的工作方式对于 Web 开发人员来说至关重要,并且可以节省数小时的调试时间。当您使用大型有状态应用程序(例如那些在 React 或 Vue 中构建的应用程序)时,这一点变得越来越重要。
浅拷贝和深拷贝指的是我们如何在 JavaScript 中复制一个对象,以及在"拷贝"中创建了哪些数据。在本文中,我们将深入研究这些方法之间的区别,探索它们在现实世界中的应用,并揭示使用它们时可能出现的潜在陷阱。让我们看看下面的代码示例。新创建的对象是通过扩展运算符shallowCopyZoo
创建为的副本,这导致了一些意想不到的后果。
let zoo = {
name: "Amazing Zoo",
location: "Melbourne, Australia",
animals: [
{
species: "Lion",
favoriteTreat: "rou",
},
{
species: "Panda",
favoriteTreat: "zhuzi",
},
],
};
let shallowCopyZoo = { ...zoo };
shallowCopyZoo.animals[0].favoriteTreat = "jitui";
console.log(zoo.animals[0].favoriteTreat);
// "jitui", not "rou"
但让我们看看真正的内容是什么shallowCopyZoo
。属性name
和location
是原始值(字符串),因此它们的值被复制。但是,该animals
属性是一个对象数组,因此复制的是对该数组的引用,而不是数组本身。
您可以使用严格相等运算符( )快速对此进行测试(如果您不相信我的话)===
。如果引用同一对象,则对象仅等于另一个对象(请参阅原始数据类型与引用数据类型)。请注意属性animals
在两者上是如何相等的,但对象本身并不相等。
console.log(zoo.animals === shallowCopyZoo.animals)
// true
console.log(zoo === shallowCopyZoo)
// false
这可能会导致代码库中出现潜在问题,并在处理大型对象时使工作变得特别困难修改浅拷贝中的嵌套对象也会影响原始对象和任何其他浅拷贝,因为它们都共享相同的引用。
深度复制
深度复制是一种创建新对象的技术,新对象是现有对象的精确副本。这包括复制其所有属性和任何嵌套对象,而不是引用。当您需要两个不共享引用的独立对象时,深度克隆很有用,可确保对一个对象的更改不会影响另一个对象。
在复杂应用程序中处理应用程序状态对象时,程序员经常使用深度克隆。在不影响先前状态的情况下创建新的状态对象对于维护应用程序的稳定性和正确实现撤消-重做功能至关重要。
如何使用 JSON.stringify() 和 JSON.parse() 进行深度复制
一种流行且无库的深度复制方式是使用内置的JSON stringify()
和parse()
方法。
parse (stringify()) 方法并不完美。例如,像这样的特殊数据类型Date
将被字符串化,而undefined
值将被忽略。与本文中的所有选项一样,应针对您的个人用例考虑它。
在下面的代码中,我们将创建一个deepCopy
函数这些方法来深度克隆一个对象。然后我们复制playerProfile
对象并修改复制的对象而不影响原始对象。这展示了深度复制在维护没有共享引用的单独对象方面的价值。
const playerProfile = {
name: 'Alice',
level: 10,
achievements: [
{
title: 'Fast Learner',
emoji: 'huojian'
},
{
title: 'Treasure Hunter',
emoji: 'money'
}
]
};
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: 'huojian'
},
{
title: 'Treasure Hunter',
emoji: 'money'
}
]
}
*/
// Modify the cloned profile without affecting the original profile
clonedProfile.achievements.push({ title: 'Marathon Runner', emoji: 'running' });
console.log(playerProfile.achievements.length); // Output: 2
console.log(clonedProfile.achievements.length); // Output: 3
深度复制库 {#h-libraries-for-deep-copying}
还有各种提供深度复制解决方案的第三方库。
-
正确处理循环引用、函数和特殊对象的Lodash 库
cloneDeep()
函数。 -
jQuery 库
extend()
[ deep = true] 函数 -
immer库在构建时考虑到了 React-Redux 开发人员,并为改变对象提供了方便的工具。
一个 Vanilla JS 深拷贝函数
如果出于某种原因您不想使用 JSON 对象或第三方库,您还可以在 vanilla 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()
可以更有效地处理深度复制,但为深度复制添加外部依赖项可能并不总是理想的。
结论
感谢您花时间阅读本文。浅拷贝与深拷贝比任何新手想象的都要复杂得多。尽管每种方法都有很多缺陷,但花时间审查和考虑这些选项将确保您的应用程序和数据完全符合您的要求。