JS 现代化的深克隆

发布时间:2023年12月25日

前端手写深拷贝/深克隆是一道回头率超高的笔试题,但笔试版一般不适用于生产环境,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内置函数

ArrayArrayBufferBooleanDataViewDateError?类型(那些下面具体列出),Map,仅限于普通对象的?Object(比如来自对象字面量),除了?symbol?的原始类型(又名?numberstringnullundefinedbooleanBigInt)、RegExpSetTypedArray

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。

?

添加好友备注【进阶学习】拉你进技术交流群

文章来源:https://blog.csdn.net/weixin_42333548/article/details/135197481
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。