如何实现深度拷贝,前面的文章:javascript中的深拷贝和浅拷贝区分以及实现 给出了几种,但都是很复杂的,这篇文章通过另辟蹊径的用浏览器自身的API来实现深度拷贝,有MessageChannel、history api 、Notification api等。
如何在JavaScript中复制对象? 这是一个简单的问题,没有一个简单的答案。
通过参考调用
JavaScript通过引用传递所有内容。 如果你不知道这意味着什么,这里有一个例子:
function mutate(obj) { obj.a = true; } const obj = {a: false}; mutate(obj) console.log(obj.a); // prints true
函数mutate改变它作为参数传递的对象。 在“按值调用”的环境中,函数会传递该函数可以使用的值 – 所以是副本。 该函数对该对象所做的任何更改都不会在该函数外部可见。但是在像JavaScript这样的“引用调用”环境中,函数会得到 – 你猜对它 – 引用 ,并且会改变实际的对象本身。 因此,最后的console.log将显示为true 。
然而,有时候,您可能希望保留原始对象并为其他函数创建副本以便使用。
浅拷贝:Object.assign()
复制对象的一种方法是使用Object.assign(target, sources…) 。 它需要任意数量的源对象,枚举它们自己的所有属性并将它们分配给target 。 如果我们使用一个新鲜的空物体作为target ,我们基本上就是复制。
const obj = /* ... */; const copy = Object.assign({}, obj);
但是,这仅仅是一个浅拷贝。 如果我们的对象包含对象,它们将保持共享引用,这不是我们想要的:
function mutateDeepObject(obj) { obj.a.thing = true; } const obj = {a: {thing: false}}; const copy = Object.assign({}, obj); mutateDeepObject(copy) console.log(obj.a.thing); // prints true
另一件可能会跳过的事是Object.assign()将getter变成简单的属性。
所以现在怎么办? 原来,有几种方法可以创建对象的深层副本。
注意:有些人询问了对象扩散算子。 对象传播也会创建一个浅拷贝。
JSON.parse
创建对象副本的最古老方法之一是将对象转换为其JSON字符串表示形式,然后将其解析回对象。 这感觉有点霸道,但它确实有效:
const obj = /* ... */; const copy = JSON.parse(JSON.stringify(obj));
这里的缺点是您创建了一个临时的,可能很大的字符串,以便将其返回到解析器。 另一个缺点是这种方法无法处理循环对象。 尽管你可能会想,但这些可以很容易地发生。例如,当您构建树状数据结构时,节点引用其父项,并且父项又引用其自己的子项。
const x = {}; const y = {x}; x.y = y; // Cycle: x.y.x.y.x.y.x.y.x... const copy = JSON.parse(JSON.stringify(x)); // throws!
此外,诸如地图,集合,RegExps,日期,ArrayBuffers和其他内置类型的东西在序列化时会丢失。
结构化克隆
结构化克隆是一种现有的算法,用于将值从一个领域转移到另一个领域。 例如,只要调用postMessage将消息发送到其他窗口或WebWorker,就会使用它 。 关于结构化克隆的好处在于它处理循环对象并支持大量的内置类型 。 问题在于,在编写本文时算法不会直接暴露,只能作为其他API的一部分。 我想我们必须看看那些,我们不会…
MessageChannel
正如我所说的,无论何时调用postMessage ,都会使用结构化克隆算法。 我们可以创建一个MessageChannel并发送消息。 在接收端,消息包含我们原始数据对象的结构化克隆。
function structuralClone(obj) { return new Promise(resolve => { const {port1, port2} = new MessageChannel(); port2.onmessage = ev => resolve(ev.data); port1.postMessage(obj); }); } const obj = /* ... */; const clone = await structuralClone(obj);
这种方法的缺点是它是异步的。 这并不是什么大问题,但有时您需要一种同步方式来深度复制对象。
历史API
如果您曾经使用history.pushState()构建SPA,那么您知道可以提供一个状态对象来保存URL。 事实证明,这个状态对象在结构上被克隆 – 同步。 我们必须小心,不要混淆可能使用状态对象的任何程序逻辑,所以我们需要在完成克隆后恢复原始状态。 为了防止发生任何事件,请使用history.replaceState()而不是history.pushState() 。
function structuralClone(obj) { const oldState = history.state; history.replaceState(obj, document.title); const copy = history.state; history.replaceState(oldState, document.title); return copy; } const obj = /* ... */; const clone = structuralClone(obj);
再一次,为了复制一个对象而使用浏览器的引擎感觉有点过分,但是你必须做一些事情。 此外,Safari会在30秒内将调用replaceState限制为100次。
通知API
在 Twitter 上对整个旅程进行了推特风暴之后, Jeremy Banks向我展示了第三种方法可以利用结构化克隆:Notification API。 通知有一个与它们相关的数据对象被克隆。
function structuralClone(obj) { return new Notification('', {data: obj, silent: true}).data; } const obj = /* ... */; const clone = structuralClone(obj);
简洁,简洁。 我喜欢它! 但是,它基本上是在浏览器中的权限机制的踢,所以我怀疑它很慢。 出于某种原因,Safari总是返回undefined的数据对象。
大量演示
我想测量哪些方法是最高性能的。 在我的第一次(天真的)尝试中,我拿了一个小JSON对象并通过这些不同的克隆对象一千次的方法来传送它。 幸运的是, Mathias Bynens告诉我,当你添加属性到一个对象时, V8具有高速缓存 。 我比其他任何东西都更像基准测试缓存。 为了确保我永远不会碰到缓存,我写了一个函数,使用随机密钥名称生成给定深度和宽度的对象,并重新运行测试 。
图表!
以下是不同技术在Chrome,Firefox和Edge中的执行情况。 越低越好。
结论
那么我们从这里拿走了什么?
- 如果您不希望循环对象并且不需要保留内置类型,那么通过使用JSON.parse(JSON.stringify())在所有浏览器中获得最快的克隆,这让我感到非常意外。
- 如果你想要一个适当的结构化克隆, MessageChannel是你唯一可靠的跨浏览器选择。
如果我们只是把structuredClone()作为平台的一个功能,会不会更好? 我当然这样认为,并重新对HTML规范的旧问题重新考虑这种方法。