深拷贝
对象的深拷贝是指一个副本,其属性不与源对象(创建该副本的原始对象)共享相同的引用(指向相同的底层值)。因此,当你改变源对象或副本时,你可以确信不会导致另一个对象也发生改变。这种行为与浅拷贝的行为形成对比,在浅拷贝中,对源对象或副本中嵌套属性的更改可能会导致另一个对象也发生改变。
如果两个对象o1和o2的可观察行为相同,则它们是“结构等价”的。这些行为包括:
o1和o2的属性具有相同的名称和顺序。- 它们属性的值是结构等价的。
- 它们的原型链是结构等价的(尽管当我们处理结构等价时,这些对象通常是普通对象,这意味着它们都继承自
Object.prototype)。
结构等价的对象可以是同一个对象(o1 === o2),也可以是“副本”(o1 !== o2)。由于等价的原始值总是比较相等,因此你无法创建它们的副本。
我们现在可以更正式地定义深拷贝为:
- 它们不是同一个对象(
o1 !== o2)。 o1和o2的属性具有相同的名称和顺序。- 它们属性的值是彼此的深拷贝。
- 它们的原型链是结构等价的。
深拷贝的原始链可能被复制,也可能不被复制(通常不复制)。但是,具有结构不等价原型链的两个对象(例如,一个是数组而另一个是普通对象)永远不是彼此的副本。
一个对象(其所有属性都具有原始值)的副本符合深拷贝和浅拷贝的定义。然而,谈论这种拷贝的“深度”有些无用,因为它没有嵌套属性,而我们通常在修改嵌套属性的上下文中使用深拷贝。
在 JavaScript 中,标准内置的对象拷贝操作(展开语法、Array.prototype.concat()、Array.prototype.slice()、Array.from()和Object.assign())不创建深拷贝(相反,它们创建浅拷贝)。
如果一个 JavaScript 对象可以序列化,那么一种创建深拷贝的方法是使用JSON.stringify()将对象转换为 JSON 字符串,然后使用JSON.parse()将字符串转换回一个(全新的)JavaScript 对象。
const ingredientsList = ["noodles", { list: ["eggs", "flour", "water"] }];
const ingredientsListDeepCopy = JSON.parse(JSON.stringify(ingredientsList));
因为深拷贝不与其源对象共享任何引用,所以对深拷贝进行的任何更改都不会影响源对象。
// Change the value of the 'list' property in ingredientsListDeepCopy.
ingredientsListDeepCopy[1].list = ["rice flour", "water"];
// The 'list' property does not change in ingredients_list.
console.log(ingredientsList[1].list);
// Array(3) [ "eggs", "flour", "water" ]
然而,尽管上面代码中的对象足够简单,可以序列化,但许多 JavaScript 对象根本无法序列化——例如,函数(带闭包)、Symbol、代表 HTML DOM API 中 HTML 元素的对象、递归数据以及许多其他情况。在这些情况下调用JSON.stringify()来序列化对象将失败。因此,无法创建此类对象的深拷贝。
Web API structuredClone()也创建深拷贝,其优点是允许源中的可转移对象被“转移”到新副本中,而不仅仅是克隆。它还处理更多数据类型,例如Error。但请注意,structuredClone()本身不是 JavaScript 语言的特性——它是实现 Web API 的浏览器和其他 JavaScript 主机的特性。并且调用structuredClone()来克隆不可序列化对象将失败,就像调用JSON.stringify()来序列化它会失败一样。