你知道吗,在JavaScript
中现在有一种原生方法可以深度复制对象吗?
没错,这个structuredClone
函数已经内置到JavaScript运行时中了:
1 | const calendarEvent = { |
上面的示例中,我们不仅复制了对象,还复制了嵌套数组,甚至是Date
对象吗?
而且一切都正如预期的那样运作:
1 | copied.attendees // ["Steve"] |
structuredClone
不仅可以做到上述,而且还能:
- 无限深度地克隆对象和数组
- 克隆循环引用
- 克隆各种
JavaScript
类型,如Date
、Set
、Map
、Error
、RegExp
、ArrayBuffer
、Blob
、File
、ImageData
等等 - 传输任何可传输的对象
这个例子也会按预期运行:
1 | const kitchenSink = { |
为什么不使用对象扩展?
重要的是要注意我们谈论的是深度复制。如果你只需要进行浅复制,即不复制嵌套对象或数组,那么我们可以简单地使用对象扩展:
1 | const simpleEvent = { |
或者是这些,如果你更喜欢的话:
1 | const shallowCopy = Object.assign({}, simpleEvent) |
但是一旦我们有了嵌套项目,我们就会遇到麻烦:
1 | const calendarEvent = { |
我们并没有完全复制这个对象。
嵌套的日期和数组仍然是两者之间共享的引用,这可能会在我们想要编辑它们时造成重大问题,我们可能会认为只更新了复制的日历事件对象。
为什么不用 JSON.parse(JSON.stringify(x))?
啊,是的,这个技巧。实际上是一个很好的方法,而且出乎意料地高效,但它有一些缺点。
举个例子:
1 | const calendarEvent = { |
如果我们记录 problematicCopy,我们会得到:
1 | { |
这不是我们想要的!date
应该是一个Date
对象,而不是一个字符串。
这是因为JSON.stringify
只能处理基本对象、数组和原始值。任何其他类型可能以难以预测的方式处理。例如,日期被转换为字符串。但是集合只是转换为 {}
。
JSON.stringify
甚至会完全忽略某些东西,比如undefined
或函数。
例如,如果我们使用这种方法复制我们的kitchenSink
示例:
1 | const kitchenSink = { |
我们会得到:
1 | { |
噫!
哦,是的,我们还必须移除我们最初为此设置的循环引用,因为JSON.stringify
如果遇到其中一个会直接抛出错误。
所以,虽然如果我们的要求符合它所能做的事情,这种方法可能非常棒,但是我们可以用structuredClone做的事情(即我们在这里失败的所有内容)这种方法无法做到。
为什么不使用 _.cloneDeep?
迄今为止,Lodash
的cloneDeep
函数一直是解决这个问题的一个非常常见的解决方案。
实际上,这确实像预期的那样工作:
1 | import cloneDeep from 'lodash/cloneDeep' |
但是,这里只有一个注意事项。根据我的IDE
中的Import Cos
t扩展,,这个函数最小化后的大小为17.4kb
(gzip后为5.3kb)
虽然这假设您只导入了该函数。您可能会为这个函数增加多达25kb空间
虽然这对任何人来说都不是世界末日,但在我们的情况下,这并不是必要的,因为浏览器已经内置了structuredClone
。
structuredClone 无法克隆以下内容:
- 函数不能被克隆,它们会抛出一个
DataCloneError
异常:1
2// 🚩 错误!
structuredClone({ fn: () => { } }) DOM
节点也会抛出DataCloneError
异常:1
2// 🚩 错误!
structuredClone({ el: document.body })- 属性描述符、设置器和获取器以及类似的元数据特性也不会被克隆。例如,对于一个获取器,结果值会被克隆,但是获取器函数本身(或任何其他属性元数据)不会被克隆:
1
2structuredClone({ get foo() { return 'bar' } })
// 结果:{ foo: 'bar' } - 对象原型链不会被遍历或复制。因此,如果你克隆了
MyClass
的一个实例,克隆的对象将不再被认为是这个类的一个实例(但这个类的所有有效属性都会被克隆):1
2
3
4
5
6
7
8
9
10class MyClass {
foo = 'bar'
myMethod() { /* ... */ }
}
const myClass = new MyClass()
const cloned = structuredClone(myClass)
// 结果:{ foo: 'bar' }
cloned instanceof myClass // falsestructuredClone支持的类型列表:
简单地说,任何不在以下列表中的内容都无法被克隆:JS 内置类型
Array
、ArrayBuffer
、Boolean
、DataView
、Date
、Error
(特别列出的那些)、Map
、Object
(但只限于普通对象,例如对象字面量)、原始类型(除了 symbol,即数字、字符串、null
、undefined
、布尔值、BigInt)、RegExp
、Set
、TypedArray
Error 类型
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
浏览器支持:
结构化克隆在所有主流浏览器中都得到支持,甚至在 Node.js
和 Deno
中也是如此。只需注意 Web Workers
的支持有一些限制。
我们现在有了 structuredClone
,让在JavaScript
中深度克隆对象变得轻而易举