本文总结了一些解决深拷贝的常用方法,没有银弹,每种方法都有其优劣,使用时要区分其场景。
在 JavaScript 中,深拷贝已经是一个老生常谈的问题,也无数次被面试官用来考核一个前端的 JS 水平,同时,在开发中,如果对深拷贝理解不够深刻,也会出现很难发现的 BUG。此前,我已经写过一篇文章《深入理解 JavaScript 之克隆》,其中分析了深拷贝的概念,但是其解决方法还很粗浅,这篇文章,记录了一些解决深拷贝的常用方法,没有银弹,每种方法都有其优劣,使用时要区分其场景。
递归拷贝
递归拷贝应该是最开始了解深拷贝时遇到的解决办法,其原理是对引用类型进行遍历,判断其值是否为引用类型,如果为引用类型则递归调用该函数并传入该值,否则为简单数据类型则执行常规赋值拷贝。
function deepClone(obj) { |
这是一个简单的深拷贝方法,处理了对象、数组和函数,但是 Date、Regexp、Map 等引用类型都没有处理,而且无法解决对象循环引用的场景。
JSON.parse(JSON.stringify(object))
深拷贝一般用 JSON.parse(JSON.stringify(object))就可以解决了,
这种方法的局限性:
- 会忽略 undefined
- 不能序列化函数
- 不能解决循环引用的对象
undefined 和函数会被忽略,而尝试拷贝循环引用的对象则会报错:
Uncaught TypeError:Converting circular structure to JSON |
另外,诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失。
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。
Structured Clone 结构化克隆算法
Structured cloning 是一种现有的算法,用于将值从一个地方转移到另一地方。例如,每当您调用 postMessage 将消息发送到另一个窗口或 WebWorker 时,都会使用它。关于结构化克隆的好处在于它处理循环对象并 支持大量的内置类型。结构化克隆包含使用 MessageChannel 和 History API。
MessageChannel
MessageChannel API 允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。
我们可以创建一个 MessageChannel 并发送消息。在接收端,消息包含我们原始数据对象的结构化克隆。
MessageChannel 的 postMessage 传递的数据也是深拷贝的,这和 web worker 的 postMessage 一样。而且还可以拷贝 undefined 和循环引用的对象。
// 有undefined + 循环引用 |
但拷贝有函数的对象时,还是会报错:
Uncaught (in promise) DOMException:Failed to execute 'postMessage' on 'MessagePort': function() {} could not be cloned. |
这种方法的缺点是它是异步的,但是你可以使用 await 来解决。
const clone = await structuralClone(obj); |
History
如果你曾经使用 history.pushState()写过 SPA,你可以提供一个状态对象来保存 URL。事实证明,这个状态对象使用结构化克隆,而且是同步的。我们必须小心使用,不要把原有的路由状态搞乱了,所以我们需要在完成克隆之后恢复原始状态。为了防止发生任何意外,请使用 history.replaceState()而不是 history.pushState()。
const structuralClone = obj => { |
此方法的优点是:能解决循环对象的问题,并且支持许多内置类型的克隆,也是同步的。缺点是有的浏览器对调用频率有限制。比如 Safari 30 秒内只允许调用 100 次
Notification
Notification 是用于桌面通知的。也可以用来实现 JS 对象的深拷贝。
function structuralClone(obj) { |
此方法的优点就是可以解决循环对象问题,也支持许多内置类型的克隆,并且是同步的。缺点就是这个 api 的使用需要向用户请求权限,但是用在这里克隆数据的时候,不经用户授权也可以使用。在 http 协议的情况下会提示你再 https 的场景下使用。
由于某种原因,Safari 总是返回 undefined。
lodash 的深拷贝函数
lodash 的_.cloneDeep()支持循环对象,和大量的内置类型,对很多细节都处理的比较不错,推荐使用。
解决循环引用
通过分析上述的方法,发现只有结构化克隆算法才能解决循环引用的问题,那么使用原生 JS 如何解决呢?这里尝试使用闭包来解决循环问题。
function deepClone(object) { |
在上述代码中,对递归拷贝进行了优化,使用闭包来解决循环拷贝的问题,使用了 memo 这个变量来记录被拷贝的引用地址,在每次递归前,记录递归的参数值,这样在递归内部就可以跳出递归,解决循环引用问题。
总结
- 如果您没有循环对象,并且不需要保留内置类型,则可以使用跨浏览器的 JSON.parse(JSON.stringify())获得最快的克隆性能。
- 如果你想要一个适当的结构化克隆,MessageChannel 是你唯一可靠的跨浏览器的选择。
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章