原文:Deep-copyinginJavaScript-DasSur.ma如何在JavaScript中拷贝一个对象?对于这个很简单的问题,答案却并不简单。JavaScript中的所有内容都是通过引用传递的。如果你不知道它是什么意思,看看下面的例子:functionmutate(obj){obj.a=true;}constobj={a:false};mutate(obj)console.log(obj.A);//输出true函数mutate改变了它的参数。在传值场景下,函数的形参只是实参的一个拷贝——拷贝——并不会在函数调用完成后改变实参。但是在JavaScript的引用传递场景中,函数的形参和实参指向同一个对象。当形参在参数内部改变时,函数外的实参也随之改变。所以在某些情况下,你需要保留原始对象,然后你需要将原始对象的副本传递给函数,以防止函数更改原始对象。浅拷贝:Object.assign()获取对象副本的一种简单方法是使用Object.assign(target,sources...)。它接受任意数量的源对象,枚举它们的所有属性并分配给目标。如果我们使用一个新的空对象目标,那么我们就可以实现对象复制。constobj=/*...*/;constcopy=Object.assign({},obj);然而,这只是一个浅拷贝。如果我们的对象包含其他对象作为它自己的属性,它们将保留共享引用,这不是我们想要的:functionmutateDeepObject(obj){obj.a.thing=true;}constobj={a:{thing:false}};constcopy=Object.assign({},obj);mutateDeepObject(copy)console.log(obj.a.thing);//打印trueObject.assign方法只会将源对象本身和可枚举属性复制到目标对象。该方法使用了源对象的[[Get]]和目标对象的[[Set]],所以调用了相关的getter和setter。因此,它分配属性,而不仅仅是复制或定义新属性。如果合并的源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括它们的可枚举性)复制到原型,应该使用Object.getOwnPropertyDescriptor()和Object.defineProperty()。所以现在怎么办?有几种方法可以创建对象的深拷贝。注意:可能有人提到了对象解构操作,也就是浅拷贝。JSON.parse创建对象副本的最古老方法之一是将该对象转换为其JSON字符串表示,然后将其解析回对象。这感觉有点压抑,但它确实有效:constobj=/*...*/;constcopy=JSON.parse(JSON.stringify(obj));这里的缺点是您创建了一个临时的、可能很大的字符串,只是为了将它放回解析器中。另一个缺点是这种方法不能处理循环对象。循环对象经常发生。例如,当您构建一个树状数据结构时,其中一个节点引用其父节点,而父节点又引用其子节点。constx={};consty={x};x.y=y;//循环:x.y.x.y.x.y.x.y.x...constcopy=JSON.parse(JSON.stringify(x));//抛出!此外,诸如Map、Set、RegExp、Date、ArrayBuffer等内置类型在序列化时都会丢失。结构化克隆结构化克隆是一种将价值从一个地方转移到另一个地方的现有算法。例如,每当您调用postMessage将消息发送到另一个窗口或WebWorker时,都会使用它。结构化克隆的好处在于它可以处理循环对象并支持大量内置类型。问题在于,在撰写本文时,该算法不能直接使用,只能作为其他API的一部分使用。我想我们应该看看其中包含的内容,不是吗?..只要你调用postMessage结构化克隆算法,就像我说的MessageChannel是可用的。我们可以创建一个MessageChannel并发送消息。在接收端,消息包含我们原始数据对象的结构化克隆。functionstructuralClone(obj){returnnewPromise(resolve=>{const{port1,port2}=newMessageChannel();port2.onmessage=ev=>resolve(ev.data);port1.postMessage(obj);});}常量对象=/*...*/;constclone=awaitstructuralClone(obj);这种方法的缺点是它是异步的。虽然这很好,但有时您需要以同步方式深拷贝一个对象。HistoryAPI如果您曾经使用history.pushState()编写过SPA,您就会知道您可以提供一个状态对象来保存URL。事实证明,这个状态对象使用结构化克隆——而且它是同步的。我们必须注意不要弄乱程序逻辑使用的状态对象,所以我们需要在克隆完成后恢复原始状态。为防止出现任何意外,请使用history.replaceState()而不是history.pushState()。functionstructuralClone(obj){constoldState=history.state;history.replaceState(obj,document.title);constcopy=history.state;history.replaceState(oldState,document.title);返回副本;}constobj=/*...*/;constclone=structuralClone(obj);然而,仅仅使用浏览器的引擎来克隆一个对象感觉有点矫枉过正。此外,Safari浏览器限制在30秒内对replaceState调用100次。NotificationAPI在一条推文之后,JeremyBanks向我展示了利用结构化克隆的第三种方法:NotificationAPI。functionstructuralClone(obj){returnnewNotification('',{data:obj,silent:true}).data;}constobj=/*...*/;constclone=structuralClone(obj);简明扼要。我喜欢!但是,它需要浏览器的内部权限机制,所以我怀疑它很慢。由于某种原因,Safari总是返回undefined。性能盛会我想衡量哪种方法性能最高。在我的第一次(幼稚)尝试中,我使用了一个小的JSON对象,并通过不同的方式将该对象克隆了1000次。幸运的是,MathiasBynens告诉我,当您向对象添加属性时,V8有一个缓存。所以我正在对缓存进行基准测试。为了确保我永远不会命中缓存,我编写了一个函数,该函数使用随机键名来生成给定深度和宽度的对象,并重新运行测试。图表!以下是不同技术在Chrome、Firefox和Edge中的表现。越低越好。结论那么我们从中得到了什么?如果您不遍历对象,也不需要保留内置类型,我很惊讶您使用JSON.parse(JSON.stringify())跨浏览器获得最快的克隆性能。如果你想要一个结构合理的克隆,MessageChannel是你唯一可靠的跨浏览器选择。如果浏览器平台直接提供一个structuredClone()函数不是更好吗?我当然这么认为,最新的HTML规范正在谈论这个Synchronousclone=global.structuredClone(value,transfer=[])API·Issue#793·whatwg/html。
