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

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

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

浏览器支持

  • 98
  • 98
  • 94
  • 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 中,基元(基元值,基元数据类型)是指既不是对象又没有方法的数据。有七种原始数据类型:字符串、数字、bigint、布尔值、未定义、符号和 null。

MDN - 原初

非基元值是作为“引用”处理的,也就是说,复制值的操作实际上只是复制对同一基础对象的引用,从而导致浅层复制行为。references

深层副本

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

过去,通过 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()。万岁。