JavaScript 深拷贝机能剖析

原文:
Deep-copying in JavaScript – DasSur.ma

如安在 JavaScript 中拷贝一个对象?关于这个很简朴的题目,然则答案却不简朴。

援用传值

在 JavaScript 中一切的东西都是援用通报(原文有误,稍后写篇批评文 “By Value” or “By Reference” in JavaScript · Issue #22)。

假如你不晓得什么意思,看看下面的例子:

function mutate(obj) {
  obj.a = true;
}

const obj = {a: false};
mutate(obj)
console.log(obj.a); // 输出 true

函数 mutate 转变了它的参数。在值通报的场景中,函数的形参只是实参的一个副本——a copy——当函数挪用完成后,并不转变实参。然则在 JavaScript 这类援用通报的场景中,函数的形参和实参指向同一个对象,当参数内部转变形参的时刻,函数表面的实参也被转变了。

因而在某些情况下,你须要保留原始对象,这时候你须要把原始对象的一个拷贝传入到函数中,以防备函数转变原始对象。

浅拷贝: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

在发了一条推文以后,Jeremy Banks 向我展现了第三种要领来应用结构化克隆: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 中差别手艺的机能。越低越好。

《JavaScript 深拷贝机能剖析》

《JavaScript 深拷贝机能剖析》

《JavaScript 深拷贝机能剖析》

结论

那末我们从中得到了什么呢?

  • 假如您没有轮回对象,而且不须要保留内置范例,则能够运用跨浏览器的JSON.parse(JSON.stringify())取得最快的克隆机能,这让我觉得异常惊奇。
  • 假如你想要一个恰当的结构化克隆,MessageChannel是你唯一牢靠的跨浏览器的挑选。

假如浏览器平台直接供应一个 structuredClone()函数,会不会更好?我固然如许以为,最新的 HTML 范例正在议论这个 Synchronous clone = global.structuredClone(value, transfer = []) API · Issue #793 · whatwg/html

    原文作者:justjavac
    原文地址: https://segmentfault.com/a/1190000013107871
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞