前言
本文讲述3种前端常用的深度克隆方式,并进行相互比较。
- JSON.parse(JSON.stringify(obj))
- _.cloneDeep(obj)
- structuredClone(obj)
什么是深度克隆?
如果你只需要复制一个对象,也就是不复制嵌套对象或数组,那么我们可以只做一个对象传播:
const simpleEvent = {
title: "juejin.cn",
}
const shallowCopy = {...calendarEvent}
或者使用其中之一,
const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)
但是一旦我们有了嵌套项,我们就会遇到麻烦:
const calendarEvent = {
title: "juejin.cn",
date: new Date(123),
attendees: ["Tom"]
}
const shallowCopy = {...calendarEvent}
// 🚩 我们将会把"Bob"添加到 calendarEvent 和 shallowCopy 中
shallowCopy.attendees.push("Bob")
// 🚩 我们将会更新 calendarEvent 和 shallowCopy 中 的date
shallowCopy.date.setTime(456)
如您所见,我们并没有完整的复制这个对象。
嵌套的日期和数组数据两者之间仍然共享引用,如果我们只想编辑复制后的对象内的数据,这可能会给我们带来重大问题。
这时就需要使用深度克隆,或者是深度拷贝
常见的有三种
- JSON.parse(JSON.stringify(obj))
- _.cloneDeep(obj)
- structuredClone(obj)
下面我们将一一对比说明
JSON.parse(JSON.stringify(obj))
这个方法。它实际上是一个很棒的,并且性能出奇地好,但有一些缺点,
以此为例:
const calendarEvent = {
title: "juejin.cn",
date: new Date(123),
attendees: ["Tom"]
}
// 🚩 JSON.stringify 将 `date` 转换成字符串
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))
如果我们打印 problematicCopy, 得到:
{
title: "juejin.cn",
date: "1970-01-01T00:00:00.123Z"
attendees: ["Tom"]
}
那不是我们想要的! 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!')
}
const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))
得到:
{
"set": {},
"map": {},
"regex": {},
"deep": {
"array": [
{}
]
},
"error": {},
}
我们必须删除我们最初为此设置的循环引用,因为如果 JSON.stringify 遇到其中之一,它只会抛出错误。
因此,如果只是简单的数据结构使用 JSON.stringify 很棒,但是遇到复杂的嵌套就没办法了。
_.cloneDeep
迄今为止,Lodash 的 cloneDeep 函数是解决此问题的一种非常普遍的方法。
import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: "juejin.cn",
date: new Date(123),
attendees: ["Tom"]
}
const clonedEvent = structuredClone(calendarEvent)
但是。根据我的 IDE 中的 Import Cost 扩展,打印我导入的任何东西的 kb 成本,这个函数压缩后总共有 17.4kb(gzip 后为 5.3kb):
并且假设您只导入该功能。如果你以更常见的方式导入,你可能会不小心导入全部的Lodash高达 25kb 的数据,只是为了这个功能 😱
虽然这对任何人来说都不是世界末日,但在我们的情况下根本没有必要,因为浏览器已经内置了 structuredClone 。
structuredClone
const calendarEvent = {
title: "juejin.cn",
date: new Date(123),
attendees: ["Tom"]
}
// 😍
const copied = structuredClone(calendarEvent)
在上面的示例中我们不仅复制了对象,还复制了嵌套数组,甚至 Date 对象
copied.attendees // ["Tom"]
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
// ✅ 完整的克隆了所有
const clonedSink = structuredClone(kitchenSink)
缺点1:无法克隆函数
他们将抛出一个 DataCloneError 异常:
// 🚩 Error!
structuredClone({ fn: () => { } })
缺点2:无法克隆DOM 节点
// 🚩 Error!
structuredClone({ el: document.body })
缺点3:无法克隆 属性描述符、setter 和 getter
以及类似的类似元数据的功能不会被克隆。
例如,对于 getter,结果值被克隆,但不是 getter 函数本身(或任何其他属性元数据):
structuredClone({ get foo() { return 'bar' } })
// Becomes: { foo: 'bar' }
缺点4:无法克隆 原型
原型链未被遍历或复制。因此,如果您克隆 MyClass 的实例,则克隆的对象将不再是此类的实例(但此类的所有有效属性都将被克隆)
class MyClass {
foo = 'bar'
myMethod() { /* ... */ }
}
const myClass = new MyClass()
const cloned = structuredClone(myClass)
// Becomes: { foo: 'bar' }
cloned instanceof myClass // false
缺点5:无法克隆 错误类型
Error, EvalError, RangeError, ReferenceError , SyntaxError, TypeError, URIError
缺点6:无法克隆 网页/API 类型
AudioData, Blob, CryptoKey, DOMException, DOMMatrix, DOMMatrixReadOnly, DOMPoint, DomQuad, DomRect, File, FileList, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle, ImageBitmap, ImageData, RTCCertificate, VideoFrame
其他
更简单地说,任何不在下面列表中的东西都不能被克隆:
Array 、 ArrayBuffer 、 Boolean 、 DataView 、 Date 、 Error 类型(下面具体列出的那些)、 Map 、 Object 但仅限普通对象(例如来自对象字面量)、原始类型类型,除了 symbol (又名 number 、 string 、 null 、 undefined 、 boolean 、 BigInt )、 RegExp 、 Set 、 TypedArray
浏览器和运行时支持
这是最好的部分——所有主流浏览器都支持 structuredClone ,甚至 Node.js 和 Deno。
请注意 Web Workers 的支持更有限:
来源: MDN
总结
- JSON.parse(JSON.stringify(obj))
适用于简单基本的数据结构,往往是接口返回的数据,常量等
- _.cloneDeep(obj)
适用于无脑使用,如果不是追求项目的体积大小
- structuredClone(obj)
适用于没装lodash时复制复杂的对象。或者是装逼使用,或者八股文
全文完。
谢谢!
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天