JavaScriptでオブジェクトをコピーする
オブジェクトはJavaScriptの基本的なブロックです。 オブジェクトはプロパティのコレクションであり、プロパティはキー(または名前)と値の間の関連付けです。 JavaScriptのほとんどすべてのオブジェクトは、プロトタイプチェーンの最上位にあるObject
のインスタンスです。
序章
ご存知のように、代入演算子はオブジェクトのコピーを作成せず、オブジェクトへの参照を割り当てるだけです。次のコードを見てみましょう。
let obj = { a: 1, b: 2, }; let copy = obj; obj.a = 5; console.log(copy.a); // Result // a = 5;
obj
変数は、初期化された新しいオブジェクトのコンテナです。 copy
変数は同じオブジェクトを指しており、そのオブジェクトへの参照です。 つまり、基本的にこの{ a: 1, b: 2, }
オブジェクトは次のように言っています。私にアクセスするには2つの方法があります。 obj
変数またはcopy
変数を通過する必要があります。どちらの方法でも私に到達でき、これらの方法(ゲートウェイ)を介して私に行うことはすべて私に影響します。
最近、不変性が広く話題になっているので、この呼び出しに耳を傾ける必要があります。 このメソッドは、あらゆる形式の不変性を削除し、元のオブジェクトがコードの別の部分で使用された場合にバグを引き起こす可能性があります。
オブジェクトをコピーする素朴な方法
オブジェクトをコピーする素朴な方法は、元のオブジェクトをループして、各プロパティを次々にコピーすることです。 このコードを見てみましょう:
function copy(mainObj) { let objCopy = {}; // objCopy will store a copy of the mainObj let key; for (key in mainObj) { objCopy[key] = mainObj[key]; // copies each property to the objCopy object } return objCopy; } const mainObj = { a: 2, b: 5, c: { x: 7, y: 4, }, } console.log(copy(mainObj));
固有の問題
objCopy
オブジェクトには、mainObj
オブジェクトのプロトタイプメソッドとは異なる新しいObject.prototype
メソッドがありますが、これは私たちが望んでいることではありません。 元のオブジェクトの正確なコピーが必要です。- プロパティ記述子はコピーされません。 値がfalseに設定された「書き込み可能な」記述子は、
objCopy
オブジェクトでtrueになります。 - 上記のコードは、
mainObj
の列挙可能なプロパティのみをコピーします。 - 元のオブジェクトのプロパティの1つがオブジェクト自体である場合、それはコピーと元のオブジェクトの間で共有され、それぞれのプロパティが同じオブジェクトを指すようになります。
浅いコピーオブジェクト
ソースの最上位プロパティが参照なしでコピーされ、値がオブジェクトであり、参照としてコピーされるソースプロパティが存在する場合、オブジェクトは浅くコピーされたと言われます。 ソース値がオブジェクトへの参照である場合、その参照値のみがターゲットオブジェクトにコピーされます。
浅いコピーは最上位のプロパティを複製しますが、ネストされたオブジェクトは元の(ソース)とコピー(ターゲット)の間で共有されます。
Object.assign()メソッドの使用
Object.assign()メソッドは、列挙可能なすべての独自のプロパティの値を1つ以上のソースオブジェクトからターゲットオブジェクトにコピーするために使用されます。
let obj = { a: 1, b: 2, }; let objCopy = Object.assign({}, obj); console.log(objCopy); // Result - { a: 1, b: 2 }
さて、これはこれまでのところ仕事をします。 obj
のコピーを作成しました。 不変性が存在するかどうかを見てみましょう。
let obj = { a: 1, b: 2, }; let objCopy = Object.assign({}, obj); console.log(objCopy); // result - { a: 1, b: 2 } objCopy.b = 89; console.log(objCopy); // result - { a: 1, b: 89 } console.log(obj); // result - { a: 1, b: 2 }
上記のコードでは、objCopy
オブジェクトのプロパティ'b'
の値を89
に変更し、変更したobjCopy
オブジェクトをコンソールに記録すると変更はobjCopy
にのみ適用されます。 コードの最後の行は、obj
オブジェクトがまだ無傷であり、変更されていないことを確認します。 これは、ソースオブジェクトへの参照なしでソースオブジェクトのコピーが正常に作成されたことを意味します。
Object.assign()の落とし穴
そんなに早くない! コピーの作成に成功し、すべてが正常に機能しているように見えますが、浅いコピーについて説明したことを覚えていますか? この例を見てみましょう:
let obj = { a: 1, b: { c: 2, }, } let newObj = Object.assign({}, obj); console.log(newObj); // { a: 1, b: { c: 2} } obj.a = 10; console.log(obj); // { a: 10, b: { c: 2} } console.log(newObj); // { a: 1, b: { c: 2} } newObj.a = 20; console.log(obj); // { a: 10, b: { c: 2} } console.log(newObj); // { a: 20, b: { c: 2} } newObj.b.c = 30; console.log(obj); // { a: 10, b: { c: 30} } console.log(newObj); // { a: 20, b: { c: 30} } // Note: newObj.b.c = 30; Read why..
obj.bc = 30なのはなぜですか?
それがObject.assign()
の落とし穴です。 Object.assign
は浅いコピーのみを作成します。 newObj.b
とobj.b
はどちらも、個別のコピーが作成されなかったため、オブジェクトへの同じ参照を共有します。代わりに、オブジェクトへの参照がコピーされました。 オブジェクトのプロパティに加えられた変更は、オブジェクトを使用するすべての参照に適用されます。 どうすればこれを修正できますか? 続きを読む…次のセクションで修正があります。
注:プロトタイプチェーンのプロパティと列挙できないプロパティはコピーできません。 ここを参照してください:
let someObj = { a: 2, } let obj = Object.create(someObj, { b: { value: 2, }, c: { value: 3, enumerable: true, }, }); let objCopy = Object.assign({}, obj); console.log(objCopy); // { c: 3 }
someObj
はobjのプロトタイプチェーン上にあるため、コピーされません。property b
は列挙できないプロパティです。property c
には列挙可能なプロパティ記述子があり、列挙可能にすることができます。 それがコピーされた理由です。
オブジェクトのディープコピー
ディープコピーは、遭遇するすべてのオブジェクトを複製します。 コピーと元のオブジェクトは何も共有しないため、元のオブジェクトのコピーになります。 Object.assign()
を使用して発生した問題の修正は次のとおりです。 探検しましょう。
JSON.parse(JSON.stringify(object));を使用する
これにより、以前に発生した問題が修正されます。 これで、newObj.b
にはコピーがあり、参照はありません。 これは、オブジェクトをディープコピーする方法です。 次に例を示します。
let obj = { a: 1, b: { c: 2, }, } let newObj = JSON.parse(JSON.stringify(obj)); obj.b.c = 20; console.log(obj); // { a: 1, b: { c: 20 } } console.log(newObj); // { a: 1, b: { c: 2 } } (New Object Intact!)
不変:✓
落とし穴
残念ながら、このメソッドを使用してユーザー定義のオブジェクトメソッドをコピーすることはできません。 下記参照。
オブジェクトメソッドのコピー
メソッドは、関数であるオブジェクトのプロパティです。 これまでの例では、メソッドを使用してオブジェクトをコピーしていません。 今それを試して、コピーを作成するために学んだ方法を使用してみましょう。
let obj = { name: 'scotch.io', exec: function exec() { return true; }, } let method1 = Object.assign({}, obj); let method2 = JSON.parse(JSON.stringify(obj)); console.log(method1); //Object.assign({}, obj) /* result { exec: function exec() { return true; }, name: "scotch.io" } */ console.log(method2); // JSON.parse(JSON.stringify(obj)) /* result { name: "scotch.io" } */
結果は、Object.assign()
はメソッドのコピーに使用でき、JSON.parse(JSON.stringify(obj))
は使用できないことを示しています。
円形オブジェクトのコピー
円形オブジェクトは、それ自体を参照するプロパティを持つオブジェクトです。 これまでに学んだオブジェクトをコピーする方法を使用して、円形のオブジェクトのコピーを作成し、それが機能するかどうかを確認してみましょう。
JSON.parse(JSON.stringify(object))を使用する
JSON.parse(JSON.stringify(object))
を試してみましょう:
// circular object let obj = { a: 'a', b: { c: 'c', d: 'd', }, } obj.c = obj.b; obj.e = obj.a; obj.b.c = obj.c; obj.b.d = obj.b; obj.b.e = obj.b.c; let newObj = JSON.parse(JSON.stringify(obj)); console.log(newObj);
結果は次のとおりです。
JSON.parse(JSON.stringify(obj))
は明らかに円形のオブジェクトでは機能しません。
Object.assign()の使用
Object.assign()
を試してみましょう:
// circular object let obj = { a: 'a', b: { c: 'c', d: 'd', }, } obj.c = obj.b; obj.e = obj.a; obj.b.c = obj.c; obj.b.d = obj.b; obj.b.e = obj.b.c; let newObj2 = Object.assign({}, obj); console.log(newObj2);
結果は次のとおりです。
Object.assign()
は、円形オブジェクトの浅いコピーには正常に機能しますが、深いコピーには機能しません。 ブラウザコンソールでcircular object tree
を自由に探索してください。 きっとたくさんの面白い仕事が行われていることでしょう。
スプレッド要素の使用(…)
ES6には、配列を破棄する割り当て用の残りの要素と、実装されている配列リテラル用の拡散要素がすでにあります。 ここで、配列へのスプレッド要素の実装を見てください。
const array = [ "a", "c", "d", { four: 4 }, ]; const newArray = [...array]; console.log(newArray); // Result // ["a", "c", "d", { four: 4 }]
オブジェクトリテラルのspreadプロパティは、現在、ECMAScriptのステージ3の提案です。 オブジェクト初期化子のスプレッドプロパティは、独自の列挙可能なプロパティをソースオブジェクトからターゲットオブジェクトにコピーします。 以下の例は、提案が受け入れられた後、オブジェクトをコピーするのがいかに簡単であるかを示しています。
let obj = { one: 1, two: 2, } let newObj = { ...obj }; // { one: 1, two: 2 }
注:これは浅いコピーにのみ有効です
結論
JavaScriptでオブジェクトをコピーすることは、特にJavaScriptを初めて使用し、言語の使い方がわからない場合は、非常に困難な場合があります。 この記事が、オブジェクトのコピーで発生する可能性のある将来の落とし穴を理解し、回避するのに役立つことを願っています。 より良い結果を達成するライブラリまたはコードがある場合は、コミュニティと共有することを歓迎します。 ハッピーコーディング!