前端手写深拷贝/深克隆是一道回头率超高的笔试题,但笔试版一般不适用于生产环境,JSON 的奇技淫巧和 Lodash 的工具函数也各有缺点。
您知道吗,JS 现在有一种原生方法可以深层复制对象?
structuredClone
?函数内置在 JS 运行时中:
const?calendarEvent?=?{
??title:?'攻城狮',
??date:?new?Date(111),
??attendees:?['Steve']
}
const?copied?=?structuredClone(calendarEvent)
您是否注意到,上述示例中我们不仅复制了对象,还复制了嵌套数组,甚至是?Date
?对象?
一切都如期工作:
copied.attendees?//?["Steve"]
copied.date?//?Date:?Wed?Dec?31?1969?16:00:00
cocalendarEvent.attendees?===?copied.attendees?//?false
structuredClone
?不仅可以如上操作,还可以:
克隆无限嵌套的对象和数组
克隆循环引用
克隆各种 JavaScript 类型,比如 Date 、 Set 、 Map 、 Error 、 RegExp 、 ArrayBuffer 、 Blob 、 File 、 ImageData 等等
传送任何可转移对象(transferable objects)
举个栗子,这种奇葩操作甚至也会如期工作:
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:?'攻城狮'
}
//???问题不大,此处没有嵌套对象/数组
const?shallowCopy?=?{?...calendarEvent?}
或者其他备胎,只要您愿意:
const?shallowCopy?=?Object.assign({},?simpleEvent)
const?shallowCopy?=?Object.create(simpleEvent)
虽然但是,一旦我们嵌套了元素,我们就会遭遇“滑铁卢”:
const?calendarEvent?=?{
??title:?'攻城狮',
??date:?new?Date(123),
??attendees:?['Steve']
}
const?shallowCopy?=?{?...calendarEvent?}
//?🚩?夭寿啦:我们同时在 calendarEvent 及其副本中添加了 Bob
shallowCopy.attendees.push('Bob')
//?🚩?天呢噜:我们同时为 calendarEvent 及其副本更新了 date
shallowCopy.date.setTime(456)
如你所见,我们没有完整拷贝该对象。
嵌套日期和数组仍然是两者之间的共享引用,如果我们想编辑那些被认为只会更新?calendarEvent
?对象副本的内容,这可能会给我们带来无妄之灾。
它实际上是一个很棒的点子,且具有惊人的性能,但存在若干?structuredClone
?解决了的短板。
如下所示:
const?calendarEvent?=?{
??title:?'攻城狮',
??date:?new?Date(123),
??attendees:?['Steve']
}
//??JSON.stringify?会把?date?转换为字符串
const?problematicCopy?=?JSON.parse(JSON.stringify(calendarEvent))
如果我们打印?problematicCopy
,我们会看到:
{
??title:?"攻城狮",
??date:?"1970-01-01T00:00:00.123Z"
??attendees:?["Steve"]
}
这不是我们想要的!date
?应该是?Date
?对象,而不是字符串。
发生这种情况是因为?JSON.stringify
?只能处理基本对象、数组和原始值。处理任何其他类型都十分佛系。举个栗子,Date
?被转换为字符串。但?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
?肝一大坨事情(也就是上述我们未能做到的事情),而此方法却做不到。
3. 为什么不选择_.cloneDeep呢?
迄今为止,Lodash 的?cloneDeep
?函数已经是解决此问题的一个十分常见的解决方案。
事实上,这确实能如期工作:
import?cloneDeep?from?'lodash/cloneDeep'
const?calendarEvent?=?{
??title:?'攻城狮',
??date:?new?Date(123),
??attendees:?['Steve']
}
//???一切顺利!
const?clonedEvent?=?structuredClone(calendarEvent)
虽然但是,此时有且仅有一个警告。根据本人 IDE 中的导入成本(import cost)扩展,它会打印我导入的任何内容的 kb 成本,该函数压缩后总共有 17.4kb(gzip 压缩后为 5.3kb):
而这是假设您只导入了该函数的情况。如果您以更常见的方式导入,却没有意识到 Tree Shaking 优化并不总是如期奏效,您可能会一不小心仅针对这一函数导入多达 25kb 的数据 😱
虽然这对任何人而言都不会是世界末日,但在我们的例子中根本没有必要,尤其是浏览器已经内置了?structuredClone
。
这会报错 ——?DataCloneError
?异常:
//?🚩?报错!
structuredClone({?fn:?()?=>?{}?})
梅开二度 ——?DataCloneError
?异常:
//?🚩?报错!
structuredClone({?el:?document.body?})
类似的类元数据(metadata-like)的功能也无法被克隆。
举个栗子,使用?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
简而言之,下述列表中未列出的任何内容都无法克隆:
Array
、ArrayBuffer
、Boolean
、DataView
、Date
、Error
?类型(那些下面具体列出),Map
,仅限于普通对象的?Object
(比如来自对象字面量),除了?symbol
?的原始类型(又名?number
、string
、null
、undefined
、boolean
、BigInt
)、RegExp
、Set
、TypedArray
Error
EvalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
AudioData
Blob
CryptoKey
DOMException
DOMMatrix
DOMMatrixReadOnly
DOMPoint
DomQuad
DomRect
File
FileList
FileSystemDirectoryHandle
FileSystemFileHandle
FileSystemHandle
ImageBitmap
ImageData
RTCCertificate
VideoFrame
这是最好的部分 —— 所有主流浏览器都支持?structuredClone
,甚至包括 Node.js 和 Deno。
?
添加好友备注【进阶学习】拉你进技术交流群