JS 现代化的深克隆
前端手写深拷贝/深克隆是一道回头率超高的笔试题,但笔试版一般不适用于生产环境,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)
1. 为什么不选择展示对象克隆呢?
注意,我们正在谈论的是深拷贝。如果您只需浅拷贝,即不复制嵌套对象或数组的副本,那么我们可以直接展开对象克隆:
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
?对象副本的内容,这可能会给我们带来无妄之灾。
2. 为什么不选择JSON.parse(JSON.stringify(i))呢?
它实际上是一个很棒的点子,且具有惊人的性能,但存在若干?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
。
4.?structuredClone的短板
无法克隆函数
这会报错 ——?DataCloneError
?异常:
//?🚩?报错!
structuredClone({?fn:?()?=>?{}?})
DOM节点
梅开二度 ——?DataCloneError
?异常:
//?🚩?报错!
structuredClone({?el:?document.body?})
5. 属性描述符,setters和getters
类似的类元数据(metadata-like)的功能也无法被克隆。
举个栗子,使用?getter
?时,会克隆结果值,但不会克隆?getter
?函数本身(或任何其他属性元数据):
structuredClone({
??get?foo()?{
????return?'bar'
??}
})
//?结果变成:?{ foo:?'bar'?}
6. 对象原型
原型链不会被遍历或重复。因此,如果您克隆?MyClass
?的实例,那么克隆对象将不再被视为此类的实例(但此类的所有有效属性都将被克隆)
class?MyClass?{
??foo?=?'bar'
??myMethod()?{
????/*?...?*/
??}
}
const?myClass?=?new?MyClass()
const?cloned?=?structuredClone(myClass)
//?结果变成:?{ foo:?'bar'?}
cloned?instanceof?myClass?//?false
7.?支持的类型的完整列表
简而言之,下述列表中未列出的任何内容都无法克隆:
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。
?
添加好友备注【进阶学习】拉你进技术交流群
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!