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

ディープコピー用の組み込み関数である structuredClone() が Platform に追加されました。

長い間、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() が追加されました。バンザイ。