大家好,这里是大家的林语冰。
前端手写深克隆/深拷贝是一道回头率超高的笔试题,虽然但是:
- 笔试版深克隆一般不适用于生产环境(因为大部分前端八股文的网红应试都没考虑 Corner Cases)
JSON
的奇技淫巧并不完美- Lodash 的工具函数体积臃肿
所以,本期《前端翻译计划》给大家推荐的是一种运行时原生支持的深克隆方法 ------ structuredClone
(结构化克隆算法)。

免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Deep Cloning Objects in JavaScript, the Modern Way。
您知道吗,JS 现在有一种原生方法可以深层复制对象?
是的没错,该 structuredClone
函数内置在 JS 运行时中:
js
const calendarEvent = {
title: 'Builder.io Conf',
date: new Date(123),
attendees: ['Steve']
}
// 😍
const copied = structuredClone(calendarEvent)
您是否注意到,上述示例中我们不仅复制了对象,还复制了嵌套数组,甚至是 Date
对象?
一切都如期工作:
js
copied.attendees // ["Steve"]
copied.date // Date: Wed Dec 31 1969 16:00:00
cocalendarEvent.attendees === copied.attendees // false
是的没错,structuredClone
不仅可以如上操作,还可以:
- 克隆无限嵌套的对象和数组
- 克隆循环引用
- 克隆各种 JS 类型,比如
Date
、Set
、Map
、Error
、RegExp
、ArrayBuffer
、Blob
、File
、ImageData
等等 - 传送任何可转移对象(transferable objects)
举个栗子,下述奇葩操作甚至也会如期工作:
js
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)
为什么不选择展开对象克隆呢?
注意,我们正在谈论的是深拷贝。如果您只需浅拷贝,即不复制嵌套对象或数组的副本,那么我们可以直接展开对象克隆:
js
const simpleEvent = {
title: 'Builder.io Conf'
}
// ✅ 问题不大,此处没有嵌套对象/数组
const shallowCopy = { ...calendarEvent }
或者其他备胎,只要您愿意:
js
const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)
虽然但是,一旦我们嵌套了元素,我们就会遭遇"滑铁卢":
js
const calendarEvent = {
title: 'Builder.io Conf',
date: new Date(123),
attendees: ['Steve']
}
const shallowCopy = { ...calendarEvent }
// 🚩 夭寿啦:我们同时在 calendarEvent 及其副本中添加了 Bob
shallowCopy.attendees.push('Bob')
// 🚩 天呢噜:我们同时为 calendarEvent 及其副本更新了 date
shallowCopy.date.setTime(456)
如你所见,我们没有完整拷贝该对象。
嵌套日期和数组仍然是两者之间的共享引用,如果我们想编辑那些被认为只会更新 calendarEvent
对象副本的内容,这可能会给我们带来无妄之灾。
为什么不选择 JSON.parse(JSON.stringify(x))
呢?
啊对对对,这确实是奇技淫巧。它实际上是一个很棒的点子,且具有惊人的性能,但存在若干 structuredClone
解决了的短板。
如下所示:
js
const calendarEvent = {
title: 'Builder.io Conf',
date: new Date(123),
attendees: ['Steve']
}
// 🚩 JSON.stringify 会把 date 转换为字符串
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))
如果我们打印 problematicCopy
,我们会看到:
js
{
title: "Builder.io Conf",
date: "1970-01-01T00:00:00.123Z"
attendees: ["Steve"]
}
这不是我们想要的!date
应该是 Date
对象,而不是字符串。
发生这种情况是因为 JSON.stringify
只能处理基本对象、数组和原始值。处理任何其他类型都十分佛系。举个栗子,Date
被转换为字符串。但 Set
则转换为 {}
。
JSON.stringify
甚至完全无视某些内容,比如 undefined
或函数。
举个栗子,如果我们使用此方法复制 kitchenSink
:
js
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))
结果如下:
js
{
"set": {},
"map": {},
"regex": {},
"deep": {
"array": [
{}
]
},
"error": {},
}
我的天姥爷!哇哦,是的没错,我们必须删除最初为此使用的循环引用,因为如果 JSON.stringify
遭遇其中之一,就能且仅能报错。
因此,虽然如果我们的需求刚好符合其功能,这个方法自然棒棒哒,但我们可以用 structuredClone
肝一大坨事情(也就是上述我们未能做到的事情),而此方法却做不到。
为什么不选择 _.cloneDeep
呢?
迄今为止,Lodash 的 cloneDeep
函数已经是解决此问题的一个十分常见的技术方案。
事实上,这确实能如期工作:
js
import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: 'Builder.io Conf',
date: new Date(123),
attendees: ['Steve']
}
// ✅ 一切顺利!
const clonedEvent = structuredClone(calendarEvent)
虽然但是,此处有且仅有一个警告。根据本人 IDE 中的导入成本(import cost)扩展插件,它会打印我导入的任何内容的 kb 成本,该函数压缩后总共有 17.4kb(gzip 压缩后为 5.3kb):

而这是假设您只导入了该函数的情况。如果您以更常见的方式导入,却没有意识到 Tree Shaking 优化并不总是如期奏效,您可能会一不小心仅针对这一函数导入多达 25kb 的数据 😱

虽然这对任何人而言都不会是世界末日,但在我们的例子中根本没有必要,尤其是浏览器已经内置了 structuredClone
。
structuredClone
的短板
无法克隆函数
这会报错 ------ DataCloneError
异常:
js
// 🚩 报错!
structuredClone({ fn: () => {} })
DOM 节点
梅开二度 ------ DataCloneError
异常:
js
// 🚩 报错!
structuredClone({ el: document.body })
属性描述符,setters 和 getters
类似的类元数据(metadata-like)的功能也无法被克隆。
举个栗子,使用 getter
时,会克隆结果值,但不会克隆 getter
函数本身(或任何其他属性元数据):
js
structuredClone({
get foo() {
return 'bar'
}
})
// 结果变成: { foo: 'bar' }
对象原型
原型链不会被遍历或重复。因此,如果您克隆 MyClass
的实例,那么克隆对象将不再被视为此类的实例(但此类的所有有效属性都将被克隆)
js
class MyClass {
foo = 'bar'
myMethod() {
/* ... */
}
}
const myClass = new MyClass()
const cloned = structuredClone(myClass)
// 结果变成: { foo: 'bar' }
cloned instanceof myClass // false
支持的类型的完整列表
简而言之,下述列表中未列出的任何内容都无法克隆:
JS 内置函数
Array
、ArrayBuffer
、Boolean
、DataView
、Date
、Error
类型(那些下面具体列出),Map
,仅限于普通对象的 Object
(比如来自对象字面量),除了 symbol
的原始类型(又名 number
、string
、null
、undefined
、boolean
、BigInt
)、RegExp
、Set
、TypedArray
Error
类型
Error
EvalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
Web/API 类型
AudioData
Blob
CryptoKey
DOMException
DOMMatrix
DOMMatrixReadOnly
DOMPoint
DomQuad
DomRect
File
FileList
FileSystemDirectoryHandle
FileSystemFileHandle
FileSystemHandle
ImageBitmap
ImageData
RTCCertificate
VideoFrame
浏览器和运行时支持
这是最好的部分 ------ 所有主流浏览器都支持 structuredClone
,甚至包括 Node.js 和 Deno。
请注意 Web Workers 的支持更有限的警告:

您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~
