type
status
date
slug
summary
tags
category
icon
password
前端面试中,“手写深拷贝/深克隆”可谓是常客中的常客。但传统的面试版深拷贝方案往往不适用于复杂的生产环境,而广为流传的
JSON.parse(JSON.stringify()) 奇技淫巧以及 Lodash 等工具库的 _.cloneDeep 函数也各有其局限性。本期《前端翻译计划》将为大家隆重介绍一种由 JavaScript 运行时原生支持的现代深克隆方法——
structuredClone。你没听错,JavaScript 现在拥有一个原生的方法来实现对象的深层复制!它就是内置于 JS 运行时的
structuredClone 函数。注意到没?上面的例子中,我们不仅复制了对象本身,还完美复制了其嵌套的数组以及
Date 对象。所有的一切都如预期般工作:structuredClone 的强大之处远不止于此,它还能:- 克隆无限嵌套的对象和数组。
- 优雅处理循环引用(对象自我引用或相互引用)。
- 克隆多种 JavaScript 内置类型,例如
Date、Set、Map、Error、RegExp、ArrayBuffer、Blob、File、ImageData等等。
- 传送(transfer)任何可转移对象 (transferable objects)。
举个“栗子”,即便是下面这种包含各种复杂类型的“大杂烩”对象,
structuredClone 也能轻松搞定:为什么不选择展开运算符 (...) 进行对象克隆呢?
首先明确,我们这里讨论的是深拷贝。如果你只需要浅拷贝(即只复制对象的第一层属性,而不复制嵌套对象或数组的副本),那么使用对象展开运算符 (
...) 是完全可以的:或者,你也可以选择其他浅拷贝的备胎方案:
然而,一旦我们的对象包含了嵌套元素,浅拷贝的“滑铁卢”就来了:
如你所见,这根本不是一个完整的副本。嵌套的
Date 对象和 attendees 数组在原对象和浅拷贝副本之间仍然是共享引用。如果我们期望的只是修改副本,这种共享引用无疑会给我们带来“无妄之灾”。为什么不选择 JSON.parse(JSON.stringify(x)) 呢?
啊对对对,这确实是个流传甚广的“奇技淫巧”。它在处理纯JSON兼容数据时性能表现惊人,思路也简单粗暴。但它存在若干
structuredClone 能够完美解决的硬伤。看招:
如果我们打印
problematicCopy,会看到:这显然不是我们想要的!
date 属性应该是一个 Date 对象,而不是字符串。undefined 和函数属性也丢失了。发生这种情况是因为
JSON.stringify 只能正确处理 JavaScript 的基本对象、数组和原始类型(字符串、数字、布尔值、null)。对于其他任何类型,它的处理方式就十分“佛系”了。例如,Date 对象被转换为字符串,Set 和 Map 对象会转换为空对象 {},RegExp 也会变为空对象。更要命的是,
JSON.stringify 甚至会完全忽略某些内容,比如 undefined 属性和任何函数。如果我们用此方法复制前面提到的
kitchenSink 对象(为避免报错,先移除循环引用):结果如下:
我的天姥爷!哇哦,是的没错,如果
JSON.stringify 遇到循环引用,它会且仅会抛出一个 TypeError。因此,虽然
JSON.parse(JSON.stringify()) 在其适用范围内是个不错的选择,但 structuredClone 能处理的场景远超于它,尤其是那些它处理不了或处理不好的复杂类型和循环引用。为什么不选择 Lodash 的 _.cloneDeep 呢?
迄今为止,Lodash 库的
_.cloneDeep 函数一直是解决 JavaScript 深拷贝问题的一个非常流行且可靠的方案。事实上,它确实能如期工作:
虽然但是,这里有且仅有一个“小”警告。根据我IDE中的“导入成本 (Import Cost)”扩展显示,即使只导入
cloneDeep 这一个函数,其压缩后的大小也有约 17.4KB(gzip压缩后约 5.3KB):这还是在你正确地只导入了单个函数的情况下。如果你不小心使用了更常见的导入方式 (如
import _ from 'lodash'),并且项目的 Tree Shaking 优化未能如期生效,你可能仅仅为了这一个深拷贝功能就引入了高达 25KB甚至更多的代码 😱。虽然这点体积对某些大型应用而言可能不算世界末日,但在许多场景下,尤其是在浏览器已经内置了
structuredClone 的今天,这样的额外依赖和体积开销就显得没有必要了。structuredClone 的局限性 (短板)
尽管
structuredClone 非常强大,但它也并非万能钥匙,存在一些它无法处理或处理方式不符合某些特定预期的场景:1. 无法克隆函数
尝试克隆包含函数的对象会抛出
DataCloneError 异常:2. 无法克隆 DOM 节点
同样,尝试克隆 DOM 节点也会抛出
DataCloneError 异常,“梅开二度”:3. 属性描述符、Setters 和 Getters
对象的属性描述符(如
writable, configurable, enumerable)以及 getter 和 setter 函数本身不会被克隆。structuredClone 只会复制属性的最终值。举个栗子,对于一个带有
getter 的属性:4. 对象原型链
structuredClone 不会遍历或复制对象的原型链。如果你克隆一个自定义类的实例,克隆出来的对象将不再是该类的实例 (即 instanceof MyClass 会返回 false),但该实例的所有可克隆属性都会被正确复制。5. 不支持的类型
某些特定的内置类型或对象,如果不在其支持列表中,也无法被克隆。
structuredClone 支持的类型完整列表
简单来说,任何未在下述列表中明确列出的类型,
structuredClone 都无法保证能够克隆,或者克隆行为可能不符合预期。JavaScript 内置类型
Array
ArrayBuffer
Boolean(对象和原始值)
DataView
Date
Error类型 (具体见下)
Map
Object(仅限于普通对象,例如通过对象字面量{}创建的对象)
- 原始类型 (除了
symbol):number,string,null,undefined,boolean,BigInt
RegExp(注意:lastIndex字段不会被保留)
Set
TypedArray(所有类型,如Int8Array,Float64Array等)
特定的 Error 类型
structuredClone 可以克隆标准的 Error 对象及其一些子类:Error
EvalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
- (注意:
AggregateError等较新的错误类型可能支持情况不一,需查阅具体环境文档)
Web/API 类型 (浏览器环境)
这些类型通常在浏览器环境的 Web API 中定义:
AudioData
Blob
CryptoKey
DOMException
DOMMatrix,DOMMatrixReadOnly
DOMPoint,DOMPointReadOnly
DOMQuad
DOMRect,DOMRectReadOnly
File,FileList
FileSystemDirectoryHandle,FileSystemFileHandle,FileSystemHandle(File System Access API)
ImageBitmap
ImageData
RTCCertificate(WebRTC)
VideoFrame
浏览器和运行时支持
这大概是最好的部分了——
structuredClone 的支持情况相当广泛!所有主流现代浏览器(Chrome, Firefox, Safari, Edge)都已经支持它,甚至包括服务器端运行时 Node.js (v17.0.0+) 和 Deno (v1.14+)。(请注意,上图可能来自原文,具体支持细节请查阅 MDN Web Docs for structuredClone 或 caniuse.com 获取最新信息)
对于 Web Workers,其支持可能存在一些更早的限制或差异,但现代浏览器中的支持也已趋于完善。
结论:
structuredClone 为 JavaScript 开发者提供了一个强大、原生且高效的深拷贝解决方案。虽然它有其局限性(如不能克隆函数和DOM节点),但在其支持的类型范围内,它无疑是现代Web开发中进行对象深拷贝的首选方法,可以帮助我们告别繁琐的手写实现和不必要的库依赖。上一篇
JavaScript 事件侦听器移除指南:从removeEventListener到AbortController的多重选择
下一篇
Vue Router 4 params传参失效?详解 Extraneous non-declared params 警告及解决方案
- 作者:90_blog
- 链接:https://blog.tri7e.com/article/js_structuredClone
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
