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

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

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

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

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等等
  • 传输任何可传输的对象

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

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 或函数。

例如,如果我们用这种方法复制我们的 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 呢?

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

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

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扩展,它会打印我导入的任何东西的kb成本,这一个函数在被压缩后的大小为17.4kb(gzip压缩后为5.3kb):

javaScript 复制代码
// 17.4kb (5.3kb gzipped)
import cloneDeep from lodash/cloneDeep;

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

javascript 复制代码
// 71.5kb (25.2kb gzipped)
import _ from 'lodash'

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

structuredClone 不能克隆什么

  • 函数不能被克隆

它会抛出一个 DataCloneError 异常:

javascript 复制代码
// 🚩 Error!
structuredClone({ fn: () => { } })
  • DOM节点

也会抛出 DataCloneError 异常:

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

例如,使用 getter,结果值会被克隆,但是 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(但仅限普通对象,例如来自对象字面的对象)、基础类型(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 的支持有些有限即可:

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

结论

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

原文链接

相关推荐
未来之窗软件服务34 分钟前
资源管理器必要性———仙盟创梦IDE
前端·javascript·ide·仙盟创梦ide
西哥写代码2 小时前
基于cornerstone3D的dicom影像浏览器 第十八章 自定义序列自动播放条
前端·javascript·vue
清风细雨_林木木2 小时前
Vue 中生成源码映射文件,配置 map
前端·javascript·vue.js
雪芽蓝域zzs3 小时前
JavaScript splice() 方法
开发语言·javascript·ecmascript
森叶3 小时前
Electron 主进程中使用Worker来创建不同间隔的定时器实现过程
前端·javascript·electron
霸王蟹4 小时前
React 19 中的useRef得到了进一步加强。
前端·javascript·笔记·学习·react.js·ts
霸王蟹4 小时前
React 19版本refs也支持清理函数了。
前端·javascript·笔记·react.js·前端框架·ts
codelxy4 小时前
vue引用cesium,解决“Not allowed to load local resource”报错
javascript·vue.js
程序猿阿伟5 小时前
《社交应用动态表情:RN与Flutter实战解码》
javascript·flutter·react native
明似水5 小时前
Flutter 开发入门:从一个简单的计数器应用开始
前端·javascript·flutter