JavaScript 中的 structuredClone():详尽解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 您是否知道,现在 JavaScript 中有一种原生的方式可以深拷贝对象?

您是否知道,现在 JavaScript 中有一种原生的方式可以深拷贝对象?

没错,这个内置于 JavaScript 运行时的structuredClone函数就是这样:

const calendarEvent = {
  title: "Builder.io大会",
  date: new Date(123),
  attendees: ["Steve"]
}
// 😍
const copied = structuredClone(calendarEvent)

您是否注意到在上面的例子中,我们不仅复制了对象,还复制了嵌套数组,甚至还包括 Date 对象?

一切正如预期工作:

copied.attendees // ["Steve"]
copied.date // Date: 1969年12月31日16:00:00
cocalendarEvent.attendees === copied.attendees // false

没错,structuredClone不仅能做到上述功能,还能够:

  • 克隆无限嵌套的对象和数组
  • 克隆循环引用
  • 克隆广泛的 JavaScript 类型,例如DateSetMapErrorRegExpArrayBufferBlobFileImageData,以及许多其他类型
  • 转移任何可转移对象

例如,甚至以下这种疯狂的情况也能如期工作:

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)

为什么不仅使用对象扩展?

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

const simpleEvent = {
  title: "Builder.io大会",
}
// ✅ 没问题,没有嵌套对象或数组
const shallowCopy = {...calendarEvent}

甚至如果您更喜欢,可以使用以下方式之一:

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

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

const calendarEvent = {
  title: "Builder.io大会",
  date: new Date(123),
  attendees: ["Steve"]
}
const shallowCopy = {...calendarEvent}
// 🚩 哎呀 - 我们刚刚将“Bob”添加到了复制品*和*原始事件中
shallowCopy.attendees.push("Bob")
// 🚩 哎呀 - 我们刚刚更新了复制品*和*原始事件的日期
shallowCopy.date.setTime(456)

如您所见,我们并没有完全复制这个对象。

嵌套的日期和数组仍然是两者之间的共享引用,如果我们想要编辑这些,认为我们只是在更新复制的日历事件对象,这可能会导致我们遇到重大问题。

为什么不用JSON.parse(JSON.stringify(x))?

啊,是的,这个技巧。实际上这是一个很好的方法,而且出人意料地高效,但structuredClone解决了它的一些缺点。

以此为例:

const calendarEvent = {
  title: "Builder.io大会",
  date: new Date(123),
  attendees: ["Steve"]
}
// 🚩 JSON.stringify 将`date`转换为了字符串
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))

如果我们记录problematicCopy,我们会得到:

{
  title: "Builder.io大会",
  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?

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

实际上,这确实如预期工作:

import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
  title: "Builder.io大会",
  date: new Date(123),
  attendees: ["Steve"]
}
const clonedEvent = cloneDeep(calendarEvent)

但是,这里只有一个警告。根据我的 IDE 中的 Import Cost 扩展,它打印了我导入的任何东西的 kb 成本,这一个功能的成本为整整 17.4kb压缩后的大小(5.3kb gzip 压缩后):

而这是假设您仅导入了那个功能。如果您以更常见的方式导入,没有意识到 tree shaking 并不总是按照您希望的方式工作,您可能意外地仅为这一个功能导入多达 25kb😱

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

什么是structuredClone不能克隆

函数不能被克隆

它们会抛出一个DataCloneError异常:

// 🚩 错误!
structuredClone({ fn: () => { } })

DOM 节点

也会抛出一个DataCloneError异常:

// 🚩 错误!
structuredClone({ el: document.body })

属性描述符、设置器和获取器

以及类似的元数据特性都不会被克隆。

例如,对于一个获取器,克隆的是结果值,而不是获取器函数本身(或任何其他属性元数据):

structuredClone({ get foo() { return 'bar' } })
// 变成了:{ foo: 'bar' }

对象原型

不会遍历或复制原型链。因此,如果您克隆了MyClass的一个实例,克隆的对象将不再被识别为这个类的实例(但这个类的所有有效属性将被克隆):

class MyClass { 
  foo = 'bar' 
  myMethod() { /* ... */ }
}
const myClass = new MyClass()
const cloned = structuredClone(myClass)
// 变成了:{ foo: 'bar' }
cloned instanceof myClass // false

支持的类型全列表

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

JS 内建类型

ArrayArrayBufferBooleanDataViewDateError类型(下面具体列出的那些)、MapObject但仅限于普通对象(例如,来自对象字面量)、原始类型,除了symbol(即numberstringnullundefinedbooleanBigInt)、RegExpSetTypedArray

错误类型

ErrorEvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError

Web/API 类型

AudioDataBlobCryptoKeyDOMExceptionDOMMatrixDOMMatrixReadOnlyDOMPointDomQuadDomRectFileFileListFileSystemDirectoryHandleFileSystemFileHandleFileSystemHandleImageBitmapImageDataRTCCertificateVideoFrame

浏览器和运行时支持

这里是最好的部分 - structuredClone在所有主要浏览器中都得到支持,甚至包括 Node.js 和 Deno。

只需注意 Web Workers 的支持较为有限的警告:

来源:MDN

结论

经过漫长的等待,我们终于现在有了structuredClone,使得在 JavaScript 中深度克隆对象变得轻而易举。

相关文章
|
25天前
|
JavaScript 前端开发 Go
CSS 与 JS 对 DOM 解析和渲染的影响
【10月更文挑战第16天】CSS 和 JS 会在一定程度上影响 DOM 解析和渲染,了解它们之间的相互作用以及采取适当的优化措施是非常重要的。通过合理的布局和加载策略,可以提高网页的性能和用户体验,确保页面能够快速、流畅地呈现给用户。在实际开发中,要根据具体情况进行权衡和调整,以达到最佳的效果。
|
21天前
|
存储 前端开发 JavaScript
JavaScript垃圾回收机制深度解析
【10月更文挑战第21】JavaScript垃圾回收机制深度解析
97 59
|
1月前
|
机器学习/深度学习 自然语言处理 JavaScript
信息论、机器学习的核心概念:熵、KL散度、JS散度和Renyi散度的深度解析及应用
在信息论、机器学习和统计学领域中,KL散度(Kullback-Leibler散度)是量化概率分布差异的关键概念。本文深入探讨了KL散度及其相关概念,包括Jensen-Shannon散度和Renyi散度。KL散度用于衡量两个概率分布之间的差异,而Jensen-Shannon散度则提供了一种对称的度量方式。Renyi散度通过可调参数α,提供了更灵活的散度度量。这些概念不仅在理论研究中至关重要,在实际应用中也广泛用于数据压缩、变分自编码器、强化学习等领域。通过分析电子商务中的数据漂移实例,展示了这些散度指标在捕捉数据分布变化方面的独特优势,为企业提供了数据驱动的决策支持。
61 2
信息论、机器学习的核心概念:熵、KL散度、JS散度和Renyi散度的深度解析及应用
|
30天前
|
JavaScript 前端开发 索引
JavaScript ES6及后续版本:新增的常用特性与亮点解析
JavaScript ES6及后续版本:新增的常用特性与亮点解析
26 4
|
13天前
|
前端开发 JavaScript
JavaScript新纪元:ES6+特性深度解析与实战应用
【10月更文挑战第29天】本文深入解析ES6+的核心特性,包括箭头函数、模板字符串、解构赋值、Promise、模块化和类等,结合实战应用,展示如何利用这些新特性编写更加高效和优雅的代码。
31 0
|
29天前
|
JavaScript 前端开发 开发者
原型链深入解析:JavaScript中的核心机制
【10月更文挑战第13天】原型链深入解析:JavaScript中的核心机制
28 0
|
30天前
|
JavaScript API
深入解析JS中的visibilitychange事件:监听浏览器标签间切换的利器
深入解析JS中的visibilitychange事件:监听浏览器标签间切换的利器
89 0
|
30天前
|
JavaScript
深入解析:JS与Vue中事件委托(事件代理)的高效实现方法
深入解析:JS与Vue中事件委托(事件代理)的高效实现方法
39 0
|
30天前
|
存储 JavaScript 前端开发
Vue.js项目中全面解析定义全局变量的常用方法与技巧
Vue.js项目中全面解析定义全局变量的常用方法与技巧
39 0
|
1月前
|
前端开发 JavaScript UED
JavaScript异步编程深入解析
【10月更文挑战第8天】JavaScript异步编程深入解析
14 0

推荐镜像

更多