在 JavaScript 中使用 structuredClone 进行深拷贝

该平台现在附带 structuredClone(),这是一个用于深拷贝的内置函数。

在很长一段时间里,您不得不求助于变通方法和库来创建 JavaScript 值的深拷贝。该平台现在附带 structuredClone(),这是一个用于深拷贝的内置函数。

浏览器支持

  • Chrome: 98.
  • Edge: 98.
  • Firefox: 94.
  • Safari: 15.4.

来源

浅拷贝

在 JavaScript 中复制值几乎总是拷贝,而不是拷贝。这意味着对深层嵌套值的更改在副本和原始值中都可见。

在 JavaScript 中,使用对象展开运算符 ... 创建浅拷贝的一种方法

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

直接在浅拷贝上添加或更改属性只会影响副本,而不会影响原始对象

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

但是,添加或更改深层嵌套的属性会影响副本和原始对象

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

表达式 {...myOriginal} 使用展开运算符迭代 myOriginal 的(可枚举)属性。它使用属性名称和值,并将它们逐个分配给新创建的空对象。因此,结果对象的形状相同,但具有其自己的属性和值列表的副本。值也被复制,但 JavaScript 值处理所谓的原始值的方式与非原始值不同。引用 MDN 的话

在 JavaScript 中,原始类型(原始值、原始数据类型)是不属于对象且没有方法的数据。有七种原始数据类型:string、number、bigint、boolean、undefined、symbol 和 null。

MDN — 原始类型

非原始值被视为引用处理,这意味着复制值的行为实际上只是复制对同一底层对象的引用,从而导致浅拷贝行为。

深拷贝

与浅拷贝相反的是深拷贝。深拷贝算法也逐个复制对象的属性,但在找到对另一个对象的引用时,会递归地调用自身,同时创建该对象的副本。这对于确保两段代码不会意外地共享一个对象并在不知情的情况下相互操纵状态非常重要。

过去,在 JavaScript 中创建值的深拷贝并没有简单或好的方法。许多人依赖第三方库,例如 Lodash 的 cloneDeep() 函数。可以说,解决此问题最常见的方法是基于 JSON 的技巧

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

事实上,这是一种非常流行的变通方法,以至于 V8 积极优化了 JSON.parse(),特别是上面的模式,使其尽可能快。虽然它速度很快,但它也有一些缺点和陷阱

  • 递归数据结构:当您给 JSON.stringify() 一个递归数据结构时,它会抛出错误。当使用链表或树时,这种情况很容易发生。
  • 内置类型:如果值包含其他 JS 内置类型(如 MapSetDateRegExpArrayBuffer),JSON.stringify() 将抛出错误。
  • 函数JSON.stringify() 会静默丢弃函数。

结构化克隆

该平台已经需要在几个地方创建 JavaScript 值的深拷贝:在 IndexedDB 中存储 JS 值需要某种形式的序列化,以便可以将其存储在磁盘上,并在以后反序列化以恢复 JS 值。同样,通过 postMessage() 向 WebWorker 发送消息需要将 JS 值从一个 JS 领域传输到另一个 JS 领域。用于此目的的算法称为“结构化克隆”,直到最近,开发人员还无法轻松访问它。

现在情况已经改变!HTML 规范被修订以公开一个名为 structuredClone() 的函数,该函数完全运行该算法,作为开发人员轻松创建 JavaScript 值深拷贝的一种手段。

const myDeepCopy = structuredClone(myOriginal);

就是这样!这就是整个 API。如果您想深入了解详细信息,请查看 MDN 文章

特性和限制

结构化克隆解决了 JSON.stringify() 技术的许多(尽管不是全部)缺点。结构化克隆可以处理循环数据结构,支持许多内置数据类型,并且通常更健壮且通常更快。

但是,它仍然有一些局限性可能会让您措手不及

  • 原型:如果您将 structuredClone() 与类实例一起使用,您将获得一个普通对象作为返回值,因为结构化克隆会丢弃对象的原型链。
  • 函数:如果您的对象包含函数,structuredClone() 将抛出 DataCloneError 异常。
  • 不可克隆对象:某些值不可进行结构化克隆,最值得注意的是 Error 和 DOM 节点。这将导致 structuredClone() 抛出错误。

如果这些限制中的任何一个对您的用例来说是决定性因素,那么像 Lodash 这样的库仍然提供其他深克隆算法的自定义实现,这些算法可能适合也可能不适合您的用例。

性能

虽然我没有进行新的微基准测试比较,但在 structuredClone() 公开之前,我在 2018 年初做过一次比较。那时,JSON.parse() 是非常小的对象的最快选择。我预计这种情况会保持不变。依赖于结构化克隆的技术对于较大的对象来说(明显)更快。考虑到新的 structuredClone() 没有滥用其他 API 的开销,并且比 JSON.parse() 更健壮,我建议您将其作为创建深拷贝的默认方法。

结论

如果您需要在 JS 中创建值的深拷贝——可能是因为您使用不可变数据结构,或者您想确保函数可以操作对象而不影响原始对象——您不再需要求助于变通方法或库。JS 生态系统现在有了 structuredClone()。万岁。