如何在 JavaScript 中拷贝一个对象?对于这个很简单的问题,可是答案却不简单。html
引用传值 若是你不知道什么意思,看看下面的例子:前端
function mutate(obj) {
obj.a = true;
}
const obj = {a: false};
mutate(obj)
console.log(obj.a); // 输出 true
复制代码
函数 mutate 改变了它的参数。在值传递的场景中,函数的形参只是实参的一个副本——a copy——当函数调用完成后,并不改变实参。可是在 JavaScript 这种引用传递的场景中,函数的形参和实参指向同一个对象,当参数内部改变形参的时候,函数外面的实参也被改变了。web
所以在某些状况下,你须要保留原始对象,这时你须要把原始对象的一个拷贝传入到函数中,以防止函数改变原始对象。算法
浅拷贝: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 方法 只会拷贝源对象自身的而且可枚举的属性到目标对象。该方法使用源对象的 [[Get]]和目标对象的 [[Set]],因此它会调用相关 getter 和 setter。所以,它分配属性,而不只仅是复制或定义新的属性。若是合并源包含 getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用 Object.getOwnPropertyDescriptor()和 Object.defineProperty() 。缓存
因此如今怎么办?有几种方法能够建立一个对象的深拷贝。数据结构
注意:也许有人提到了对象解构运算,这也是浅拷贝。并发
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!
复制代码
另外,诸如 Map, Set, RegExp, Date, ArrayBuffer 和其余内置类型在进行序列化时会丢失。
Structured Clone 结构化克隆算法
Structured cloning 是一种现有的算法,用于将值从一个地方转移到另外一地方。例如,每当您调用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);
复制代码
这种方法的缺点是它是异步的。虽然这并没有大碍,可是有时候你须要使用同步的方式来深度拷贝一个对象。
History 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 浏览器对replaceState调用的限制数量为 30 秒内 100 次。
Notification API 第三种方法来利用结构化克隆:Notification API。
function structuralClone(obj) {
return new Notification('', {data: obj, silent: true}).data;
}
const obj = /* ... */;
const clone = structuralClone(obj);
复制代码
短小,简洁。我喜欢它!
可是,它须要浏览器内部的权限机制,因此我怀疑它是很慢的。因为某种缘由,Safari 老是返回undefined。
Performance extravaganza 我想测量哪一种方法是最高性能的。在个人第一次(天真的)尝试中,我拿了一个小 JSON 对象,并经过不一样的方式克隆对象 1 千次。幸运的是,Mathias Bynens 告诉我,当你添加属性到一个对象时,V8有一个缓存。因此我是在给缓存作基准测试。为了确保我永远不会碰到缓存,我编写了一个函数,使用随机密钥名称生成给定深度和宽度的对象点击预览,并从新运行测试。
图表! 如下是 Chrome,Firefox 和 Edge 中不一样技术的性能。越低越好。
那么咱们从中获得了什么呢?
若是您没有循环对象,而且不须要保留内置类型,则可使用跨浏览器的JSON.parse(JSON.stringify())得到最快的克隆性能,这让我感到很是惊讶。 若是你想要一个适当的结构化克隆,MessageChannel是你惟一可靠的跨浏览器的选择。
这里推荐一下个人前端学习交流群:784783012,里面都是学习前端的,若是你想制做酷炫的网页,想学习编程。本身整理了一份2018最全面前端学习资料,从最基础的HTML+CSS+JS【炫酷特效,游戏,插件封装,设计模式】到移动端HTML5的项目实战的学习资料都有整理,送给每一位前端小伙伴,有想学习web前端的,或是转行,或是大学生,还有工做中想提高本身能力的,正在学习的小伙伴欢迎加入学习。