当下有个原生方法能够对对象进行深拷贝,没错,就是内置函数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类型,像
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
// ✅ 点赞,完全符合预期结果
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甚至会忽略像undefined或functions之类的。
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):
假设您只导入该功能。如果使用更常规的方式导入, tree shaking 不一定会起作用,你可能会不小心导入高达 25kb 的数据 😱
这很明显是非常大的开销,任何开发人员都不希望引入这么大的包,特别是当我们的浏览器内置了
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
所有支持的类型
任何不在下面列表中的东西都不能被克隆:
内置
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。