您知道吗?JavaScript 现在有一种原生方式可以对象深拷贝。
没错,就是 structuredClone 函数,它是内置在 JavaScript 运行时中的:
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
// 😍
const copied = structuredClone(calendarEvent)
是否注意到,在上面的示例中我们不仅复制了对象,还复制了嵌套数组,甚至 Date 对象!
一切都按预期工作:
copied.attendees // ["Steve"]
copied.date // Date: Wed Dec 31 1969 16:00:00
cocalendarEvent.attendees === copied.attendees // false
没错, structuredClone 不仅可以做到以上几点,还可以:
- 可复制无限嵌套的对象和数组
- 可复制循环引用自身的
- 可复制各种 JavaScript 类型,例如
Date、Set、Map、Error、RegExp、ArrayBuffer、Blob、File、ImageData等等,更多 - 转移任何 可转移的对象
例如,这种疯狂甚至会像预期的那样奏效:
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
// ✅ All good, fully and deeply copied!
const clonedSink = structuredClone(kitchenSink)
为什么不是对象扩展运算 { ...obj } 呢?
重要的是要注意我们正在谈论深拷贝。如果你只需要做一个浅拷贝,也就是不复制嵌套对象或数组的拷贝,那么我们可以只做一个对象扩展运算:
const simpleEvent = {
title: "Builder.io Conf",
}
// ✅ no problem, there are no nested objects or arrays
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}
// 🚩 oops - we just added "Bob" to both the copy *and* the original event
shallowCopy.attendees.push("Bob")
// 🚩 oops - we just updated the date for the copy *and* original event
shallowCopy.date.setTime(456)
如上所见,我们没有得到该对象的完整副本。
嵌套的日期和数组仍然是两者之间的共享引用,如果我们想编辑那些认为我们只是更新复制的日历事件对象,这可能会给我们带来重大问题。
为什么不用 JSON.parse(JSON.stringify(x)) ?
是啊,这也一种方式。它实际上是一个很棒的,并且性能出奇地好,但也有一些缺点,这正是 structuredClone 解决的问题。如下例:
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
// 🚩 JSON.stringify converted the `date` to a string
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))
如果我们打印 problematicCopy,会得到:
{
title: "Builder.io Conf",
date: "1970-01-01T00:00:00.123Z"
attendees: ["Steve"]
}
这不是我们想要的! date 应该是 Date 对象,而不是字符串。发生这种情况是因为 JSON.stringify 只能处理基本对象、数组和基础数据类型。任何其他类型都可以以难以预测的方式处理。例如,日期被转换为字符串。但是 Set 只是简单地转换为 {} 。
JSON.stringify 甚至完全忽略了某些东西,比如 undefined 或函数。比如,如果我们使用此方法复制我们的 kitchenSink 例子:
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 veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))
我们会得到:
{
"set": {},
"map": {},
"regex": {},
"deep": {
"array": [
{}
]
},
"error": {},
}
是的,我们必须删除我们最初为它设置的循环引用,因为如果 JSON.stringify 遇到其中之一,它只会抛出错误。
因此,虽然如果我们的需求符合它所能做的,那么这个方法可能很好,但我们可以使用structuredClone(也就是我们在这里未能做到的所有事情)做很多事情,而这个方法无法做到。
为什么不用 _.cloneDeep?
目前为止,Lodash 的 cloneDeep 函数是解决此问题的一种非常普遍的方法。事实上,这确实如预期的那样:
import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
// ✅ All good!
const clonedEvent = structuredClone(calendarEvent)
但是,这里有个警告。根据 VSCode 的 Import Cost 扩展,它可以显示导入包 kb 大小,这个函数压缩后总共有 17.4kb(gzip 后为 5.3kb):
并且如果只导入该功能。如果改用更常见的方式导入,没有意识到 tree shaking 并不总是按照你所想的方式工作,你可能会不小心导入高达 25kb 的数据,只是为了这个功能 😱
虽然这对任何人来说都不是问题,但在我们的情况下,这根本不是必要的,因为浏览器已经内置了structuredClone。
structureClone 不能 拷贝什么?
无法拷贝 Functions
拷贝将抛出一个 DataCloneError 异常:
// 🚩 Error!
structuredClone({ fn: () => { } })
无法拷贝 DOM 节点
会抛出 DataCloneError 异常:
// 🚩 Error!
structuredClone({ el: document.body })
无法拷贝对象属性的 descriptors、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
structureClone 支持类型的完整列表
更简单地说,不在以下列表中的任何内容都不能被克隆:
JS Built-ins
Array 、 ArrayBuffer 、 Boolean 、 DataView 、 Date 、 Error 类型(下面具体列出的那些)、 Map 、 Object 但仅限普通对象(例如来自对象字面量)、原始类型 除了 symbol (如 number 、 string 、 null 、 undefined 、 boolean 、 BigInt )、 RegExp 、 Set 、 TypedArray
Error types
Error, EvalError, RangeError, ReferenceError , SyntaxError, TypeError, URIError
Web/API types
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中进行深度克隆对象。
参考资源: