配列をコピーすると毎回バグる人へ
みなさん、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を完全に理解できるかもしれないクイズ」もあります。よろしければぜひ。