- 原文地址:www.builder.io/blog/struct...
- 原文作者:STEVE SEWELL
你知道现在 JavaScript 有一种原生的深拷贝方法吗?
没错,就是 structuredClone
方法。该方法已内置于 JavaScript 运行时中。(译者注:Nodejs > 17)
javascript
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
// 😍
const copied = structuredClone(calendarEvent)
你是否注意到在上面的例子中,我们拷贝整个对象同时也拷贝了内嵌的数组甚至是 Date
对象。
并且所有的一切都符合我们的预期。
javascript
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
等。 - 转移任何可转移对象(译者注:有点像 Rust 中所有权的转移。MDN 中的例子很不错。)
比如,像下面这种抽风的示例也会符合预期:(译者注:可以去控制台试试,真的无限嵌套了)
javascript
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)
为什么不展开对象呢?
需要注意的是我们正在讨论深拷贝。如果你仅仅只需要做浅拷贝,即不用拷贝那些嵌套的对象或者数组,那自然是可以使用对象展开的。
javascript
const simpleEvent = {
title: "Builder.io Conf",
}
// ✅ no problem, there are no nested objects or arrays
const shallowCopy = {...calendarEvent}
又或者是下面两种方式
javascript
const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)
但只要有嵌套对象,我们就会遇到问题:
javascript
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
可以解决这些缺点。
这里有一个例子:
javascript
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
就能看到:
javascript
{
title: "Builder.io Conf",
date: "1970-01-01T00:00:00.123Z"
attendees: ["Steve"]
}
这显然不是我们想要的!date
应该是一个 Date
对象而非字符串。
会出现这样的情况是因为 JSON.stringify
只能处理基本的对象、数组和基本数据类型。而其他数据类型的处理方式则各不相同。比如日期类型会被转为字符串,但 Set
类型则转换为 {}
。
JSON.stringify
甚至还会忽略某些类型,比如 undefined
或者方法。
比如下面这个例子,我们用 JSON.stringify
来拷贝 kitchenSink
对象:
javascript
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))
我们会得到这样的结果:
javascript
{
"set": {},
"map": {},
"regex": {},
"deep": {
"array": [
{}
]
},
"error": {},
}
是的,我们还必须删除循环引用,因为当 JSON.stringify
遇到后就会抛出错误。
当我们的需求满足时,尽管这个方式很棒,但 structuredClone
不仅也能做到并且还能做的更多。
为什么不是 _.cloneDeep
?
至今为止,Lodash
的 cloneDeep
方法已经是解决这个问题的通用方式。
并且实际也符合我们的需求:
javascript
import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
const clonedEvent = cloneDeep(calendarEvent)
但有一点需要注意。根据我的 IDE 中 Import Cost
插件显示,引用这一个方法就需要 17.4kb(压缩后为 5.3kb)。
这仅是引用这一个方法。如果你用更普通的方式引入,并没有意识到 Tree Shaking 不会总按照预期执行。那你将会因为这个方法额外引入 25kb。
当然这对任何人来说都不会是灭顶之灾。在我们的示例中没有必要,更不用说在已经内置了 structuredClone
的浏览器内。
什么 structuredClone
不能拷贝的
函数/方法不能被拷贝
会抛出 DataCloneError
错误。
javascript
// 🚩 Error!
structuredClone({ fn: () => { } })
DOM 节点
同样会抛出 DataCloneError
错误。
javascript
// 🚩 Error!
structuredClone({ el: document.body })
属性表述、setter 和 getter
类似的元数据(meta-data)不会被拷贝。
比如,当有 getter 时,会拷贝结果但不会拷贝函数本身(或者其他属性上的元数据):
javascript
structuredClone({ get foo() { return 'bar' } })
// Becomes: { foo: 'bar' }
对象原型
原型链不会被遍历或者重复。因此,当你拷贝一个 MyClass
的实例,被拷贝的对象将不会被认为是该类的实例(但该类中所有合法的属性都会被拷贝)。
javascript
class MyClass {
foo = 'bar'
myMethod() { /* ... */ }
}
const myClass = new MyClass()
const cloned = structuredClone(myClass)
// Becomes: { foo: 'bar' }
cloned instanceof myClass // false
支持的类型列表
简单地说,不在下面列表中的内容无法被拷贝。
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 Worker 有更多的限制。
来源: MDN
结论
虽然经历了长时间的等待,我们终于可以通过 structuredClone
让深拷贝在 JavaScript 中像呼吸那样简单。谢谢你,Surma.