Lazy loaded image
前端
JavaScript 深拷贝的现代方案:原生 structuredClone 完全指南
字数 2977阅读时长 8 分钟
2023-12-28
2025-5-14
type
status
date
slug
summary
tags
category
icon
password
前端面试中,“手写深拷贝/深克隆”可谓是常客中的常客。但传统的面试版深拷贝方案往往不适用于复杂的生产环境,而广为流传的 JSON.parse(JSON.stringify()) 奇技淫巧以及 Lodash 等工具库的 _.cloneDeep 函数也各有其局限性。
本期《前端翻译计划》将为大家隆重介绍一种由 JavaScript 运行时原生支持的现代深克隆方法——structuredClone
notion image
你没听错,JavaScript 现在拥有一个原生的方法来实现对象的深层复制!它就是内置于 JS 运行时的 structuredClone 函数。
注意到没?上面的例子中,我们不仅复制了对象本身,还完美复制了其嵌套的数组以及 Date 对象。所有的一切都如预期般工作:
structuredClone 的强大之处远不止于此,它还能:
  • 克隆无限嵌套的对象和数组。
  • 优雅处理循环引用(对象自我引用或相互引用)。
  • 克隆多种 JavaScript 内置类型,例如 DateSetMapErrorRegExpArrayBufferBlobFileImageData 等等。
  • 传送(transfer)任何可转移对象 (transferable objects)。
举个“栗子”,即便是下面这种包含各种复杂类型的“大杂烩”对象,structuredClone 也能轻松搞定:

为什么不选择展开运算符 (...) 进行对象克隆呢?

首先明确,我们这里讨论的是深拷贝。如果你只需要浅拷贝(即只复制对象的第一层属性,而不复制嵌套对象或数组的副本),那么使用对象展开运算符 (...) 是完全可以的:
或者,你也可以选择其他浅拷贝的备胎方案:
然而,一旦我们的对象包含了嵌套元素,浅拷贝的“滑铁卢”就来了:
如你所见,这根本不是一个完整的副本。嵌套的 Date 对象和 attendees 数组在原对象和浅拷贝副本之间仍然是共享引用。如果我们期望的只是修改副本,这种共享引用无疑会给我们带来“无妄之灾”。

为什么不选择 JSON.parse(JSON.stringify(x)) 呢?

啊对对对,这确实是个流传甚广的“奇技淫巧”。它在处理纯JSON兼容数据时性能表现惊人,思路也简单粗暴。但它存在若干 structuredClone 能够完美解决的硬伤。
看招:
如果我们打印 problematicCopy,会看到:
这显然不是我们想要的!date 属性应该是一个 Date 对象,而不是字符串。undefined 和函数属性也丢失了。
发生这种情况是因为 JSON.stringify 只能正确处理 JavaScript 的基本对象、数组和原始类型(字符串、数字、布尔值、null)。对于其他任何类型,它的处理方式就十分“佛系”了。例如,Date 对象被转换为字符串,SetMap 对象会转换为空对象 {}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):
notion image
这还是在你正确地只导入了单个函数的情况下。如果你不小心使用了更常见的导入方式 (如 import _ from 'lodash'),并且项目的 Tree Shaking 优化未能如期生效,你可能仅仅为了这一个深拷贝功能就引入了高达 25KB甚至更多的代码 😱。
notion image
虽然这点体积对某些大型应用而言可能不算世界末日,但在许多场景下,尤其是在浏览器已经内置了 structuredClone 的今天,这样的额外依赖和体积开销就显得没有必要了。

structuredClone 的局限性 (短板)

尽管 structuredClone 非常强大,但它也并非万能钥匙,存在一些它无法处理或处理方式不符合某些特定预期的场景:

1. 无法克隆函数

尝试克隆包含函数的对象会抛出 DataCloneError 异常:

2. 无法克隆 DOM 节点

同样,尝试克隆 DOM 节点也会抛出 DataCloneError 异常,“梅开二度”:

3. 属性描述符、Setters 和 Getters

对象的属性描述符(如 writable, configurable, enumerable)以及 gettersetter 函数本身不会被克隆。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+)。
notion image
(请注意,上图可能来自原文,具体支持细节请查阅 MDN Web Docs for structuredClonecaniuse.com 获取最新信息)
对于 Web Workers,其支持可能存在一些更早的限制或差异,但现代浏览器中的支持也已趋于完善。
结论: structuredClone 为 JavaScript 开发者提供了一个强大、原生且高效的深拷贝解决方案。虽然它有其局限性(如不能克隆函数和DOM节点),但在其支持的类型范围内,它无疑是现代Web开发中进行对象深拷贝的首选方法,可以帮助我们告别繁琐的手写实现和不必要的库依赖。
上一篇
JavaScript 事件侦听器移除指南:从removeEventListener到AbortController的多重选择
下一篇
Vue Router 4 params传参失效?详解 Extraneous non-declared params 警告及解决方案