環境
- macOS Monterey 12.6
- Windows 11
- VSCode
- node.js v18.6.0
結論
structuredClone
を使用する。この関数の引数にコピーしたいオブジェクトを渡すだけでOK。返り値は元のオブジェクトのディープコピーされた値が返る
具体的には以下のようにoriginalに影響を及ぼすことなく、コピーに対してのみ変更を加えることが可能(keyに関しては浅いコピーでも問題なし)
const original = {
key: "key1",
obj: {id: 1, name: "foo"},
list: [2,1,4,3,5]
}
const deepCopied = structuredClone(original)
deepCopied.key = "key2"
deepCopied.obj.id = 2
deepCopied.obj.name = "hoge"
deepCopied.list.push(6)
// [LOG]: { "key": "key1", "obj": { "id": 1, "name": "foo" }, "list": [ 2, 1, 4, 3, 5 ] }
console.log(original)
// [LOG]: { "key": "key2", "obj": { "id": 2, "name": "hoge" }, "list": [ 2, 1, 4, 3, 5, 6 ] }
console.log(deepCopied)
2021年11月にFirefoxで実装され、現在はモダンブラウザでは対応済み
https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#browser_compatibility
ユースケース
ネストされたオブジェクトのディープコピーを最小限の記述で行いたい場合に有効。
不変のデータ構造を使用している場合や、元のオブジェクトに影響を与えずに関数がオブジェクトを操作できるようにしたい場合などを想定(オブジェクトをコピーしてコピー後のオブジェクトのプロパティのみに変更を加えたつもりが、意図せずオリジナルにも変更が加わりそれによってバグが生じるということを回避できる)
オブジェクトのプロパティを 1 つずつコピーしながら、別のオブジェクトへの参照を見つけると再帰的にそれらを呼び出し、そのオブジェクトのコピーも作成してくれる
注意点
関数およびメソッドはコピーできない(グローバルコンテキスト、関数コンテキスト、オブジェクトのメソッドいずれも不可)。また、DOM要素(HTMLElement オブジェクト)のコピーもできない
以下は関数のコピーの例
const hoge = () => {
return "hoge"
}
const hogeCopy = structuredClone(hoge)
// [ERR]: Failed to execute 'structuredClone' on 'Window': () => { return "hoge"; } could not be cloned.
console.log(hogeCopy)
また、クラスのインスタンスをコピーする場合は注意が必要。コピーによりプロトタイプチェーンが破棄されるため、プレーンオブジェクトとしてコピーが生成されるのと、メソッドが含まれている場合は削除される
class Product {
private name = "キーボード"
public printName() {
console.log(this.name)
}
}
const productInstance = new Product()
const deepCopied = structuredClone(productInstance)
// [LOG]: { "name": "キーボード" }
console.log(deepCopied)
従来までの方法
浅いコピーであれば、スプレッド構文やObject.assignで事足りる
(オリジナルに影響を与えることなくコピーに対して変更を加えることができる)
const original = {
id: 1,
name: "hoge"
}
const shallowCopied = {...original}
shallowCopied.id = 2
shallowCopied.name = "foo"
// [LOG]: { "id": 1, "name": "hoge" }
console.log(original)
// [LOG]: { "id": 2, "name": "foo" }
console.log(shallowCopied)
ネストされたオブジェクトをスプレッド構文やObject.assignでコピーすると、ネストされた値までコピーできず同じ参照を持つため、コピー先の値を変更するとコピー元にも影響してしまう
const original = {
key: "key1",
obj: {id: 1, name: "foo"},
list: [2,1,4,3,5]
}
const shallowCopied = {...original}
shallowCopied.key = "key2"
shallowCopied.obj.id = 2
shallowCopied.obj.name = "foo"
shallowCopied.list.push(6)
// [LOG]: { "key": "key1", "obj": { "id": 2, "name": "foo" }, "list": [ 2, 1, 4, 3, 5, 6 ] }
console.log(original)
// [LOG]: { "key": "key2", "obj": { "id": 2, "name": "foo" }, "list": [ 2, 1, 4, 3, 5, 6 ] }
console.log(shallowCopied)
そのため、これまではlodashなどのサードパーティー製のライブラリを使用するかJSON.parse(JSON.stringify())を使用する必要があった
オブジェクトを JSON文字列に変換し、再度オブジェクトに戻す方法では、undefinedの値もつキーが消滅する点や再帰的なデータ構造に対応していないなどの問題点があった
const original = {
key: "key1",
obj: {id: 1, name: "foo"},
list: [2,1,4,3,5],
und: undefined
}
const deepCopied = JSON.parse(JSON.stringify(original));
deepCopied.key = "key2"
deepCopied.obj.id = 2
deepCopied.obj.name = "foo"
deepCopied.list.push(6)
// [LOG]: { "key": "key1", "obj": { "id": 1, "name": "foo" }, "list": [ 2, 1, 4, 3, 5 ], "und": undefined }
console.log(original)
// [LOG]: { "key": "key2", "obj": { "id": 2, "name": "foo" }, "list": [ 2, 1, 4, 3, 5, 6 ] }
console.log(deepCopied)
これに対し、structuredCloneを使用すると、undefinedの値を持つキーがコピー後のオブジェクトでも保持されるし、再帰的なデータ構造も対応している
以下はundefinedがコピー後も保持されている例
const original = {
key: "key1",
list: [2,1,4,3,5],
und: undefined
}
const deepCopied = structuredClone(original)
// 降順に並び替え
const sorted = deepCopied.list.sort((num1: number, num2: number) => num2 - num1) //sortは破壊的(mutable)なメソッド
// [LOG]: { "key": "key1", "list": [ 2, 1, 4, 3, 5 ], "und": undefined }
console.log(original)
// [LOG]: { "key": "key1", "list": [ 5, 4, 3, 2, 1 ], "und": undefined }
console.log(deepCopied)まとめ
まとめ
いかがでしたでしょうか。今回は、本記事では、structuredCloneを用いて、JavaScriptで最も簡単にオブジェクトをディープコピーする方法について紹介しました。lodashやJSON化する従来の方法よりもより簡単に扱いやすくディープコピーができるビルトインの関数なのでぜひ使用してみてください