JavaScriptでオブジェクトを変数に代入して、ゴニョゴニョする処理を書きました。
代入先の変数の内容を変えると、代入元の内容も変わって「あれっ?」となりました。
そういえば、JavaScriptではオブジェクトの代入は単純なコピーじゃなくて、代入元の値も参照されることを思い出しました。
これを回避するにはObject.assignやスプレッド構文を使用します。
JavaScriptでオブジェクトを代入すると…
今回問題になったコードの例を記載します。
let fruits = { apple: 'りんご', banana: 'ばなな', lemon: 'レモン'};
let copyFruits = fruits;
copyFruits.lemon = 'れもん';
console.log(fruits); // 結果:{ apple: 'りんご', banana: 'ばなな', lemon: 'れもん'}
fruits
オブジェクトをcopyFruits
オブジェクトに代入します。
コピー先のcopyFruits
のlemon
プロパティを書き換えて、コピー元のfruits
を出力してみました。
すると、コピー元のプロパティも書き変わってしまいました。
これを回避して、値のみをコピーするようにしたい場合はObject.Assignかスプレッド構文を使います。
それぞれ確認してみましょう。
Object.assignを使用してコピーする
Object.assignを使用して、先ほどのコピー元参照の件を解決します。
まずは、Object.assignの挙動を見てみましょう。
Object.assignについて
Object.assignは下記のように使います。
let mergeObject = Object.assign(target, source);
第1引数のtarget
にコピー先のオブジェクト, 第2引数のsource
にコピー元のオブジェクトを記載します。
実行されると結果として、2つの内容をマージしたオブジェクトが返ってきます。
なので、こうすると…
let fruit1 = { banana:'ばなな', melon: 'めろん'};
let fruit2 = { apple:'りんご', grape: 'ぶどう'};
let mergeFruits = Object.assign(fruit1, fruit2);
console.log(mergeFruits); // 結果:{ banana: 'ばなな', melon: 'めろん', apple: 'りんご', grape: 'ぶどう' }
fruit1
とfruit2
をObject.assignでマージしています。
結果として、両方のオブジェクトのプロパティがマージされた内容が返ってきました。
返ってくるのは、マージされたコピー先のオブジェクトです。
なので、上記の状態で、それぞれfruit1
とfruit2
を出力すると…
console.log(fruit1); // 結果: { banana: 'ばなな', melon: 'めろん', apple: 'りんご', grape: 'ぶどう' }
console.log(fruit2); // 結果: { apple: 'りんご', grape: 'ぶどう' }
このようにfruit1
にマージされていることが、確認できます。
このためfruit1
の内容を変更するとmergeFruits
の内容も変わります。
Object.assignでオブジェクト参照を解決する
空のオブジェクトとコピーしたいオブジェクトをマージすることで、解決できます。
具体的には下記のようにします。
let fruits = { apple: 'りんご', banana: 'ばなな', lemon: 'レモン'};
let copyFruits = fruits;
copyFruits = Object.assign({}, fruits);
copyFruits.lemon = 'れもん';
console.log(fruits); // 結果:{ apple: 'りんご', banana: 'ばなな', lemon: 'レモン' }
console.log(copyFruits); // 結果:{ apple: 'りんご', banana: 'ばなな', lemon: 'れもん' }
このように空のオブジェクトとコピー元をマージしました。(Object.assign({}, fruits)
)
こうすることで、コピー先のオブジェクトが返って、fruits
のオブジェクトには値の変更の影響がなくなります。
Object.assignはShallow Copyなので注意
Object.assignはshallow copy(浅いコピー)です。
なので、オブジェクトの階層が深くなった場合は、コピー元の参照も引き継がれてしまいます。
let fruits = { apple: 'りんご', banana: 'ばなな', other:{ watermelon:'スイカ' } };
let copyFruits = fruits;
copyFruits = Object.assign({}, fruits);
copyFruits.other.watermelon = '西瓜???';
console.log(fruits); // 結果:{ apple: 'りんご', banana: 'ばなな', other:{ watermelon:'西瓜???' } };
console.log(copyFruits); // 結果:{ apple: 'りんご', banana: 'ばなな', other:{ watermelon:'西瓜???' } };
このように、オブジェクトの中にオブジェクトを作成しています。(other:{ watermelon:'スイカ' }
の箇所)
階層が深くなった場合には、コピーした後にオブジェクトの中のオブジェクトを変更すると、コピー元にも影響があります。
この場合は、オブジェクトをJSON文字列に一度変換した後に、オブジェクトに戻すと解決できます。
let fruits = { apple: 'りんご', banana: 'ばなな', other:{ watermelon:'スイカ' } };
let copyFruits = fruits;
copyFruits = JSON.parse(JSON.stringify(fruits));
copyFruits.other.watermelon = '西瓜???';
console.log(fruits); // 結果:{ apple: 'りんご', banana: 'ばなな', other: { watermelon: 'スイカ' } }
console.log(copyFruits); // 結果:{ apple: 'りんご', banana: 'ばなな', other: { watermelon: '西瓜???' } }
こうすることで、Deep Copy(深いコピー)になって、コピー元と関係なく変更できるようになります。
スプレッド構文を使ってコピーする
スプレッド構文を使うことでも解決できます。
まず、スプレッド構文についての書き方をみてみましょう。
スプレッド構文の書き方
JavaScriptのスプレッド構文は...
を使用します。
引数や要素を書いた位置で展開してくれます。
オブジェクトで使用する場合は、このようになります。
let sweets1 = { cookie:'クッキー', chocolate:'チョコレート', candy:'飴'};
let sweets2 = { poteto:'ポテチ', ...sweets1, poteto_stick:'じゃがりこ'};
console.log(sweets2); // 結果:{ poteto: 'ポテチ', cookie: 'クッキー', chocolate: 'チョコレート', candy: '飴', poteto_stick: 'じゃがりこ'}
sweets2
にはsweets1
が展開されて、結果としてsweets1
の要素を含んだsweets2
ができました。
値だけコピーされるので、sweets2.cookie = "Cookie!!"
などと変更しても、sweets1
は変わりません。
スプレッド構文でオブジェクト参照を解決する
スプレッド構文を使って、参照される問題を解決します。
let fruits = { apple: 'りんご', banana: 'ばなな', lemon: 'レモン'};
let copyFruits = {...fruits};
copyFruits.lemon = 'れもん';
console.log(fruits); // 結果:{ apple: 'りんご', banana: 'ばなな', lemon: 'レモン' }
console.log(copyFruits); // 結果:{ apple: 'りんご', banana: 'ばなな', lemon: 'れもん' }
コピー先に代入するときに、コピー元をスプレッド構文で展開する({...fruits}
)と良いです。
これで値のみコピーされるので、コピー先の変更がコピー元に反映されません。
スプレッド構文もShallow Copyなので注意
スプレッド構文もshallow copy(浅いコピー)です。
let fruits = { apple: 'りんご', banana: 'ばなな', other:{ watermelon:'スイカ' } };
let copyFruits = {...fruits};
copyFruits.other.watermelon = '西瓜???';
console.log(fruits); // 結果:{ apple: 'りんご', banana: 'ばなな', other: { watermelon: '西瓜???' } }
console.log(copyFruits); // 結果:{ apple: 'りんご', banana: 'ばなな', other: { watermelon: '西瓜???' } }
Object.assignと同じで「オブジェクトの中のオブジェクト」のような場合は
コピー先の内容を変更するとコピー元も変更されてしまいます。
Object.assignのときに見たようにオブジェクトの階層が深くなる場合はJSON.parse(JSON.stringify([オブジェクト]));
を使うようにすると良いかと思います。
Object.Assignとスプレッド構文のどっちを使う方が良い?
スプレッド構文のほうが、見やすくて簡単にかけるので、こちらを使っていくと良いと思います。
どちらもオブジェクトの階層が深くなる場合は、Deep Copyでないとコピーできないため注意が必要です。
その場合はJSON.parse(JSON.stringify([オブジェクト]));
で対応していくと良いかと思います。
コメント