优雅的深度克隆,三种常用的前端克隆方式

582 阅读5分钟

前言

本文讲述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):

Screenshot of the import cost of 'lodash/cloneDeep' at 5.3kb gzipped

并且假设您只导入该功能。如果你以更常见的方式导入,你可能会不小心导入全部的Lodash高达 25kb 的数据,只是为了这个功能 😱

Screenshot of the import cost of 'lodash' at 25.2kb gzipped

虽然这对任何人来说都不是世界末日,但在我们的情况下根本没有必要,因为浏览器已经内置了 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:无法克隆 错误类型

ErrorEvalErrorRangeErrorReferenceError , SyntaxErrorTypeErrorURIError

缺点6:无法克隆 网页/API 类型

AudioDataBlobCryptoKeyDOMExceptionDOMMatrixDOMMatrixReadOnlyDOMPointDomQuadDomRectFileFileListFileSystemDirectoryHandleFileSystemFileHandleFileSystemHandleImageBitmapImageDataRTCCertificateVideoFrame

其他

更简单地说,任何不在下面列表中的东西都不能被克隆:

Array 、 ArrayBuffer 、 Boolean 、 DataView 、 Date 、 Error 类型(下面具体列出的那些)、 Map 、 Object 但仅限普通对象(例如来自对象字面量)、原始类型类型,除了 symbol (又名 number 、 string 、 null 、 undefined 、 boolean 、 BigInt )、 RegExp 、 Set 、 TypedArray

浏览器和运行时支持

这是最好的部分——所有主流浏览器都支持 structuredClone ,甚至 Node.js 和 Deno。

请注意 Web Workers 的支持更有限:

Browser support table - screenshot from the link directly below this image

来源: MDN

总结

  • JSON.parse(JSON.stringify(obj))

适用于简单基本的数据结构,往往是接口返回的数据,常量等

  • _.cloneDeep(obj)

适用于无脑使用,如果不是追求项目的体积大小

  • structuredClone(obj)

适用于没装lodash时复制复杂的对象。或者是装逼使用,或者八股文

全文完。

谢谢!

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天

点击查看活动详情