• structuredClone() 详解


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

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

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

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

    一切正如预期工作:

    1. copied.attendees // ["Steve"]
    2. copied.date // Date: 1969123116:00:00
    3. cocalendarEvent.attendees === copied.attendees // false

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

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

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

    1. const kitchenSink = {
    2. set: new Set([1, 3, 3]),
    3. map: new Map([[1, 2]]),
    4. regex: /foo/,
    5. deep: { array: [ new File(someBlobData, 'file.txt') ] },
    6. error: new Error('Hello!')
    7. }
    8. kitchenSink.circular = kitchenSink
    9. // ✅ 全部良好,完全而深入地复制!
    10. const clonedSink = structuredClone(kitchenSink)

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

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

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

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

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

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

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

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

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

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

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

    以此为例:

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

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

    1. {
    2. title: "Builder.io大会",
    3. date: "1970-01-01T00:00:00.123Z"
    4. attendees: ["Steve"]
    5. }

    这不是我们想要的!date应该是一个Date对象,而不是一个字符串。

    这是因为JSON.stringify只能处理基本的对象、数组和原始类型。任何其他类型都以难以预测的方式处理。例如,日期被转换为字符串。但是Set简单地转换为{}

    JSON.stringify甚至完全忽略某些东西,如undefined函数

    例如,如果我们用这种方法复制我们的kitchenSink示例:

    1. const kitchenSink = {
    2. set: new Set([1, 3, 3]),
    3. map: new Map([[1, 2]]),
    4. regex: /foo/,
    5. deep: { array: [ new File(someBlobData, 'file.txt') ] },
    6. error: new Error('Hello!')
    7. }
    8. const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))

    我们会得到:

    1. {
    2. "set": {},
    3. "map": {},
    4. "regex": {},
    5. "deep": {
    6. "array": [
    7. {}
    8. ]
    9. },
    10. "error": {},
    11. }

    呃!

    哦,是的,我们不得不移除我们最初拥有的循环引用,因为JSON.stringify如果遇到其中的一个,就会简单地抛出错误。

    因此,虽然如果我们的需求适合它能做的事情,这种方法可能很好,但structuredClone(也就是上面我们未能做到的所有事情)能做许多这种方法不能做的事情。

    为什么不用_.cloneDeep?

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

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

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

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

    导入'lodash/cloneDeep'的成本截图,5.3kb gzip压缩后的大小

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

    导入'lodash'的成本截图,25.2kb gzip压缩后的大小

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

    什么是structuredClone不能克隆

    函数不能被克隆

    它们会抛出一个DataCloneError异常:

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

    DOM 节点

    也会抛出一个DataCloneError异常:

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

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

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

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

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

    对象原型

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

    1. class MyClass {
    2. foo = 'bar'
    3. myMethod() { /* ... */ }
    4. }
    5. const myClass = new MyClass()
    6. const cloned = structuredClone(myClass)
    7. // 变成了:{ foo: 'bar' }
    8. cloned instanceof myClass // false

    支持的类型全列表

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

    JS 内建类型

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

    错误类型

    ErrorEvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError

    Web/API 类型

    AudioDataBlobCryptoKeyDOMExceptionDOMMatrixDOMMatrixReadOnlyDOMPointDomQuadDomRectFileFileListFileSystemDirectoryHandleFileSystemFileHandleFileSystemHandleImageBitmapImageDataRTCCertificateVideoFrame

    浏览器和运行时支持

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

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

    浏览器支持表 - 直接来自下面链接的截图

    来源:MDN

    结论

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

  • 相关阅读:
    xv6源码阅读——虚拟内存
    大三实习生,字节跳动面经分享,已拿Offer
    G1D20-Anaconda&CSKE综述&AttacKG终于配好环境啦&KG book& CTF
    LeetCode-102-二叉树的层序遍历
    如何用Python做量化交易策略?
    斯坦福大学为机器人操作模仿学习设计了示教新范式
    【雷达通信】基于距离角度解耦法MIMO-OFDM雷达波束形成附matlab代码
    基于微信小程序的公交信息在线查询系统小程序设计与实现(源码+lw+部署文档+讲解等)
    Linux下安装MySQL8.0的详细步骤
    Ubuntu 24.04 LTS 安装 touchegg 开启触控板多指手势
  • 原文地址:https://blog.csdn.net/LiamHong_/article/details/136397264