オブジェクト代入で「あれっ?」(Object.assign,スプレッド構文)

JavaScript

JavaScriptでオブジェクトを変数に代入して、ゴニョゴニョする処理を書きました。
代入先の変数の内容を変えると、代入元の内容も変わって「あれっ?」となりました。

そういえば、JavaScriptではオブジェクトの代入は単純なコピーじゃなくて、代入元の値も参照されることを思い出しました。
これを回避するにはObject.assignやスプレッド構文を使用します。

JavaScriptでオブジェクトを代入すると…

今回問題になったコードの例を記載します。

let fruits = { apple: 'りんご', banana: 'ばなな', lemon: 'レモン'};
let copyFruits = fruits;

copyFruits.lemon = 'れもん';
console.log(fruits); // 結果:{ apple: 'りんご', banana: 'ばなな', lemon: 'れもん'}

fruitsオブジェクトをcopyFruitsオブジェクトに代入します。
コピー先のcopyFruitslemonプロパティを書き換えて、コピー元の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: 'ぶどう' }

fruit1fruit2をObject.assignでマージしています。
結果として、両方のオブジェクトのプロパティがマージされた内容が返ってきました。

返ってくるのは、マージされたコピー先のオブジェクトです。
なので、上記の状態で、それぞれfruit1fruit2を出力すると…

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([オブジェクト]));で対応していくと良いかと思います。

コメント

タイトルとURLをコピーしました