前端手写深拷贝/深克隆是一道回头率超高的笔试题,但笔试版一般不适用于生产环境,JSON 的奇技淫巧和 Lodash 的工具函数也各有缺点。
您知道吗,JS 现在有一种原生方法可以深层复制对象?
structuredClone
函数内置在 JS 运行时中:
const calendarEvent = {
title: '攻城狮',
date: new Date(111),
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 等等
-
传送任何可转移对象(transferable objects)
举个栗子,这种奇葩操作甚至也会如期工作:
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)
1. 为什么不选择展示对象克隆呢?
注意,我们正在谈论的是深拷贝。如果您只需浅拷贝,即不复制嵌套对象或数组的副本,那么我们可以直接展开对象克隆:
const simpleEvent = {
title: '攻城狮'
}
// ✅ 问题不大,此处没有嵌套对象/数组
const shallowCopy = { ...calendarEvent }
或者其他备胎,只要您愿意:
const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)
虽然但是,一旦我们嵌套了元素,我们就会遭遇“滑铁卢”:
const calendarEvent = {
title: '攻城狮',
date: new Date(123),
attendees: ['Steve']
}
const shallowCopy = { ...calendarEvent }
// 🚩 夭寿啦:我们同时在 calendarEvent 及其副本中添加了 Bob
shallowCopy.attendees.push('Bob')
// 🚩 天呢噜:我们同时为 calendarEvent 及其副本更新了 date
shallowCopy.date.setTime(456)
如你所见,我们没有完整拷贝该对象。
嵌套日期和数组仍然是两者之间的共享引用,如果我们想编辑那些被认为只会更新 calendarEvent
对象副本的内容,这可能会给我们带来无妄之灾。
2. 为什么不选择JSON.parse(JSON.stringify(i))呢?
它实际上是一个很棒的点子,且具有惊人的性能,但存在若干 structuredClone
解决了的短板。
如下所示:
const calendarEvent = {
title: '攻城狮',
date: new Date(123),
attendees: ['Steve']
}
// JSON.stringify 会把 date 转换为字符串
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))
如果我们打印 problematicCopy
,我们会看到:
{
title: "攻城狮",
date: "1970-01-01T00:00:00.123Z"
attendees: ["Steve"]
}
这不是我们想要的!date
应该是 Date
对象,而不是字符串。
发生这种情况是因为 JSON.stringify
只能处理基本对象、数组和原始值。处理任何其他类型都十分佛系。举个栗子,Date
被转换为字符串。但 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!')
}
const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))
结果如下:
{
"set": {},
"map": {},
"regex": {},
"deep": {
"array": [
{}
]
},
"error": {},
}
我们必须删除最初为此使用的循环引用,因为如果 JSON.stringify
遭遇其中之一,就能且仅能报错。
因此,虽然如果我们的需求刚好符合其功能,这个方法自然棒棒哒,但我们可以用 structuredClone
肝一大坨事情(也就是上述我们未能做到的事情),而此方法却做不到。
3. 为什么不选择_.cloneDeep呢?
迄今为止,Lodash 的 cloneDeep
函数已经是解决此问题的一个十分常见的解决方案。
事实上,这确实能如期工作:
import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: '攻城狮',
date: new Date(123),
attendees: ['Steve']
}
// ✅ 一切顺利!
const clonedEvent = structuredClone(calendarEvent)
虽然但是,此时有且仅有一个警告。根据本人 IDE 中的导入成本(import cost)扩展,它会打印我导入的任何内容的 kb 成本,该函数压缩后总共有 17.4kb(gzip 压缩后为 5.3kb):
而这是假设您只导入了该函数的情况。如果您以更常见的方式导入,却没有意识到 Tree Shaking 优化并不总是如期奏效,您可能会一不小心仅针对这一函数导入多达 25kb 的数据 😱
虽然这对任何人而言都不会是世界末日,但在我们的例子中根本没有必要,尤其是浏览器已经内置了 structuredClone
。
4. structuredClone的短板
无法克隆函数
这会报错 —— DataCloneError
异常:
// 🚩 报错!
structuredClone({ fn: () => {} })
DOM节点
梅开二度 —— DataCloneError
异常:
// 🚩 报错!
structuredClone({ el: document.body })
5. 属性描述符,setters和getters
类似的类元数据(metadata-like)的功能也无法被克隆。
举个栗子,使用 getter
时,会克隆结果值,但不会克隆 getter
函数本身(或任何其他属性元数据):
structuredClone({
get foo() {
return 'bar'
}
})
// 结果变成: { foo: 'bar' }
6. 对象原型
原型链不会被遍历或重复。因此,如果您克隆 MyClass
的实例,那么克隆对象将不再被视为此类的实例(但此类的所有有效属性都将被克隆)
class MyClass {
foo = 'bar'
myMethod() {
/* ... */
}
}
const myClass = new MyClass()
const cloned = structuredClone(myClass)
// 结果变成: { foo: 'bar' }
cloned instanceof myClass // false
7. 支持的类型的完整列表
简而言之,下述列表中未列出的任何内容都无法克隆:
JS内置函数
Array
、ArrayBuffer
、Boolean
、DataView
、Date
、Error
类型(那些下面具体列出),Map
,仅限于普通对象的 Object
(比如来自对象字面量),除了 symbol
的原始类型(又名 number
、string
、null
、undefined
、boolean
、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
浏览器和运行时支持
这是最好的部分 —— 所有主流浏览器都支持 structuredClone
,甚至包括 Node.js 和 Deno。
添加好友备注【进阶学习】拉你进技术交流群