最新HTML规范——structuredClone深拷贝函数,能取代JSON或者lodash吗?

2,910 阅读6分钟

长期以来,你不得不求助于变通方法和库来创建 JavaScript 值的深拷贝。但是在最新的 HTML5规范 中,javaScript附带了一个用于深拷贝的内置函数——structuredClone()

浏览器支持:

浏览器支持

在撰写本文时,所有浏览器都在它们的灰度版本中实现了这个 API,Firefox 已经在 Firefox 94 中将其稳定版发布。此外,Node 17 和 Deno 1.14 已经实现了这个 API。你可以立即尝试开始使用此功能。

浅拷贝

在 JavaScript 中复制一个对象几乎都是浅拷贝而不是深拷贝。这意味着对对象属性值的更改将在副本和原始值中可见。

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

const me = {
  name: '月夕',
  age: 21,
  girlFriend: {
      name: '某富婆',
      age: 81
  }
};

const meCopy = {...me};

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

meShallowCopy.age = 22;
console.log(me.age);// 21

然而,添加或更改嵌套很深的属性会影响副本和原始值:

meShallowCopy.girlFriend.age = 82;
console.log(me.girlFriend.agep) // 82

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

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

引用MDN

非原始值作为引用处理,这意味着拷贝值的行为实际上只是拷贝对象的引用,从而导致浅拷贝行为。

深拷贝

浅拷贝的反面是深拷贝。深拷贝算法也会一个一个地拷贝一个对象的属性,但是当它找到对另一个对象的引用时会递归调用它自己,同时创建该对象的一个副本。这对于确保两段代码不会意外共享一个对象并在不知不觉中操纵彼此的状态非常重要。

过去没有简单或好的方法可以在 JavaScript 中创建值的深层副本。很多人依赖第三方库,比如 Lodash 的cloneDeep() 函数。可以说,这个问题最常见的解决方案是基于 JSON 的 hack:

const meDeepCopy = JSON.parse(JSON.stringify(me));

事实上,这是一种非常流行的解决方法,V8 在积极的优化 JSON.parse(),特别是上面的情况下,以使 JSON 尽可能快。虽然速度很快,但它也有一些缺点和绊脚石:

  • 递归数据结构JSON.stringify()当你给它一个递归数据结构时会抛出错误。在使用链表或树时,这很容易发生。
  • 内置类型JSON.stringify() 如果该值包含其他 JS 内置类型,诸如 Maps、Sets、RegExps、Dates、ArrayBuffers 和其他内置类型之类的东西只会在序列化时丢失。
  • 函数JSON.stringify() 会悄悄地丢弃函数。

就目前V8对JSON.parse()的优化来看,JSON.parse()创建对象的性能已经超过了字面量创建对象的性能,但是这仅仅是数据量比较小的时候

也就是说 let obj = JSON.parse('{"name":"zs"}') 会比 let obj = {name:"zs"} 更快,详情可以看V8给出的报告 v8.dev/blog/cost-o…

结构化克隆

JavaScript已经提供并实现的几个深拷贝方案:

  • 在 IndexedDB 中存储 JS 值需要某种形式的序列化,以便它可以存储在磁盘上,然后反序列化以恢复 JS 值。
  • 类似地,通过向 WebWorker 发送消息postMessage()需要将 JS 值从一个 JS 领域传输到另一个领域。用于此的算法称为“结构化克隆”。

但是直到最近,开发人员都还无法方便的使用这些方案来实现深拷贝

但是现在不用担心了!最新的 HTML 规范公开一个称为structuredClone()运行结构化克隆算法的函数,开发人员可以通过它轻松的实现 JavaScript 深拷贝的一种方式。

structuredClone(value, { transfer })

参数

  • value

    要克隆的对象:这可以是任何 结构化可克隆类型。

  • transfer 可选

    可转移的对象: 为一个数组,其中的值将被移动到新的对象,而不是克隆至新的对象。

返回值

返回的值是原始 的深层副本value

例外

  • DataCloneError

    如果输入值的任何部分不可序列化,则抛出该错误。

const meDeepCopy = structuredClone(me);

特点和限制

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

但是,它仍然有一些限制:

  • 原型:如果你使用structuredClone()类实例,你将获得一个普通对象作为返回值,因为结构化克隆会丢弃对象的原型链。
  • 函数:如果你的对象包含函数,它们将被悄悄丢弃。
  • 不可克隆:有些值不是结构化可克隆的,尤其是ErrorDOM 节点Function。尝试这样做将引发 DataCloneError 异常。
  • 属性描述符:setter和getter(以及类似元数据的功能)不会被复制。例如,如果使用属性描述符将对象标记为只读,则复制后的对象中是可读写(默认配置)。
  • RegExp:RegExp对象的lastIndex字段不会保留。

如果你的任何一个用例中会因为上面的限制对你期待的结果产生影响,像 Lodash 这样的库仍然提供其他深度克隆算法的自定义实现,这些算法可能适合也可能不适合你的用例。

支持的类型

  • 除了symbol以外的基本类型
  • Boolean object
  • String object
  • Date
  • RegExp
  • Blob
  • File
  • FileList
  • ArrayBuffer
  • ArrayBufferView 所有的arrayBuffer视图,例如Int32Array.
  • ImageBitmap
  • ImageData
  • Array
  • Object 普通js Object
  • Map
  • Set

性能

structuredClone()出现之前。JSON.parse() 对于非常小的数据来说,这是最快的选择。对于更大的对象,依赖结构化克隆的技术会更快更快。考虑到新的structuredClone()没有滥用其他 API 的开销并且比 更健壮JSON.parse(),我建议您可以将其设为创建深度副本的默认方法。

结论

如果你需要在 JS 中使用深拷贝——可能是因为你使用了不可变的数据结构,或者你想确保一个函数可以在不影响原始对象的情况下操作一个对象——你不再需要寻求解决方法或百度。JS 生态系统现在有structuredClone()就能满足你。虽然我相信不久的将来他就会被各大浏览器实现,但是现在你必须考虑它的兼容性,毕竟它现在只是个孩子啊😎