深度解析 JavaScript 的 structuredClone() 方法
您是否知道,现在 JavaScript 中有一种原生的方式可以深拷贝对象?
没错,这个内置于 JavaScript 运行时的 structuredClone
函数就是这样:
1const calendarEvent = {
2 title: "Builder.io大会",
3 date: new Date(123),
4 attendees: ["Steve"]
5}
6
7//
8const copied = structuredClone(calendarEvent)
9
您是否注意到在上面的例子中,我们不仅复制了对象,还复制了嵌套数组,甚至还包括 Date 对象?
一切正如预期工作:
1copied.attendees // ["Steve"]
2copied.date // Date: 1969年12月31日16:00:00
3cocalendarEvent.attendees === copied.attendees // false
4
没错, structuredClone
不仅能做到上述功能,还能够:
- 克隆无限嵌套的对象和数组
- 克隆循环引用
- 克隆广泛的 JavaScript 类型,例如
Date
、Set
、Map
、Error
、RegExp
、ArrayBuffer
、Blob
、File
、ImageData
,以及许多其他类型 - 转移任何可转移对象
例如,甚至以下这种疯狂的情况也能如期工作:
1const kitchenSink = {
2 set: new Set([1, 3, 3]),
3 map: new Map([[1, 2]]),
4 regex: /foo/,
5 deep: { array: [ new File(someBlobData, 'file.txt') ] },
6 error: new Error('Hello!')
7}
8kitchenSink.circular = kitchenSink
9
10// 全部良好,完全而深入地复制!
11const clonedSink = structuredClone(kitchenSink)
12
为什么不仅使用对象扩展?
重要的是要注意我们在谈论深度拷贝。如果您只需要进行浅拷贝,也就是不复制嵌套对象或数组的拷贝,那么我们可以简单地使用对象扩展:
1const simpleEvent = {
2 title: "Builder.io大会",
3}
4// 没问题,没有嵌套对象或数组
5const shallowCopy = {...calendarEvent}
6
甚至如果您更喜欢,可以使用以下方式之一:
1const shallowCopy = Object.assign({}, simpleEvent)
2const shallowCopy = Object.create(simpleEvent)
3
但是一旦我们有了嵌套项,就会遇到麻烦:
1const calendarEvent = {
2 title: "Builder.io大会",
3 date: new Date(123),
4 attendees: ["Steve"]
5}
6
7const shallowCopy = {...calendarEvent}
8
9// 哎呀 - 我们刚刚将“Bob”添加到了复制品*和*原始事件中
10shallowCopy.attendees.push("Bob")
11
12// 哎呀 - 我们刚刚更新了复制品*和*原始事件的日期
13shallowCopy.date.setTime(456)
14
如您所见,我们并没有完全复制这个对象。
嵌套的日期和数组仍然是两者之间的共享引用,如果我们想要编辑这些,认为我们只是在更新复制的日历事件对象,这可能会导致我们遇到重大问题。
为什么不用 JSON.parse(JSON.stringify(x))
?
啊,是的,这个技巧。实际上这是一个很好的方法,而且出人意料地高效,但 structuredClone
解决了它的一些缺点。
以此为例:
1const calendarEvent = {
2 title: "Builder.io大会",
3 date: new Date(123),
4 attendees: ["Steve"]
5}
6
7// JSON.stringify 将`date`转换为了字符串
8const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))
9
如果我们记录 problematicCopy
,我们会得到:
1{
2 title: "Builder.io大会",
3 date: "1970-01-01T00:00:00.123Z"
4 attendees: ["Steve"]
5}
6
这不是我们想要的! date
应该是一个 Date
对象,而不是一个字符串。
这是因为 JSON.stringify
只能处理基本的对象、数组和原始类型。任何其他类型都以难以预测的方式处理。例如,日期被转换为字符串。但是 Set
简单地转换为 {}
。
JSON.stringify
甚至完全忽略某些东西,如 undefined
或 函数。
例如,如果我们用这种方法复制我们的 kitchenSink
示例:
1const kitchenSink = {
2 set: new Set([1, 3, 3]),
3 map: new Map([[1, 2]]),
4 regex: /foo/,
5 deep: { array: [ new File(someBlobData, 'file.txt') ] },
6 error: new Error('Hello!')
7}
8
9const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))
10
我们会得到:
1{
2 "set": {},
3 "map": {},
4 "regex": {},
5 "deep": {
6 "array": [
7 {}
8 ]
9 },
10 "error": {},
11}
12
呃!
哦,是的,我们不得不移除我们最初拥有的循环引用,因为 JSON.stringify
如果遇到其中的一个,就会简单地抛出错误。
因此,虽然如果我们的需求适合它能做的事情,这种方法可能很好,但 structuredClone
(也就是上面我们未能做到的所有事情)能做许多这种方法不能做的事情。
为什么不用 _.cloneDeep
?
到目前为止,Lodash 的 cloneDeep
函数一直是这个问题的一个非常常见的解决方案。
实际上,这确实如预期工作:
1import cloneDeep from 'lodash/cloneDeep'
2
3const calendarEvent = {
4 title: "Builder.io大会",
5 date: new Date(123),
6 attendees: ["Steve"]
7}
8
9const clonedEvent = cloneDeep(calendarEvent)
10
但是,这里只有一个警告。根据我的 IDE 中的 Import Cost 扩展,它打印了我导入的任何东西的 kb 成本,这一个功能的成本为整整 17.4kb压缩后的大小(5.3kb gzip 压缩后):
而这是假设您仅导入了那个功能。如果您以更常见的方式导入,没有意识到 tree shaking 并不总是按照您希望的方式工作,您可能意外地仅为这一个功能导入多达 25kb
虽然这对任何人来说都不会是世界末日,但在我们的案例中,这根本不是必要的,不是在浏览器中已经内置了 structuredClone
的情况下。
什么是 structuredClone
不能 克隆
函数不能被克隆
它们会抛出一个 DataCloneError
异常:
1// 错误!
2structuredClone({ fn: () => { } })
3
DOM 节点
也会抛出一个 DataCloneError
异常:
1// 错误!
2structuredClone({ el: document.body })
3
属性描述符、设置器和获取器
以及类似的元数据特性都不会被克隆。
例如,对于一个获取器,克隆的是结果值,而不是获取器函数本身(或任何其他属性元数据):
1structuredClone({ get foo() { return 'bar' } })
2// 变成了:{ foo: 'bar' }
3
对象原型
不会遍历或复制原型链。因此,如果您克隆了 MyClass
的一个实例,克隆的对象将不再被识别为这个类的实例(但这个类的所有有效属性将被克隆):
1class MyClass {
2 foo = 'bar'
3 myMethod() { /* ... */ }
4}
5const myClass = new MyClass()
6
7const cloned = structuredClone(myClass)
8// 变成了:{ foo: 'bar' }
9
10cloned instanceof myClass // false
11
支持的类型全列表
更简单地说,下面列表中未提到的任何东西都不能被克隆:
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
浏览器和运行时支持
这里是最好的部分 - structuredClone
在所有主要浏览器中都得到支持,甚至包括 Node.js 和 Deno。
只需注意 Web Workers 的支持较为有限的警告:
来源:MDN
结论
经过漫长的等待,我们终于现在有了 structuredClone
,使得在 JavaScript 中深度克隆对象变得轻而易举。