深拷贝
对象的**深拷贝**是指其属性不与从中创建该拷贝的源对象的属性共享相同引用(指向相同的底层值)的拷贝。因此,当您更改源或拷贝时,您可以确信您不会导致另一个对象也发生更改。此行为与浅拷贝的行为形成对比,在浅拷贝中,对源或拷贝中嵌套属性的更改可能会导致另一个对象也发生更改。
如果两个对象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 对象根本无法序列化——例如,函数(带闭包)、符号、表示HTML DOM API中 HTML 元素的对象、递归数据以及许多其他情况。调用JSON.stringify()
来序列化这些情况下的对象将失败。因此,无法创建此类对象的深拷贝。
Web API structuredClone()
也会创建深拷贝,并且具有允许将源中的可传输对象传输到新拷贝,而不仅仅是克隆的优点。它还处理更多数据类型,例如Error
。但请注意,structuredClone()
本身并不是 JavaScript 语言的功能——而是实现 Web API 的浏览器和其他 JavaScript 主机的一项功能。并且,调用structuredClone()
来克隆不可序列化的对象将与调用JSON.stringify()
来序列化它一样失败。