構造化クローンを使用した JavaScript でのディープコピー

プラットフォームには、ディープコピーの組み込み関数である structuredClone() が付属しています。

長い間、回避策とライブラリを利用して JavaScript 値のディープコピーを作成する必要がありました。プラットフォームには、ディープコピーの組み込み関数である structuredClone() が付属しています。

対応ブラウザ

  • Chrome: 98。 <ph type="x-smartling-placeholder">
  • エッジ: 98。 <ph type="x-smartling-placeholder">
  • Firefox: 94。 <ph type="x-smartling-placeholder">
  • Safari: 15.4。 <ph type="x-smartling-placeholder">

ソース

シャローコピー

JavaScript での値のコピーは、ほとんどの場合、「ディープ」ではなく「シャロー」です。つまり、深くネストされた値に対する変更は、元の値だけでなくコピーにも反映されます。

JavaScript でオブジェクト スプレッド演算子 ... を使用してシャローコピーを作成する方法の 1 つは次のとおりです。

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

シャローコピーでプロパティを直接追加または変更すると、そのコピーにのみ影響し、元のコピーには影響しません。

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

ただし、深くネストされたプロパティを追加または変更すると、コピーと元のプロパティの両方に影響します。

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

{...myOriginal} は、スプレッド演算子を使用して、myOriginal の(列挙可能な)プロパティを反復処理します。プロパティ名と値を使用して、新しく作成された空のオブジェクトに 1 つずつ割り当てます。したがって、生成されるオブジェクトの形状は同一ですが、プロパティと値のリストが独自にコピーされます。値もコピーされますが、いわゆるプリミティブ値は JavaScript 値によって非プリミティブ値とは異なる方法で処理されます。MDN の引用元:

JavaScript では、プリミティブ(プリミティブ値、プリミティブ データ型)とは、オブジェクトではなく、メソッドを持たないデータです。プリミティブ データ型には、string、number、bigint、ブール値、未定義、シンボル、null の 7 つがあります。

MDN - 基本

非プリミティブ値は参照として処理されます。つまり、値をコピーする操作は、実際には同じ基盤となるオブジェクトへの参照をコピーするだけであり、シャローコピーの動作が発生します。

ディープコピー

シャローコピーの反対がディープコピーです。ディープコピー アルゴリズムも、オブジェクトのプロパティを 1 つずつコピーしますが、別のオブジェクトへの参照を見つけると再帰的に呼び出し、そのオブジェクトのコピーも作成します。これは、2 つのコードが誤ってオブジェクトを共有し、知らないうちに互いの状態を操作することを防ぐために、非常に重要です。

これまで、JavaScript で値のディープコピーを作成する簡単で便利な方法はありませんでした。多くのユーザーが、Lodash の cloneDeep() 関数のようなサードパーティ ライブラリに依存しています。この問題の最も一般的な解決策は、おそらく JSON ベースのハッキングです。

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

実際、これは非常に人気のある回避策であり、V8 は JSON.parse()、具体的には上記のパターンを積極的に最適化して、可能な限り高速になりました。また、高速である一方で、欠点とトリップワイヤーもいくつかあります。

  • 再帰データ構造: 再帰データ構造を指定すると、JSON.stringify() がスローされます。これは、リンクリストやツリーを使って作業する場合に非常に簡単に発生します。
  • 組み込み型: 値に MapSetDateRegExpArrayBuffer などの他の JS 組み込み型が含まれている場合、JSON.stringify() がスローされます。
  • 関数: JSON.stringify() は関数を自動的に破棄します。

構造化クローン

このプラットフォームには、すでに JavaScript 値のディープコピーをいくつかの場所に作成する機能が必要でした。IndexedDB に JS 値を保存するには、なんらかのシリアル化が必要であり、それをディスクに保存し、後でシリアル化解除して JS 値を復元できるようにします。同様に、postMessage() を介して WebWorker にメッセージを送信するには、JS 値をある JS レルムから別の JS レルムに転送する必要があります。このために使用されるアルゴリズムは「構造化クローン」と呼ばれ、最近までデベロッパーが簡単に利用できるものではありませんでした。

状況が変わりました。HTML 仕様が修正され、デベロッパーが JavaScript 値の詳細なコピーを簡単に作成できるようにするための手段として、このアルゴリズムを正確に実行する structuredClone() という関数が公開されました。

const myDeepCopy = structuredClone(myOriginal);

これで完了です。以上が API 全体です。詳細については、MDN に関する記事をご覧ください。

機能と制限事項

構造化クローニングは、JSON.stringify() 手法の多くの(すべてではありませんが)欠点に対処します。構造化クローンは、周期的なデータ構造を処理し、多くの組み込みデータ型をサポートできます。また、一般的に堅牢性が高く、多くの場合高速です。

ただし、次のような制限事項があります。

  • プロトタイプ: クラス インスタンスで structuredClone() を使用すると、戻り値としてプレーン オブジェクトが返されます。 オブジェクトのプロトタイプ チェーンが破棄されるため、
  • 関数: オブジェクトに関数が含まれている場合、structuredClone()DataCloneError 例外をスローします。
  • クローン作成不可: 一部の値(特に Error ノードと DOM ノード)は構造化クローン作成の対象外です。これは、 structuredClone() がスローします。

これらの制限のいずれかがユースケースにとって大きな問題になる場合でも、Lodash などのライブラリは、他のディープ クローニング アルゴリズムのカスタム実装を提供しており、ユースケースに適している場合もあれば、そうでない場合もあります。

パフォーマンス

新しいマイクロ ベンチマークの比較は行っていませんが、structuredClone() が公開される前の 2018 年初頭に比較を行いました。当時は、非常に小さいオブジェクトの場合、JSON.parse() が最速のオプションでした。その数は変わらないと予想しています。構造化クローン作成に依存する手法は、オブジェクトのサイズが大きいほど高速になります。新しい structuredClone() は他の API を悪用するオーバーヘッドがなく、JSON.parse() よりも堅牢であることを考慮すると、ディープコピーを作成するためのデフォルトのアプローチとして使用することをおすすめします。

まとめ

不変のデータ構造を使用している場合や、元のオブジェクトに影響を与えることなく関数がオブジェクトを操作できるようにする場合など、JS で値のディープコピーを作成する必要がある場合、回避策やライブラリを利用する必要がなくなります。JS エコシステムに structuredClone() が追加されました。やったー。