Shallow Copyについて

スプレッド構文を使ってArrayやObject をコピーする方法を調べた際に、”shallow copy”という言葉が出てきた。コピーしたArrayに変更を加えたときに、起きる不思議な現象について調べてみた。

まずは普通の(違和感がない)コピー
※Primitive Values (文字列、数字、Boolean、undefined、null)をコピーすると、値自体がコピーされる

JavaScript
const array1 = [1, 2, 3];
const array2 = [...array1];

// array1 の数字を変更しても、array2は変わらない。
array1[0] = 10;

// array2 の数字を変更しても、array1は変わらない。
array2[1] = 20;

console.log(array1); // [10, 2, 3]
console.log(array2); // [1, 20, 3]

しかし、コピーするArrayの中身にArrayが入っていてその中身を変える場合、片方を変えると、もう片方も変わってしまう。
※Reference Values(array, object, function)がこのタイプ

JavaScript
const array1 = [[1], [2], [3]];  //以下①参照
const array2 = [...array1]; //以下②参照

array1[0][0] = 10; 
array2[1][0] = 20;

//片方を変えると、もう片方も変わってしまう //以下③参照
console.log(array1); // [[10], [20], [3]];
console.log(array2); // [[10], [20], [3]];

どうしてこうなるのか?

① const array1 = [[1], [2], [3]]  を実行すると、”array1″という名前で[[1], [2], [3]]という情報がコンピュータのメモリに保存される。array1を呼び出すと、”array1″という名前で保存されたデータの場所を参照しに行き、中身を表示している。

② array2という名前でarray1の情報をスプレッド構文を使って保存しているが、ネストされているarrayをコピーすると、array1の中身 1, 2, 3 ではなく、実際には[1という情報を保存した場所の参照先]、[2という情報を保存した場所の参照先]、[3という情報を保存した場所の参照先]がコピーされている。

③ データの参照先が同じ状態のため、片方を書き換えると、同じ場所を参照しているもう片方を呼び出すと変更後の値が出てくる。

中身まですべてコピーすることを”Deep Copy”というのに対し、このように参照先だけコピーすることを”Shallow Copy(浅いコピー)と言われる。


ネストされたArrayやObjectをスプレッド構文でコピーするとこうなる、とざっくり理解したところでさらにネストされた例へ。

JavaScript
const array1 = [ // Code ① 
  { list: ["dog", "cat"] }, // Code ②
  { list: ["bird", "rabbit"] } // Code ③
];
const array2 = [...array1]; // Code ④

// array2 の内容を以下の2パターンで書き換えてみる
array2[0].list = ["platypus", "wombat"] // Code ⑤ // listというプロパティの中身を更新
array2[1] = { list: ["koala", "kangaroo"] } // Code ⑥ // 新しいオブジェクトで上書き

// listというプロパティの中身を更新したほうはshallow copyされたが
//新しいオブジェクトで上書きした方は参照先ごと上書きされている。
console.log(array1); // [{ list: ["platypus", "wombat"] },{ list: ["bird", "rabbit"] }];
console.log(array2); // [{ list: ["platypus", "wombat"] },{ list: ["koala", "kangaroo"] }];

分かるような分からないような・・・?
下のテーブルと合わせて順を追ってみていく。

Code①
const array1 は配列 [ ] なので、メモリ(以下Mとする)#1にその中身の情報が登録され、M-#1の参照先がarray1の値として登録される。
Code②
M-#1に渡された中身が数字や文字列ならシンプルだったが、今回はarray1[0]とarray1[1]がオブジェクトのため、中身の情報はM-#2、M-#3に登録され、M-#1にはそれぞれの参照先だけが渡される。
Code③
さらに、listというプロパティの中身が配列なので、 中身の情報はM-#4、M-#5に登録される。
Code④
この状態でconst array2 = […array1] を実行する。arrayのコピーはshallow copyになるので、中身の値ではなく、arrayの値が入ったメモリの参照先だけをコピーしている。具体的にはまずarray2という新しい配列が作られたのでM-#6にその中身が保存されるが、この時スプレッド構文を使ってarray1と同じ値が保存される。array1と同じ値というのはM-#1に保存されている[ M-#2 , M-#3 ]なので、M-#6にも[ M-#2 , M-#3 ]が入る。
Code⑤
array2[0]は、M-#6にある通りM-#2のことである。ここではその中のlistがM-#4となっているものをM-#7 [“platypus”, “wombat”]に置き換える。array1[0]もarray2[0]もM-#2を見ているので、どちらも[“platypus”, “wombat”]となる。
Code⑥
array2[1] には、M-#6にある通りM-#3が入っているが、それを{ list: [“koala”, “kangaroo”] }に置き換える。オブジェクトなので、M-#8の参照先が渡されるこの時、M-#6が変わっているだけで、array1[1]はM-#3を参照したままなので、array2[1]のみが{ list: [“koala”, “kangaroo”] }となる。

メモリ#メモリに渡された値array1array2
M-#1[ M-#2 , M-#3 ]←array1
M-#2{ list: M-#4 }
→Code⑤で{ list: M-#7 }に書き換わる
←array1[0]
M-#3{ list: M-#5 }←array1[1]
M-#4[“dog”, “cat”]←array1[0].list
M-#5[“bird”, “rabbit”]←array1[1].list
M-#6[ M-#2 , M-#3 ]
→Code⑥で[ M-#2 , M-#8 ]に書き換わる
←array2
M-#7[“platypus”, “wombat”]←array2[0].list
M-#8{ list: M-#9 }←array2[1]
M-#9[“koala”, “kangaroo”]←array2[1].list

長くなったがshallow copyとは何か、また、見えないところでどのようにデータの受け渡しがされているのかイメージできたところで本日は終了!


もう7月!?

platypus with fan

類似投稿