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

该平台现在随附了 structuredClone(),这是一个用于深层复制的内置函数。

在很长一段时间里,您必须使用变通方案和库来创建 JavaScript 值的深层副本。该平台现在附带 structuredClone(),这是一个用于深度复制的内置函数。

浏览器支持

  • Chrome:98。 <ph type="x-smartling-placeholder">
  • Edge:98。 <ph type="x-smartling-placeholder">
  • Firefox:94。 <ph type="x-smartling-placeholder">
  • Safari:15.4. <ph type="x-smartling-placeholder">

来源

浅层副本

在 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 中,基元(基元值、基元数据类型)是指不属于对象且没有任何方法的数据。原始数据类型共有七种:string、number、bigint、布尔值、undefined、serse 和 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()。万岁。