structuredClone() 详解

您是否知道,现在 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 压缩后):

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

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

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

虽然这对任何人来说都不会是世界末日,但在我们的案例中,这根本不是必要的,不是在浏览器中已经内置了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 中深度克隆对象变得轻而易举。

  • 原文链接:Deep Cloning Objects in JavaScript, the Modern Way

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/504286.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣hot100题解(python版36-40题)

36、二叉树的中序遍历 给定一个二叉树的根节点 root ,返回 它的 *中序 遍历* 。 示例 1: 输入:root [1,null,2,3] 输出:[1,3,2]示例 2: 输入:root [] 输出:[]示例 3: 输入&am…

SparkStreaming在实时处理的两个场景示例

简介 Spark Streaming是Apache Spark生态系统中的一个组件,用于实时流式数据处理。它提供了类似于Spark的API,使开发者可以使用相似的编程模型来处理实时数据流。 Spark Streaming的工作原理是将连续的数据流划分成小的批次,并将每个批次作…

判断点是否在多边形内

std::vector<cv::Point2d> vanCorner_;bool inArea(const Pose &pos) {cv::Point2d point cv::Point2d(pos.position.x(), pos.position.y());double distance cv::pointPolygonTest(vanCorner_, point, false);return distance > 0; } 似乎效果不太好,会误报 …

Go 互斥锁的实现原理?

Go sync包提供了两种锁类型&#xff1a;互斥锁sync.Mutex 和 读写互斥锁sync.RWMutex&#xff0c;都属于悲观锁。 概念 Mutex是互斥锁&#xff0c;当一个 goroutine 获得了锁后&#xff0c;其他 goroutine 不能获取锁&#xff08;只能存在一个写者或读者&#xff0c;不能同时…

现货商品国际挂牌撮合系统功能说明书

现货商品国际挂牌撮合系统功能说明书 一、系统概述 现货商品国际挂牌撮合系统是一个基于互联网技术的电子交易平台&#xff0c;旨在为全球现货商品买卖双方提供高效、透明、公正的撮合服务。该系统通过先进的撮合算法和交易规则&#xff0c;确保交易的快速匹配和成交&#xf…

Netty5 入门HelloWorld

一、客户端代码及关键类说明 /*** netty5的客户端* author -zhengzx-**/ public class ClientSocket {public static void main(String[] args) {//服务类Bootstrap bootstrap new Bootstrap();//workerEventLoopGroup worker new NioEventLoopGroup();try {//设置线程池boo…

基础小白快速入门c语言--

变量&#xff1a; 表面理解&#xff1a;在程序运行期间&#xff0c;可以改变数值的数据&#xff0c; 深层次含义&#xff1a;变量实质上代表了一块儿内存区域&#xff0c;我们可以将变量理解为一块儿内存区域的标识&#xff0c;当我们操作变量时&#xff0c;相当于操作了变量…

leetcode hot100 每日温度

在本题中&#xff0c;我们是通过单调栈来解决的&#xff0c;因为我们采用了栈的数据结构&#xff0c;并且&#xff0c;栈内存储的元素是单调的。 本题我们考虑&#xff0c;将气温数组元素的下标存入栈中&#xff0c;首先初始化要把0放入&#xff0c;0是下标的意思。然后我们拿…

回溯热门问题(算法村第十八关白银挑战)

组合总和 39. 组合总和 - 力扣&#xff08;LeetCode&#xff09; 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 &#xff0c;并以列表形式返回。你可以按 任意顺序 返回这…

buuctf misc做题笔记

喵喵喵 使用stegsolve.jar&#xff0c;按BGR顺序提取出一个png图片&#xff0c;是一个只显示一半的二维码&#xff0c;修改图片高度显示全部二维码&#xff0c;解析出一个百度网盘地址&#xff0c;https://pan.baidu.com/s/1pLT2J4f 下载得到压缩包flag.rar。解压成功&#xf…

【JavaEE进阶】 代理模式

文章目录 &#x1f343;前言&#x1f38b;什么叫代理模式&#x1f334;静态代理&#x1f38d;动态代理&#x1f6a9;JDK动态代理&#x1f6a9;CGLIB动态代理 ⭕总结 &#x1f343;前言 前面对Spring AOP的详细使用进行了介绍&#xff0c;这篇博客博主将详细讲解一下Spring AOP…

(面试题)数据结构:链表相交

问题&#xff1a;有两个链表&#xff0c;如何判断是否相交&#xff0c;若相交&#xff0c;找出相交的起始节点 一、介绍 链表相交&#xff1a; 若两个链表相交&#xff0c;则两个链表有共同的节点&#xff0c;那从这个节点之后&#xff0c;后面的节点都会重叠&#xff0c;知道…