JS 克隆对象八种技术,为何少不了 StructuredClone?
myzbx 2025-06-23 20:54 4 浏览
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
1. 为什么需要 StructuredClone
下面是一个使用 StructuredClone 方法克隆对象的示例:
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
// 赋值对象
const copied = structuredClone(calendarEvent)
以上代码示例表明,StructuredClone 不仅可以复制对象,还可以复制嵌套数组,甚至 Date 对象。
copied.attendees
// 输出 ["Steve"]
copied.date
// 输出 Date: Wed Dec 31 1969 16:00:00
cocalendarEvent.attendees === copied.attendees
// 输出 false
其实,除了能赋值以上对象之外,structuralClone 甚至支持克隆:
- 无限嵌套的对象和数组
- 循环引用
- 各种 JavaScript 类型,例如: Date、Set、Map、Error、RegExp、ArrayBuffer、Blob、File、ImageData 等
- 转移任何可转移对象
1.可转移的对象(Transferable object)是拥有属于自己资源的对象,这些资源可以从一个上下文转移到另一个,确保资源一次仅在一个上下文可用。传输后,原始对象不再可用,也不再指向转移后的资源,并且任何读取或者写入该对象的尝试都将抛出异常。
2.可转移对象通常用于共享资源,该资源一次仅能安全地暴露在一个 JavaScript 线程中。例如,ArrayBuffer 是一个拥有内存块的可转移对象。当此类缓冲区(buffer)在线程之间传输时,相关联的内存资源将从原始的缓冲区分离出来,并且附加到新线程创建的缓冲区对象中。原始线程中的缓冲区对象不再可用,因为它不再拥有属于自己的内存资源了。
比如下面的示例:
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)
2. 为何不使用 spread 操作符
值得注意的是,文章正在谈论的是创建深层副本。如果只需要进行浅复制,即不复制嵌套对象或数组的副本,那么可以考虑 rest 符:
const simpleEvent = {
title: "Builder.io Conf",
}
// 可以,这里没有嵌套对象或者数组
const shallowCopy = {...calendarEvent}
当然,开发者还可以使用下面 Object.assign 或者 Object.create 的方式:
const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)
但是一旦存在嵌套元素,rest 就会遇到麻烦:
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
const shallowCopy = {...calendarEvent}
// 这里添加 "Bob" 字符串到原始数组和复制后的数组中了
shallowCopy.attendees.push("Bob")
// 原始 Date 对象和复制的对象全部受到影响
shallowCopy.date.setTime(456)
正如以上示例所见,rest 操作符并没有创建该对象的完整副本,嵌套日期和数组仍然是两者之间的共享引用。
3. 为什么不 JSON.parse(JSON.stringify(x)) 深度复制
JSON.parse(JSON.stringify(x)) 是众多开发者很喜欢使用的优秀工具方法,并且具有不错的性能,但也有一些缺点,而这些缺点才是 structuralClone 脱颖而出的地方。
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
// JSON.stringify 将 `date` 属性转化为字符串了
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 只能处理基本对象、数组和 Primitives 原始属性。 任何其他类型都可以以难以预测的方式处理。 例如,日期被转换为字符串, Set 只是转换为 {}。
JSON.stringify 甚至可能完全忽略某些属性,例如: undefined 或函数。如果使用 JSON.parse(JSON.stringify(x)) 复制 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 做很多该方法无法做到的事情。
4. 为什么不用_.cloneDeep 深度复制
迄今为止,Lodash 的 cloneDeep 函数是解决深度复制一个非常常见的方案。
import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
const clonedEvent = cloneDeep(calendarEvent)
但是,根据 Vscode 的插件 Import Cost VSCode Extension 提示,该函数压缩后体积总共有 17.4kb(压缩后为 5.3kb)。
假设只是简单导入该函数,却没有意识到 Tree Shaking 可能不起作用,则可能意外导入多达 25kb 的数据 。
5. 为什么不用 MessageChannel 深度复制
开发者可以创建一个 MessageChannel 并发送消息。在接收端,消息将包含原始数据对象的结构化克隆。
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
const obj = /* ... */;
const clone = await structuralClone(obj);
该方法的缺点是异步,虽然并无大碍,但是有时候开发者需要使用同步的方式来深度拷贝一个对象。
6. 为什么不用 History API 深度复制
history.pushState() 可以提供一个状态对象来保存 URL。事实证明,这个状态对象使用结构化克隆,而且是同步的。但是,开发者必须小心使用,不要把程序逻辑使用的状态对象搞乱,所以需要在完成克隆之后恢复原始状态。为了防止发生任何意外,请使用 history.replaceState() 而不是 history.pushState()。
function structuralClone(obj) {
const oldState = history.state;
// 老的 state
history.replaceState(obj, document.title);
const copy = history.state;
// 新的 state
history.replaceState(oldState, document.title);
return copy;
}
const obj = /* ... */;
const clone = structuralClone(obj);
然而,仅仅为了复制一个对象,而使用浏览器的引擎,感觉有点过分。另外,Safari 浏览器对 replaceState 调用的限制数量为 30 秒内 100 次。
7. 为什么不使用 Notification API 深度复制
Notification API 需要浏览器内部的权限机制,所以可能很慢。而且由于某种原因,Safari 总是返回 undefined。
function structuralClone(obj) {
return new Notification('', {data: obj, silent: true}).data;
}
const obj = /* ... */;
const clone = structuralClone(obj);
8.StructuredClone 方法注意事项
函数不能被克隆
将抛出 DataCloneError 异常:
// VM216:1 Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': () => { } could not be cloned. at <anonymous>:1:1
structuredClone({fn: () => { } })
DOM 节点
引发 DataCloneError 异常:
// VM220:1 Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': HTMLBodyElement object could not be cloned. at <anonymous>:1:1
structuredClone({el: document.body})
属性描述符、setter 和 getter 类似
类似元数据的功能也不会被克隆。 例如,使用 getter 时,会克隆结果值而不会克隆 getter 函数本身(或任何其他属性元数据):
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
9.StructuredClone polyfill
StructuredClone 虽然功能强大,但是并非所有浏览器都支持,此时就需要使用 structured-clone polyfill 了。关于 polyfill、ponyfill、prolyfill 的差异可以参考我的另外的文章,这里不再过多展开。
structured-clone 是与环境无关的序列化器和反序列化器,具有递归能力和 HTML 标准本身超出 JSON 的类型。值得注意的是,structured-clone 目前尚不支持包括:Blob、File、FileList、ImageBitmap、ImageData 和 ArrayBuffer,但已经支持类型化数组(Typed Arrays),但 u/int8、u/int16 和 u/int32 是目前唯一安全支持的。
可以通过下面的方式使用 structured-clone 的 polyfill:
// 默认导出
import structuredClone from '@ungap/structured-clone';
const cloned = structuredClone({any: 'serializable'});
// 作为独立的 serializer/deserializer
import {serialize, deserialize} from '@ungap/structured-clone';
// result 可以作为 JSON stringified,即使有 recursive 数据、bigint、typed arrays 等
const serialized = serialize({any: 'serializable'});
// 结果将作为原始对象的副本
const deserialized = deserialize(serialized);
当然,也可以按照 Global Polyfill 的方式使用:
// polyfill 作为全局函数
import structuredClone from "@ungap/structured-clone";
if (!("structuredClone" in globalThis)) {
globalThis.structuredClone = structuredClone;
}
// Or don't monkey patch
import structuredClone from "@ungap/structured-clone"
// Just use it in the file
structuredClone()
参考资料
https://www.builder.io/blog/structured-clone
https://github.com/ungap/structured-clone
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Transferable_objects
https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost
https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel
https://kikobeats.com/polyfill-ponyfill-and-prollyfill/
https://www.toutiao.com/article/7200739725316030976
https://matiashernandez.dev/blog/post/deep-cloning-in-javascript-the-modern-way.-use-%60structuredclone%60
https://twitter.com/builderio/status/1519743620529766401
相关推荐
- 陈冠希飞机争执事件:维权还是失态?
-
陈冠希最近又上热搜了!这次不是因为潮牌,而是在飞机上和机组人员“杠”上了。事情是这样的:他在东京飞纽约的航班上,发现机组人员让一名日籍VIP乘客优先下机,当场就炸了,直接质问:“我跟他哪里不一样?钻石...
- 风向变了,小S被吴宗宪猛爆黑料,至亲好友背刺,s家乱成一锅粥
-
前言当吴宗宪5月26日直播中甩出"黄子佼犯罪小S知情"的录音时,谁还记得这对师徒曾在《我猜》里默契十足的黄金年代?昔日提携晚辈的综艺天王,如今用三小时连爆12条黑料,把综艺女王钉在道德...
- 吴宗宪开撕小S,离婚内幕疑曝光,S家起内讧,汪小菲果然没说错
-
文|东方不败难怪葛思琪说小S大概率是不能复出了。原来一切都是有迹可循的!被吴宗宪猛曝黑料、被至亲好友背刺。失去大S的s家彻底乱成一锅粥。小S还能如以往那般幸运地“化险为夷”吗?01不得不说,作为台湾主...
- 美国俄亥俄大学性侵案细节曝光,新纪录片揭开体育界被忽视的丑闻
-
美国俄亥俄州立大学一直是美国校际体育运动的标杆,以至于很少有人将该大学与美国历史上最令人震惊的性虐待丑闻联系起来。近日,由澳大利亚纪录片导演伊娃·奥纳(EvaOrner)执导的《俄亥俄州立大学的幸存...
- 陈冠希飞机上怒怼空姐,称要让其丢掉工作?原因曝光后大家纷纷支持
-
【点新闻报道】44岁的陈冠希(Edison)被爆料在一架由东京羽田飞往纽约的航班上,疑不满头等舱的下机安排,与空姐发生口角,甚至放话:“把客诉信拿来,我会让你丢工作!”,引发网上热议。有内地网民在小红...
- 陈冠希机上风波再起!一场由“优先权”引发的对峙
-
一句“我会让你丢工作”的激烈争执录音,将陈冠希再次推向风口浪尖。飞机引擎的轰鸣尚未完全停歇,纽约机场的廊桥尚未对接,头等舱内的空气却已骤然凝固。44岁的陈冠希,这位早已褪去偶像光环却始终身处舆论漩涡...
- 传祺M8 vs 别克GL8,谁才是MPV终极选择?
-
广汽传祺M8与别克GL8一直都是很多人在选择MPV时纠结的对象,尤其是对于选择“困难症”的朋友来说,更是如此。今天我们将广汽传祺M8大师超混版和别克GL8ES陆尊进行对比,看看究竟怎么选!不是合资买...
- 开源鸿蒙OpenHarmony 6.0 Beta1发布
-
IT之家6月19日消息,开源鸿蒙OpenHarmony6.0Beta1(APILevel20)现已发布并上线Gitee。据介绍,OpenHarmony6.0Beta1版本进一...
- 巴雷特(Barrett)食管(巴雷特食管?)
-
近年来随着HP根除的增加等因素存在,食管胃结合部腺癌发病率逐年增加,食管胃结合部腺癌主要包括Barrett腺癌、胃贲门腺癌,而Barrett食管(Barrett’sesophagus,BE)为Bar...
- 儿子对象三天不出门 吵架动手后关系僵持
-
这几天家里事儿多。儿子交的女朋友搬来同住三天,人跟消失似的。每天中午才起床吃我家做的饭,吃完就喊着出去,问晚上回不回来,答不回来。昨天中午我找她谈儿子动手的事,她也不说话,现在微信电话全拉黑,连饭都不...
- 偷鸡不成蚀把米!命理师称小S将有大劫,老公许雅钧被爆换继承人
-
近期有命理师称小S将有大劫,其老公许雅钧也被爆换继承人,具体情况如下:命理师称小S有大劫有台湾省命理师称小S面相不好,将会有一场“大劫”,会影响到她生活的重大事件。还有细心网友翻出2022年某命理师在...
- 如何设计Agent的记忆系统(agent记忆方法)
-
最近看了一张画Agent记忆分类的图我觉得分类分的还可以,但是太浅了,于是就着它的逻辑,仔细得写了一下在不同的记忆层,该如何设计和选型先从流程,作用,实力和持续时间的这4个维度来解释一下这几种记忆:1...
- 深入理解跨域及常见误区揭秘(深入理解跨域及常见误区揭秘论文)
-
跨域问题是前端与后端协作中不可避免的话题,处理不当将直接影响系统可用性与安全性。本文将系统梳理跨域的概念、原理、常见解决方案,并结合实际开发中易错点进行总结,帮助你全面掌握跨域知识。一、什么是跨域?*...
- aardio + Java + JavaScript 混合开发快速入门
-
aardio最近在AI功能上做了很多细节的改进,建议大家更新。aardio的AI接口里的Gemini2.5pro更新到了刚发布的最新版本(Gemini2.5pro0605),...
- 一种改进的锂离子电池剩余寿命预测算法
-
摘要:锂离子电池故障往往会使系统性能下降甚至瘫痪,故障部件剩余寿命的精确估计对整个系统的寿命预测和健康管理至关重要。粒子滤波是一种有效的序列信号处理方法,然而应用于锂离子电池剩余寿命预测准确性并不高...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 选择器 (30)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)