structuredClone,更好的深拷贝方法

1,973 阅读6分钟

structuredClone

你知道吗, JavaScript 中添加了一个全局的方法,可以对 对象进行深拷贝了,没错那就是 structuredClone()。

该方法还支持把原始值中的可转移对象转移到新对象,而不是把属性引用拷贝过去。 可转移对象与原始对象分离并附加到新对象;它们不可以在原始对象中访问被访问到。

const calendarEvent = {
  title: "洞窝前端"date: new Date(),
  attendees: ["kenny"]
}
const copied = structuredClone(calendarEvent)

我们可以看到上面这个例子,不仅拷贝了对象,而且拷贝了数组,甚至是 Date 对象,下面我们看一下运行结果

copied.attendees // ['kenny']
copied.date //Fri Feb 10 2023 13:18:50 GMT+0800 (中国标准时间)
calendarEvent.attendees === copied.attendees // false
calendarEvent.date === copied.date // false

从上面可以看到所有的结果和我们预期的一样,该方法不仅可以拷贝上面列的那几种类型,还可以:

拷贝无限嵌套的对象和数组;

拷贝循环引用;

拷贝 JavaScript 的各种内置的类型,比如:Date,Set,Map,Error,RegExp,ArrayBuffer,Blob,File,ImageData等

还可以传递可转移对象,使其不被克隆.

像下面这行代码可以正常的运行:

const kitchenSink = {
  set: newSet([133]),
  map: newMap([[12]]),
  regex: /foo/,  
  deep: { array: [ new File(someBlobData, 'file.txt') ] },
  error: newError('Hello!')
}
kitchenSink.circular = kitchenSink
const clonedSink = structuredClone(kitchenSink)

为什么不直接用扩展运算符?

这个方法主要是用来做深拷贝的,如果你只需要浅拷贝,不需要复制嵌套对象或数组的引用,那么可以看下面:

可以使用ES6 的扩展运算符,或者是 Object 的 assign、create方法

const simpleEvent = {
  title: "洞窝前端"
}
const shallow1Copy = {...calendarEvent}
const shallow2Copy = Object.assign({}, simpleEvent)
const shallow3Copy = Object.create(simpleEvent)

但是,如果遇到嵌套的类型,就不行了

const calendarEvent = {
  title: "洞窝前端"date: new Date(),
  attendees: ["kenny"]
}
const shallowCopy = {...calendarEvent}
shallowCopy.attendees.push("居然之家")
// calendarEvent 的 attendees 也有了 新增加的参数
shallowCopy.date.setTime(123)
// calendarEvent 的 date 时间也被改变了

可以看到我们没有创建这个对象的完整副本.

嵌套的日期和数组仍然是两者之间的共享引用,如果我们想要编辑那些认为我们只是在更新复制的日历事件对象的内容,这可能会给我们带来重大问题.

为什么不用 JSON.parse(JSON.stringify(x))

这是一个比较常用方法,性能也不错,但是在处理一些类型时会有问题,举个例子:

const calendarEvent = {
  title: "洞窝前端"date: new Date(),
  attendees: ["kenny"]
}

//  JSON.stringify 会把 `date` 转换成 string 类型
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))

我们打印一下problematicCopy 我们可以看到

{
  title: "洞窝前端"date: "2023-02-10T07:21:49.355Z"
  attendees: ["kenny"]
}

这不是我们想要的,date 应该是Date()对象,而不是字符串.

这是因为 JSON.stringify 只能处理基本对象、数组和原始类型。任何其他类型都会以难以预测的方式处理。例如,Date 被转换为字符串。但是 Set 只是转换为{}。

JSON.stringify 甚至完全忽略某些东西,比如未定义的或函数。

例如,如果我们用这个方法复制 kitchenSink 的例子:

const kitchenSink = {
  set: new Set([133]),
  map: new Map([[12]]),
  regex: /foo/deep: { array: [ new File(someBlobData, 'file.txt') ] },
  error: new Error('Hello!')
}

const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))

我们会得到

{
  "set": {}"map": {}"regex": {}"deep": {
    "array": [
      {}
    ]
  }"error": {}}

OMG!我们必须删除我们最初为此使用的循环引用,因为JSON.stringify只要遇到一个就会抛出错误.所以,如果我们的数据符合要求,那么使用这个方法完全没有问题,但是我们用 structuredClone 这个方法可以做很多这个方法不能的深拷贝.

为什么不用 _.cloneDeep(x)

迄今为止,Lodash 的 cloneDeep 函数已经成为解决这个问题的一个非常常见的解决方案。

事实上,这个方法确实如预期的那样有效:

import cloneDeep from 'lodash/cloneDeep'

const calendarEvent = {
  title: "洞窝前端"date: new Date(),
  attendees: ["kenny"]
}

// ✅ 完美
const clonedEvent = cloneDeep(calendarEvent)

但是,值得提醒的是。在 VScode 中的 Import Cost 扩展,它可以打印我们所导入的任何东西的 kb 开销,这个函数会占用17.4 kb (5.3 kb gzip) :

这里假设你只导入那个函数。如果您以更常见的方式导入,而没有意识到 tree shaking 并不总是以您希望的方式工作,那么您可能会意外地仅为这一个函数导入高达25kb 的内存.

虽然这对任何人来说都不会是世界末日,但在我们的例子中,这是完全没有必要的,尤其是当浏览器已经内置了 structuredClone 的时候。

哪些场景不能用 structuredClone

不可以拷贝函数

会导致抛出 DATA_CLONE_ERR 的异常

不可以拷贝 DOM节点

同样会抛出 DATA_CLONE_ERR 异常

// 🚩 Error!
structuredClone({ fn: () => { } })

// 🚩 Error!
structuredClone({ el: document.body })

对象的某些特定参数也不会被保留

RegExp 对象的 lastIndex 字段不会被保留

属性描述符、设置器和 getter以及类似的元数据特性也不会被克隆。

例如,使用 getter 时,将克隆结果值,但不克隆 getter 函数本身(或任何其他属性元数据) :如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下

structuredClone({ get foo() { return 'bar' } })
// { foo: 'bar' }

原形链上的属性也不会被追踪以及复制。因此,如果您克隆 MyClass 的实例,被克隆的对象将不再被认为是这个类的实例(但是这个类的所有有效属性都将被克隆)

class MyClass { 
  foo = 'bar' 
  myMethod() { /* ... */ }
}
const myClass = new MyClass()

const cloned = structuredClone(myClass)
// { foo: 'bar' }

cloned instanceof myClass // false

支持类型的完整列表

更简单地说,不在下面列表中的任何东西都不能被克隆:

JS 的内置类型

Array、 ArrayBuffer、 Boolean、 DataView、 Date、 Error 类型(下面特别列出的类型)、 Map、 Object 但只是普通对象(例如对象文字)、 除了 symbol 之外的 基本类型(number、string、 null、undefined、 boolean、 BigInt))、 RegExp、 Set、 TypedArray

错误类型

Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError

Web/API类型

AudioData, Blob, CryptoKey, DOMException, DOMMatrix, DOMMatrixReadOnly, DOMPoint, DomQuad, DomRect, File, FileList, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle, ImageBitmap, ImageData, RTCCertificate, VideoFrame

浏览器兼容性

core-js 已经支持 structuredClone 的 polyfill.

需要注意的是:

 core-js 中的 structuredClone 的 polyfill ,还没有解决 ArrayBuffer 实例和许多平台类型无法在大多数引擎中传输的问题,所以 当需求兼容较低版本浏览器是因尽量避免使用 structuredClone(value, { transfer })的第二个参数。

在一些比较老的浏览器引擎中,有部分特定平台类型无法被克隆.比如 在 Safari 14.0 以前版本和Firefox 83 以前的版本 就没有实现同步克隆 ImageBitmap 。所以如果你想克隆特定的东西,建议查看 polyfill 源码。

参考文章:

developer.mozilla.org/zh-CN/docs/…

github.com/zloirock/co…

作者:洞窝-kenny