在 JavaScript 中复制和修改对象从来都不像看起来那么简单。了解对象和引用在此过程中的工作方式对于 Web 开发人员来说至关重要,并且可以节省大量的调试时间。当您创建大型有状态应用程序(例如那些在 React 或 Vue 中构建的应用程序)时,这一点变得越来越重要。
浅拷贝和深拷贝指的是我们如何在 JavaScript 中复制一个对象,以及在“拷贝”中创建了哪些数据。在本文中,我们将深入研究这些方法之间的区别,探索它们在现实世界中的应用,并揭示使用它们时可能出现的潜在缺陷。
什么是浅拷贝?
浅拷贝是指创建一个新对象的过程,该新对象是现有对象的副本,新对象的属性、引用与原始对象相同。在 JavaScript 中,通常会使用诸如扩展语法{...originalObject}之类的方法来实现对象的浅拷贝。浅拷贝只会创建对现有对象或值的新引用,不会创建深拷贝,这意味着嵌套对象仍然被引用,而不是被复制:
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);
// "🍖", 不是期望值 "🥩",新对象改变了引用类型animals的值,原对象也受到了影响
上述代码,shallowCopyZoo 的属性 name 和 location 是原始值(除了 Object 以外都为原始值),因此它们的值被复制。但是,该 animals 属性是一个对象数组,因此复制的是对该数组的引用,而不是数组本身。
我们可以使用严格相等运算符(===)来检查新旧对象及引用内容是否一致:
console.log(zoo.animals === shallowCopyZoo.animals)
// true
console.log(zoo === shallowCopyZoo)
// false
通过检查,我们可以发现,新旧对象本身是不相等的,但是他们的 animals 却是相等的。浅拷贝可能会导致代码库中出现潜在问题,并在处理大型对象时使工作变得特别困难,修改浅拷贝中的嵌套对象也会影响原始对象和任何其他浅拷贝,因为它们都共享相同的引用。
深拷贝
深度复制是一种创建新对象的技术,新对象是现有对象的精确副本。这包括复制其所有属性和任何嵌套对象,而不是引用。当您需要两个不共享引用的独立对象时,深度克隆很有用,可确保对一个对象的更改不会影响另一个对象。
在复杂应用程序中处理应用程序状态对象时,程序员经常使用深度克隆。在不影响先前状态的情况下创建新的状态对象对于维护应用程序的稳定性和正确实现撤销-重做功能至关重要。
如何使用 JSON.stringify() 和 JSON.parse() 进行深度复制
一种流行且无库的深度复制方式是使用内置的 JSON stringify()和parse()方法:
let obj = {a: [], b: 1, c: 'string'};
let newObj = JSON.parse(JSON.stringify(obj));
console.log(obj.a === newObj.a); // false
console.log(obj === newObj); // false
JSON.parse(JSON.stringify({}))方法并不完美,我们应该针对实际场景来考虑是否用它;该方法像特殊数据类型Date将被字符串化,NaN会转换为null, 而undefined值将被忽略:
console.log(JSON.parse(JSON.stringify({a: undefined, b: new Date(), c: NaN, d: null})));
// {b: '2023-05-17T06:22:20.511Z', c: null, d: null}
在 JavaScript 中, 我们通常通过递归遍历对象属性并创建具有相同属性和值的新对象。在下面的代码中,我们将创建一个 deepCopy 函数来深度克隆一个对象:
const deepCopy = (obj) => {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const newObj = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepCopy(obj[key]);
}
}
return newObj;
}
const deepCopiedObject = deepCopy(originalObject);
上述案例能满足大部分需求,但是一些特殊情况没有做处理,该方法还是有缺陷的:
-
无法处理循环引用:如果原始对象中存在循环引用,即某个属性的值引用了对象自身或对象中的其他属性,那么该代码将陷入无限循环,导致堆栈溢出或程序崩溃。 -
对于特殊对象类型的处理不完善:代码中只考虑了普通对象和数组,而对于其他特殊对象类型(例如Date、RegExp、Map、Set等)的处理不完善。对于这些特殊对象类型,深拷贝需要特殊的处理方式才能保留其原有的特性。 -
丢失对象的原型:在深拷贝过程中,新对象将失去原始对象的原型信息。新对象的原型将是一个空对象({})或空数组([]),而不是原始对象的原型。 -
性能问题:该代码使用递归方式进行深拷贝,对于深层嵌套或大型对象来说,可能导致性能下降。
针对这些缺陷,可以根据具体需求进行改进,例如使用循环和栈来处理循环引用,针对特殊对象类型实现特定的拷贝逻辑,保留原始对象的原型链等。同时,还可以考虑使用现有的深拷贝库或工具函数,这些库经过充分测试和优化,能够更好地处理不同情况下的深拷贝需求。
结论
感谢您花时间阅读本文。浅拷贝与深拷贝比任何新手想象的都要复杂得多。尽管每种方法都有很多缺陷,但花时间审查和考虑这些选项将确保您的应用程序和数据完全符合您的要求。