プラットフォームには、ディープコピー用の組み込み関数である structuredClone() が付属しています。
長い間、JavaScript 値のディープコピーを作成するには、回避策やライブラリを利用する必要がありました。プラットフォームには、ディープコピー用の組み込み関数 structuredClone()
が付属しています。
シャローコピー
JavaScript での値のコピーは、ほとんどの場合ディープではなくシャローです。つまり、深くネストされた値に対する変更は、元の値だけでなく、コピーされた状態にも反映されます。
オブジェクト スプレッド演算子 ...
を使用して JavaScript でシャローコピーを作成する方法の一つを次に示します。
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}
は、Spread 演算子を使用して myOriginal
の(列挙可能な)プロパティを反復処理します。プロパティの名前と値を使用して、新しく作成された空のオブジェクトに 1 つずつ割り当てます。そのため、結果として得られるオブジェクトの形状は同じですが、プロパティと値のリストを独自にコピーしたものです。値もコピーされますが、いわゆるプリミティブ値は、JavaScript 値によって、非プリミティブ値とは異なる方法で処理されます。MDN を引用するには:
JavaScript におけるプリミティブ(プリミティブ値、プリミティブ データ型)とは、オブジェクトではなくメソッドを持たないデータのことです。string、number、bigint、boolean、未定義、symbol、null の 7 つのプリミティブ データ型があります。
MDN - プリミティブ
非プリミティブ値は参照referencesとして処理されます。つまり、値をコピーする操作は実際には、同じ基礎となるオブジェクトへの参照をコピーするだけで、浅いコピー動作になります。
ディープコピー
シャローコピーの逆はディープコピーです。また、ディープコピー アルゴリズムはオブジェクトのプロパティを 1 つずつコピーしますが、別のオブジェクトへの参照を見つけると自身を再帰的に呼び出し、そのオブジェクトのコピーを作成します。これは、2 つのコードが誤ってオブジェクトを共有して、互いの状態を知らないうちに操作しないようにするために、非常に重要です。
以前は、JavaScript で値のディープコピーを作成する簡単な方法も、便利な方法もありませんでした。多くの人が、Lodash の cloneDeep()
関数のようなサードパーティ ライブラリを利用していました。この問題の最も一般的な解決策は、おそらく、JSON ベースのハッキングでした。
const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));
実際、これは非常に一般的な回避策であり、V8 は JSON.parse()
、具体的には上記のパターンを可能な限り高速に最適化しました。高速ですが、欠点とトリップワイヤもいくつかあります。
- 再帰データ構造: 再帰データ構造を指定すると、
JSON.stringify()
はスローします。これは、リンクされたリストやツリーを操作するときに非常に簡単に発生します。 - 組み込み型:
JSON.stringify()
は、値にMap
、Set
、Date
、RegExp
、ArrayBuffer
などの他の JS 組み込みが含まれている場合にスローします。 - 関数:
JSON.stringify()
は関数を静かに破棄します。
構造化クローン作成
このプラットフォームにはすでに、JavaScript 値のディープコピーを作成する機能が 2 か所に必要でした。JS 値を IndexedDB に保存するには、なんらかの形でシリアル化する必要があります。そうすればディスクに保存し、後でシリアル化解除して JS 値を復元できます。同様に、postMessage()
を介して WebWorker にメッセージを送信するには、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()
が追加されました。やったー。