ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript ディープコピーのパフォーマンスについての深い理解

JavaScript ディープコピーのパフォーマンスについての深い理解

小云云
小云云オリジナル
2018-02-03 14:28:231987ブラウズ

この記事では主に JavaScript のディープ コピー パフォーマンスの分析について説明します。JavaScript でオブジェクトをコピーする方法。これは非常に単純な質問ですが、答えは簡単ではありません。

その意味がわからない場合は、次の例を見てください:

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

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

関数 mutate は引数を変更します。 値渡しのシナリオでは、関数の仮パラメータは実際のパラメータのコピー (コピー) にすぎず、関数呼び出しの完了後に実際のパラメータは変更されません。しかし、JavaScript の参照渡しのシナリオでは、関数の仮パラメータと実パラメータは同じオブジェクトを指します。パラメータ内で仮パラメータが変更されると、関数の外部の実パラメータも変更されます。かわった。 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]],所以它会调用相关 gettersetter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含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

したがって、場合によっては、元のオブジェクトを保持する必要があります。この場合、関数が元のオブジェクトを変更しないように、元のオブジェクトのコピーを関数に渡す必要があります。 🎜

浅いコピー: Object.assign()

🎜 オブジェクトのコピーを取得する簡単な方法は、Object.assign(target,sources... を使用することです) )コード>。任意の数のソース オブジェクトを受け入れ、そのすべてのプロパティを列挙し、それらを <code>target に割り当てます。新しい空のオブジェクト target を使用すると、そのオブジェクトをコピーできます。 🎜rrreee🎜 ただし、 これは浅いコピーにすぎません。オブジェクトに他のオブジェクトがプロパティとして含まれている場合、それらは共有参照を保持しますが、これは私たちが望んでいることではありません: 🎜rrreee
Object.assign メソッドはソース オブジェクト自体をコピーするだけであり、列挙可能ですターゲット オブジェクトにプロパティを追加します。このメソッドは、ソース オブジェクトの [[Get]] とターゲット オブジェクトの [[Set]] を使用するため、関連する getter と セッターコード>。したがって、単に新しいプロパティをコピーまたは定義するのではなく、プロパティを割り当てます。マージ ソースに getter が含まれている場合、新しいプロパティをプロトタイプにマージするのに適さない可能性があります。プロパティ定義 (その列挙可能性を含む) をプロトタイプにコピーするには、 Object.getOwnPropertyDescriptor()Object.defineProperty() を使用します。
🎜それではどうなるでしょうか?オブジェクトのディープ コピーを作成するには、いくつかの方法があります。 🎜🎜注: おそらく誰かがオブジェクトの構造化操作について言及しましたが、これも浅いコピーです。 🎜

JSON.parse

🎜 オブジェクトのコピーを作成する最も古い方法の 1 つは、オブジェクトを JSON 文字列表現に変換してから、それを解析してオブジェクトに戻します。これは少し圧迫的に感じますが、うまくいきます: 🎜rrreee🎜 ここでの欠点は、パーサーに戻すためだけに一時的な、潜在的に大きな文字列を作成することです。もう 1 つの欠点は、このメソッドでは循環オブジェクトを処理できないことです。また、オブジェクトのループが頻繁に発生します。たとえば、ツリー状のデータ構造を構築する場合、ノードはその親を参照し、さらにその親がその子を参照します。 🎜rrreee🎜さらに、MapSetRegExpDateArrayBuffer などcode> およびその他の組み込み型はシリアル化時に失われます。 🎜<h2>構造化クローン 構造化クローン作成アルゴリズム</h2>🎜 構造化クローンは、値をある場所から別の場所に転送するために使用される既存のアルゴリズムです。たとえば、別のウィンドウまたは WebWorker にメッセージを送信するために postMessage を呼び出すたびに使用されます。構造化クローン作成の優れた点は、循環オブジェクトを処理し、多数の組み込み型をサポートしていることです。問題は、この記事の執筆時点では、このアルゴリズムは直接利用できず、他の API の一部としてのみ利用できることです。何が含まれているかを知る必要があると思います。 。 。 🎜<h3>MessageChannel</h3>🎜 先ほども述べたように、構造化クローン作成アルゴリズムは、<code>postMessage を呼び出している限り機能します。 MessageChannel を作成してメッセージを送信できます。受信側では、メッセージには元のデータ オブジェクトの構造化されたクローンが含まれています。 🎜rrreee🎜 このアプローチの欠点は、非同期であることです。これは問題ありませんが、オブジェクトを同期的にディープ コピーする必要がある場合があります。 🎜

History API

🎜 history.pushState() を使用して SPA を作成したことがある場合は、URL を保存するための状態オブジェクトを提供できることをご存知でしょう。強い>。この状態オブジェクトは構造化されたクローン作成を使用しており、同期していることがわかります。プログラム ロジックで使用される状態オブジェクトを台無しにしないように注意する必要があるため、クローン作成の完了後に元の状態に復元する必要があります。予期せぬ事態を避けるために、history.pushState() の代わりに history.replaceState() を使用してください。 🎜rrreee🎜 ただし、オブジェクトをコピーするためだけにブラウザのエンジンを使用するのは、少しやりすぎのように感じます。さらに、Safari では、replaceState 呼び出しの数が 30 秒間に 100 回に制限されています。 🎜

通知 API

🎜 ツイートの後、Jeremy Banks は構造化クローンを活用する 3 番目の方法である通知 API を教えてくれました。 🎜rrreee🎜短くて簡潔です。私はそれが好きです! 🎜🎜ただし、ブラウザ内の許可メカニズムが必要なので、非常に遅いのではないかと思います。何らかの理由で、Safari は常に unknown を返します。 🎜

パフォーマンスの祭典

どの方法が最もパフォーマンスが高いかを測定したいと思います。最初の (単純な) 試みでは、小さな JSON オブジェクトを取得し、そのオブジェクトをさまざまな方法で 1,000 回複製しました。幸いなことに、Mathias Bynens は、オブジェクトにプロパティを追加するときに V8 にはキャッシュがあると教えてくれました。そこでキャッシュのベンチマークを行っています。キャッシュにヒットしないようにするために、ランダムなキー名で指定された深さと幅のオブジェクトを生成する関数を作成し、テストを再実行しました。

チャート!

ここでは、Chrome、Firefox、Edge のさまざまなテクノロジーのパフォーマンスを示します。低いほど良いです。

JavaScript ディープコピーのパフォーマンスについての深い理解

JavaScript ディープコピーのパフォーマンスについての深い理解

JavaScript ディープコピーのパフォーマンスについての深い理解

結論

それで、ここから何が得られたでしょうか?

  • ループ オブジェクトがなく、組み込み型を保持する必要がない場合は、クロスブラウザーの JSON.parse(JSON.stringify()) を使用できます。クローン作成のパフォーマンスが最も速く、非常に驚​​きました。 JSON.parse(JSON.stringify())获得最快的克隆性能,这让我感到非常惊讶。

  • 如果你想要一个适当的结构化克隆,MessageChannel是你唯一可靠的跨浏览器的选择。

如果浏览器平台直接提供一个 structuredClone()

適切に構造化されたクローンが必要な場合は、MessageChannel が唯一の信頼できるクロスブラウザー オプションです。

ブラウザ プラットフォームが structurdClone() 関数を直接提供した方がよいでしょうか?確かにそう思います。最新の HTML 仕様では、この Synchronous clone = global.structurdClone(value, transfer = []) API · Issue #793 · whatwg/html について議論しています。

関連する推奨事項:

jQuery の $.extend 浅いコピーと深いコピーの例の分析

jQuery での深いコピーと浅いコピーの実装

🎜🎜 Js の浅いコピーと深いコピーとは何ですか🎜🎜

以上がJavaScript ディープコピーのパフォーマンスについての深い理解の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。