8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Linkbal(リンクバル)Advent Calendar 2022

Day 20

JavaScriptの参照を"完全に理解"できるかもしれないクイズ

Last updated at Posted at 2022-12-24

配列をコピーすると毎回バグる人へ

みなさん、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を入れています。
このoriginalArraystate.arrayが「等しい」のかどうか?というのが①の問題です。

まず、originalArraystateの番地は異なります。この2人は違う番地に住んでいるのです。

では、state.array、つまり、stateに格納されたarrayはどうでしょうか。面白いことに、state.arrayの番地は、originalArrayと同じになるのです。こういうケースを、

  • state.arrayoriginalArrayを参照している
  • state.arrayの参照・参照先はoriginalArrayである

などと言います。

JavaScriptの===は、「参照が同じかどうか」、つまり「番地が同じかどうか」をチェックしている(注)ので、originalArray === state.arraytrueになります。

注) オブジェクトの場合。参考サイト: 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の番地は変わらずでしたから、依然としてoriginalArraystate.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に、originalArrayfilterした結果を入れている

という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には、originalArrayfilterした新しい配列が入っています。ということは、番地も新しくなっているということです。

ですから、originalArray === state.arrayfalseになります。参照が変わってしまいましたからね。

おまけの追加問題

理解度チェックのために、追加問題を出します。分かりますか?

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を完全に理解できるかもしれないクイズ」もあります。よろしければぜひ。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?