使用更现代化的方式进行深拷贝

99 阅读4分钟

assets_YJIGb4i01jvw0SRdL5Bt_03f2036674724006ae64d9bc4d07ab6d.webp

当下有个原生方法能够对对象进行深拷贝,没错,就是内置函数structuredClone

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

// 😍
const copied = structuredClone(calendarEvent)

注意上面的例子,我们不仅拷贝了对象本身,包括嵌套的数组以及日期对象也一起拷贝了。并且结果也符合预期:

copied.attendees // ["Steve"]
copied.date // Date: Wed Dec 31 1969 16:00:00
cocalendarEvent.attendees === copied.attendees // false

没错,structuredClone不仅能做到上面这些,还包括如下:

  • 拷贝无限嵌套的对象和数组
  • 拷贝循环引用
  • 拷贝各种JS类型,像DateSetMapErrorRegExpArrayBufferBlobFileImageData等等
  • 转移任何可以转移的对象

让我们看个例子,同样能够按照预期执行:

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

// ✅ 点赞,完全符合预期结果
const clonedSink = structuredClone(kitchenSink)

为什么我们不使用对象展开方式呢

首先,我们在讨论的是深拷贝。如果你只需要浅拷贝,不需要考虑嵌套对象或者数组之类的,你完全可以使用对象展开的方式:

const simpleEvent = {
  title: "Builder.io Conf",
}
// ✅ 没有嵌套对象或数组,没问题
const shallowCopy = {...calendarEvent}

或者是:

const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)

但是,一旦我们有了嵌套项,就会出问题:

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

const shallowCopy = {...calendarEvent}

// 🚩 啊哦 - 下面的操作会对原始对象和新的对象都产生影响
shallowCopy.attendees.push("Bob")

shallowCopy.date.setTime(456)

如你看到的一样,此方法并没有进行深拷贝。 嵌套的日期和数组仍然是两者之间的共享引用,如果我们想编辑那些复制的日历事件对象,这可能会给我们带来重大问题

为什么不使用JSON.parse(JSON.stringify(X))

这是一个很好的方式,性能也很好,但是仍然有些问题,structuredClone解决了,看个例子:

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

// 🚩 JSON.stringify 把`date` 转成了字符串
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))

输出一下problematicCopy

{
  title: "Builder.io Conf",
  date: "1970-01-01T00:00:00.123Z"
  attendees: ["Steve"]
}

这不是我们想要的。我们希望date是一个对象,而不是字符串。因为JSON.stringify只能处理基础对象,数组。任何其他的类型都很难处理。比如,Dates会被转为字符串,但是Set会被转为{}JSON.stringify甚至会忽略像undefinedfunctions之类的。

const kitchenSink = {
  set: new Set([1, 3, 3]),
  map: new Map([[1, 2]]),
  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": {},
}

这下浏览器直接崩了。 因此,虽然他的功能符合我们的一些需求,但我们可以使用structuredClone做很多事情(也就是我们在这里未能完成的所有事情),而这种方法不能。

为什么不使用_.cloneDeep

目前为止,lodash的cloneDeep方法非常的流行。事实上,也的确是符合预期:

import cloneDeep from 'lodash/cloneDeep'

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

// ✅ 
const clonedEvent = structuredClone(calendarEvent)

但是,这里只有一个问题。根据我的 IDE 中的 Import Cost 扩展,展示我导入的任何东西的 kb 成本,这个函数压缩后总共有 17.4kb(gzip 后为 5.3kb):

assets_YJIGb4i01jvw0SRdL5Bt_aa5ab14fd21741bf8e327dd6e6fb68b1.webp 假设您只导入该功能。如果使用更常规的方式导入, tree shaking 不一定会起作用,你可能会不小心导入高达 25kb 的数据 😱

assets_YJIGb4i01jvw0SRdL5Bt_5dbbee190753414bb31b720059a501db.jpeg 这很明显是非常大的开销,任何开发人员都不希望引入这么大的包,特别是当我们的浏览器内置了structuredClone时,就更加不需要了。

structuredClone缺点

函数不能被拷贝,会抛出DataCloneError的错误:

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

DOM 节点

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

属性描述符、setter 和 getter 以及类似的类似元数据的功能不会被克隆。例如,对于 getter,结果会被克隆,但不是 getter 函数本身(或任何其他属性元数据):

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

对象原型,原型链无法被遍历或复制。所以如果你克隆一个 MyClass 的实例,克隆的对象将不再是这个类的一个实例(但是这个类的所有有效属性都将被克隆)

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

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

cloned instanceof myClass // false

所有支持的类型

任何不在下面列表中的东西都不能被克隆:

内置

ArrayArrayBufferBooleanDataViewDateErrorMap,字面量Object除了symbolnumberstringnullundefinedbooleanBigInt), RegExpSetTypedArray

错误类型

ErrorEvalErrorRangeErrorReferenceError , SyntaxErrorTypeErrorURIError

web api类型

AudioDataBlobCryptoKeyDOMExceptionDOMMatrixDOMMatrixReadOnlyDOMPointDomQuadDomRectFileFileListFileSystemDirectoryHandleFileSystemFileHandleFileSystemHandleImageBitmapImageDataRTCCertificateVideoFrame

兼容性

所有主流浏览器都支持 structuredClone,甚至 Node.js 和 Deno。

assets_YJIGb4i01jvw0SRdL5Bt_1fdbc5b0826240e487a4980dfee69661.webp

【参考】

原文