【译】深度克隆对象在JavaScript中的现代方法

  • × _.cloneDeep(obj)
  • × JSON.parse(JSON.stringify(obj))
  • structuredClone(obj)

你知道吗,在JavaScript中现在有一种原生的方式来深度复制对象吗?

没错,这个structuredClone函数已经内置到了JavaScript运行时中:

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  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等等
  • 传输任何可传输的对象

所以例如,这种疯狂的操作甚至也能按预期工作:

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)

为什么不只是使用对象扩展呢?

重要的是要注意我们正在谈论深度复制。如果你只需要进行浅复制,也就是不复制嵌套对象或数组的复制,那么我们可以简单地使用对象扩展:

const simpleEvent = {
  title: "Builder.io Conf",
}
// ✅ no problem, there are no nested objects or arrays
const shallowCopy = {...calendarEvent}

或者如果你更喜欢的话,甚至可以使用这些之一:

const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)

但是一旦我们有了嵌套的项目,就会遇到麻烦:

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 可以解决这些问题。

拿这个作为例子:

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,就会得到:

{
  title: "Builder.io Conf",
  date: "1970-01-01T00:00:00.123Z"
  attendees: ["Steve"]
}

这不是我们想要的!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 遇到循环引用时会直接抛出错误。

因此,如果我们的要求符合该方法的功能,那么该方法就会很好,但我们可以使用 structuredClone(也就是我们在此未完成的上述所有操作)完成很多该方法无法完成的操作。

为什么不使用 _.cloneDeep 呢?

到目前为止,LodashcloneDeep 函数一直是解决这个问题的一个非常常见的解决方案。

而且,事实上,这的确按预期工作:

import cloneDeep from 'lodash/cloneDeep'

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

const clonedEvent = cloneDeep(calendarEvent)

但是,这里有一个小问题。根据IDE中的Import Cost扩展,它会打印我导入的任何东西的kb成本,这一个函数在被压缩后的大小为17.4kb(gzip压缩后为5.3kb):

// 17.4kb (5.3kb gzipped)
import cloneDeep from lodash/cloneDeep;

而且这还假设你只导入了这个函数。如果你导入了更常见的方式,却没有意识到摇树优化并不总是按照你希望的方式工作,你可能会因为这一个函数而意外地导入多达25kb的代码量 😱

// 71.5kb (25.2kb gzipped)
import _ from 'lodash'

虽然这对任何人来说都不会是世界末日,但在我们的情况下根本没有必要,因为浏览器已经内置了 structuredClone

structuredClone 不能克隆什么

  • 函数不能被克隆

它会抛出一个 DataCloneError 异常:

// 🚩 Error!
structuredClone({ fn: () => { } })
  • DOM节点

也会抛出 DataCloneError 异常:

// 🚩 Error!
structuredClone({ el: document.body })
  • 属性描述符、设置器和获取器以及类似的元数据特性也不会被克隆

例如,使用 getter,结果值会被克隆,但是 getter 函数本身(或任何其他属性元数据)不会被克隆:

structuredClone({ get foo() { return 'bar' } })
// Becomes: { foo: 'bar' }
  • 对象原型链不会被遍历或复制

因此,如果你克隆了 MyClass 的一个实例,克隆的对象将不再被认为是这个类的一个实例(但是该类的所有有效属性将被克隆)。

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(但仅限普通对象,例如来自对象字面的对象)、基础类型(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。只要注意一下 Web Workers 的支持有些有限即可:

【译】深度克隆对象在JavaScript中的现代方法

MDN 上有更多关于 structuredClone 的信息。

结论

structuredClone 是一个非常强大的工具,它可以在 JavaScript 中进行深度复制,而且它的功能非常强大,可以处理各种类型的对象,包括循环引用和嵌套对象。虽然姗姗来迟,但我们现在终于有了 structuredClone,可以轻而易举地在 JavaScript 中深度克隆对象。

原文链接

原文链接:https://juejin.cn/post/7345312252478914600 作者:佳玮

(0)
上一篇 2024年3月13日 上午10:31
下一篇 2024年3月13日 上午10:41

相关推荐

发表回复

登录后才能评论