💀

ある日、太郎はシャローコピーの呪いにかけられた💀

2021/10/04に公開
2

JavaScriptで以下のようなことがおきます.

const arr = [{ name: 'taro', age: 28 }];
const newArr = [...arr];
newArr[0].name = 'HAGE'; // 新しく作った配列の要素の一部を変更しているつもりでも
console.log(arr); // 元の配列を見てみると
// 結果
[{ name: 'HAGE', age: 28 }]; // 書き換わってしまっている!

シャローコピーの呪いにかかったようなので解き方を書きます.

Take1(前提知識)

俺の名は太郎.どこにでもいる成人男性である.

スペックはこんな感じだ.

const iamTaro = {
  name: 'Taro',
  gender: 'male',
  age: 28,
};

そんなある日,魔の手が彼に襲いかかる.

const taro2sei = iamTaro;

taro2sei.age = 97;

太郎は朝目覚めるとなんだか体が重くなっていた.

やっとのおもいで洗面所につき,鏡を見ると,おじいちゃんになっていた.

太郎:「なんでえええええええええ!!!?????」

console.log(iamTaro);

このときの太郎のスペック

{
  name: 'Taro',
  gender: 'male',
  age: 97, // 書き換わってしまっている!!!
}

内部的に新しいデータが作成されているかどうかの違い?

今回の原因は以下の行で新しい変数が作られたように見えるが,参照元を代入しただけの状態で,内部的に使用されるデータ自体は新しく作成されたことになっていないようだ.

const taro2sei = iamTaro;

そのため,taro2seiを書き換えると参照元であるiamTaroの内部的なデータも書き換わってしまったのである.

こんな時に使用するのがシャローコピー(浅いコピー)である.参照元から値を渡して,内部的にも新しい変数が作成されるので,新しく作成した変数を変更しても,参照元のデータを書き換えることはない.

const taro2sei = { ...iamTaro };

あるいは,

const taro2sei = Object.assign({}, iamTaro)

と書けば,太郎はおじいちゃんにならずに住む.

また,配列の場合のシャローコピーは書き方が割と豊富でこんな感じである.

const names = ['Taro', 'Zico', 'Sanji'];

const newNames1 = names; // 参照元の代入
const newNames2 = [...names]; // シャローコピー
const newNames3 = Array.from(names); // シャローコピー
const newNames4 = names.map((item) => item); // シャローコピー
const newNames5 = names.concat(); // シャローコピー

まぁとにかくスプレッド構文で新しい変数を作れば,参照元に影響されることはなさそうである(これが大きな間違いであることはまだ,知る由もなかった)

太郎:「なんだそんなことか.よかったー.」

Take2(本題)

俺の名は太郎.どこにでもいる三兄弟の長男だ.

スペックはこんな感じ.

const ippanPeoples = [
  { name: 'Taro', gender: 'male', age: 28 },
  { name: 'Zico', gender: 'female', age: 52 },
  { name: 'Sanji', gender: 'male', age: 21 },
];

そんなある日,魔の手が彼に襲いかかる.

const newIppanPeoples = [...ippanPeoples]; // しっかりとスプレッド構文で

newIppanPeoples[0].gender = 'female';

太郎は朝目覚めるとなんだか胸の辺りが膨らんでいることに気がつく.

恐る恐る洗面所に向かい,鏡を見ると,アラサー女性になっていた.

太郎:「どうしてなのよおおおおおおおおお!!!???????」

console.log(ippanPeoples);

このときの太郎兄弟のスペック

[
  { name: 'Taro', gender: 'female', age: 28 }, // genderが書きkw
  { name: 'Zico', gender: 'female', age: 68 },
  { name: 'Sanji', gender: 'male', age: 21 }
]

ネストされた配列やオブジェクトはディープコピーで

残念ながら,ネストされている配列やオブジェクトに対してはシャローコピー(浅いコピー)ではその名の通り,ネスト深くまで内部的にデータが新規作成されていない様子.

const newIppanPeoples = JSON.parse(JSON.stringify(ippanPeoples));

JSONになったものを戻すことによって,ネストされたデータに関しても内部的に新しく作成されるのである.これをディープコピー(深いコピー)と言う.

太郎:「 ス マ ー ト じ ゃ な い ね . 」

※用語に関しては多少あいまいです.

Discussion

standard softwarestandard software

参照渡しという言葉の定義があっていうかどうかはさておき、

シャローコピーと、ディープコピーの用語が間違ってますよ。

const taro2sei = iamTaro;
// これは、参照を変数代入しているだけ(参照渡しと呼ぶか?)

const taro2sei = { ...iamTaro };
const taro2sei = Object.assign({}, iamTaro)
// これらはシャローコピー

ディープコピーについては、
JSONでやるのもあるけど

lodash の _.cloneDeep あたりを使うのが定番かと思います。

手前味噌なんですが、cloneDeepを自分で実装したことあります。それなりに大変です。

JavaScript さまざまな型に対応できる拡張機能つき clone と cloneDeep を実装しました。 - Qiita
https://qiita.com/standard-software/items/54bf2284ae1833a786d7

Knob/のまど先生Knob/のまど先生

なんと!一階層ずれていたと言った具合ですかね...
ご丁寧にありがとうございます!