配列をコピーすると毎回バグる人へ
みなさん、JavaScriptの参照は理解していますか?
- 参照、実体
- 破壊メソッド、非破壊メソッド
- シャローコピー、ディープコピー
これを見て、「よく見るけど何のことかよく分かってない」となった人、あるいは、「配列をコピーすると、なんかよく分かんないけど毎回バグる」という人。
そんな人は、このクイズに取り組んでみてください。
ただドキュメントを読むよりも、理解度がグンと上がります。
この記事から出るときには、「参照、完全に理解した!」となっているはずです。
クイズ本編
問. 次のコードにある、問1から問7までのログの出力結果を答えてください。
もちろん、コンソールを使わずに、コードを読み解いて答える、ということです。
自信のない人もいるかもしれませんが、とりあえずやってみましょう。
const originalArray = [1, 2, 3]
const state = {
array: originalArray
}
console.log(originalArray === state.array) // 問1
console.log(originalArray === [1, 2, 3]) // 問2
originalArray.push(4)
console.log(originalArray) // -> [1, 2, 3, 4]
console.log(state.array) // 問3
console.log(originalArray === state.array) // 問4
state.array = originalArray.filter(v => v !== 2)
console.log(state.array) // 問5
console.log(originalArray) // 問6
console.log(originalArray === state.array) // 問7
解答
解き終わったらスクロールしてください。
/*
問1 true
問2 false
問3 [1, 2, 3, 4]
問4 true
問5 [1, 3, 4]
問6 [1, 2, 3, 4]
問7 false
*/
解説
問1
私たちが普段行う、
const temp = 1
というような変数宣言を行うとき、JavaScriptでは、
- tempのためのメモリを割り当てる
- 割り当てられたメモリに、1という値を格納する
という処理が行われます。ですから、例えば、
console.log(temp)
とログ出力を行うときは、
-
temp
に割り当てられたメモリに行き、 - そのメモリの中の
1
というデータを読む
という処理が行われます。この、「メモリのどこに割り当てられたか」を、「番地」と呼んだりします。
ここで、問題文のコードをもう1度見てみましょう。
const originalArray = [1, 2, 3]
const state = {
array: originalArray
}
console.log(originalArray === state.array) // ①
originalArray
という配列を定義してから、state
というオブジェクトのarray
というキーの値に、originalArray
を入れています。
このoriginalArray
とstate.array
が「等しい」のかどうか?というのが①の問題です。
まず、originalArray
とstate
の番地は異なります。この2人は違う番地に住んでいるのです。
では、state.array
、つまり、state
に格納されたarray
はどうでしょうか。面白いことに、state.array
の番地は、originalArray
と同じになるのです。こういうケースを、
-
state.array
はoriginalArray
を参照している -
state.array
の参照・参照先はoriginalArray
である
などと言います。
JavaScriptの===
は、「参照が同じかどうか」、つまり「番地が同じかどうか」をチェックしている(注)ので、originalArray === state.array
はtrue
になります。
注) オブジェクトの場合。参考サイト: https://jsprimer.net/basic/operator/#strict-equal-operator
問2
では、次のケースはどうでしょうか。
const originalArray = [1, 2, 3]
console.log(originalArray === [1, 2, 3]) // ②
originalArray
と[1, 2, 3]
、同じ要素を持った配列を比較しています。この2つの番地は同じになるでしょうか?
答えはNOです。originalArray === [1, 2, 3]
の右辺、[1, 2, 3]
には、originalArray
とは別の番地が割り当てられます。
ですから、originalArray === [1, 2, 3]
はfalse
になります。中身は一緒でも、この2つは別物だと言うことですね。
問3
const originalArray = [1, 2, 3]
const state = {
array: originalArray
}
originalArray.push(4)
console.log(originalArray) // -> [1, 2, 3, 4]
console.log(state.array) // 問3
今度は、originalArray
に値がpush
されています。この時、state.array
はどうなるでしょうか。
originalArray
に値がpush
されても、originalArray
の番地は変わりません。以前のままです。格納されているモノが[1, 2, 3]
から[1, 2, 3, 4]
へと変化しただけです。
state.array
は、originalArray
を参照しているので、「state.array
を読みに行く」とは、「参照先のoriginalArray
の番地を読みに行く」というのと同じです。ですから、出力結果はoriginalArray
と同じく[1, 2, 3, 4]
になります。
問4
const originalArray = [1, 2, 3]
const state = {
array: originalArray
}
originalArray.push(4)
console.log(originalArray === state.array) // 問4
問4は、実は問3を考える段階で既に答えが出ています。push
してもoriginalArray
の番地は変わらずでしたから、依然としてoriginalArray
とstate.array
の番地は同じです。
ですから、===
で「番地が同じかどうか」を比較した結果はtrue
になります。
問5, 問6
const originalArray = [1, 2, 3]
const state = {
array: originalArray
}
originalArray.push(4)
state.array = originalArray.filter(v => v !== 2)
console.log(state.array) // 問5
console.log(originalArray) // 問6
-
originalArray
に値をpush
する -
state.array
に、originalArray
をfilter
した結果を入れている
という2つの操作が行われています。少しややこしいですね。
1つずつ見ていきましょう。まずはpush
です。これは問4と同じですね。
originalArray.push(4)
console.log(originalArray) // -> [1, 2, 3, 4]
console.log(state.array) // 問3: [1, 2, 3, 4]
console.log(originalArray === state.array) // 問4: true
この段階では、originalArray
の番地とstate.array
の番地は同じです。
では次に、これにfilter
するとどうなるでしょうか。
filter
をちゃんと理解していなくても、問5だけは分かるかもしれません。2を除外していますから[1, 3, 4]
になりますね。
少し難しいのは、問6、つまりfilter
したあとのoriginalArray
がどうなるか、でしょう。これを考えるには、filter
がどういうメソッドなのかを理解する必要があります。
こちらのサイトの、filter
の説明を読んでみましょう。
Arrayのfilterメソッドは配列の要素を順番にコールバック関数へ渡し、コールバック関数がtrueを返した要素だけを集めた新しい配列を返す非破壊的なメソッドです。 配列から不要な要素を取り除いた配列を作成したい場合に利用します。
この「新しい配列」というのがポイントです。「新しい」というのは、「参照が異なる」「番地が異なる」という意味です。問題文に置き換えると、「originalArray
の中から2以外
の要素だけを集めた、originalArray
とは番地の異なる配列を返す」ということです。
重要なのは、元のoriginalArray
の番地には何もしていない、ということです。ですから、originalArray
は元の[1, 2, 3, 4]
のまま、変わっていません。元の番地はそのままにして、新しい番地をfilter
するときに割り当てているのです。
このように、元の配列を変えずに新しい配列を返すメソッドを非破壊メソッド
といいます。
問7
const originalArray = [1, 2, 3]
const state = {
array: originalArray
}
originalArray.push(4)
state.array = originalArray.filter(v => v !== 2)
console.log(state.array) // 問5: [1, 3, 4]
console.log(originalArray) // 問6: [1, 2, 3, 4]
console.log(originalArray === state.array) // 問7
集大成です。state.array
には、originalArray
をfilter
した新しい配列が入っています。ということは、番地も新しくなっているということです。
ですから、originalArray === state.array
はfalse
になります。参照が変わってしまいましたからね。
おまけの追加問題
理解度チェックのために、追加問題を出します。分かりますか?
const originalArray = [1, 2, 3]
const state2 = originalArray.filter(v => v)
console.log(originalArray) // => [1, 2, 3]
console.log(state2) // => [1, 2, 3]
console.log(originalArray === state2) // 追加問題
解答
const originalArray = [1, 2, 3]
const state2 = originalArray.filter(v => v)
console.log(originalArray === state2) // =>false
この2つの配列は、入っている要素の値は同じですが、参照が異なります。filter
は「新しい配列」を返すのでしたね。
まとめ
どうだったでしょうか。「参照」と仲良くなれましたか?
より深く学びたい人は、以下のようなキーワードで検索してみてください。
「Promiseを完全に理解できるかもしれないクイズ」もあります。よろしければぜひ。