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

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

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

対応ブラウザ

  • Chrome: 98。
  • Edge: 98.
  • Firefox: 94。
  • Safari: 15.4。

ソース

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 では、プリミティブ(プリミティブ値、プリミティブ データ型)とは、オブジェクトではなく、メソッドを持たないデータです。プリミティブ データ型は、文字列、数値、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 値を 1 つの 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() が追加されました。バンザイ。