使用结构化克隆在 JavaScript 中进行深层复制

该平台现在附带 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} 使用 Spread 运算符myOriginal 的(可枚举)属性进行迭代。它使用属性名称和值,并将它们逐一分配给新创建的空对象。因此,生成的对象在形状上是相同的,但具有自己的属性和值列表副本。系统也会复制值,但 JavaScript 对所谓的原始值的处理方式与对非原始值的处理方式不同。如需引用 MDN,请执行以下操作:

在 JavaScript 中,基元(基元值、基元数据类型)是指非对象且没有方法的数据。有七种基元数据类型:字符串、数字、bigint、布尔值、未定义、符号和 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 等库仍可提供其他深度克隆算法的自定义实现,这些实现可能或可能不适合您的用例。

性能

虽然我没有进行新的微基准比较,但我在 2018 年初进行过比较,然后 structuredClone() 才会被揭示。当时,对于非常小的对象,JSON.parse() 是最快的选项。我希望这种情况不会改变。依赖结构化克隆的技术对于较大的对象要快得多。考虑到新 structuredClone() 无需滥用其他 API 且比 JSON.parse() 更稳健,因此建议您将其用作创建深层副本的默认方法。

总结

如果您需要在 JS 中创建值的深层复制(可能是因为您使用了不可变的数据结构,或者您想确保函数可以操控对象而不影响原始对象),则无需再寻求权宜解决方法或库。JS 生态系统现在有 structuredClone()。万岁。