suinさんのこちらの記事の補足的な内容になります。
JavaScriptで[ 0, 1, 2, 3, 4 ]のような連番の配列を生成する方法
JavaScriptで連番の配列を作りたいときこんなふうにできるよ、という記事です。
[...Array(5)].map((_, i) => i)
// [ 0, 1, 2, 3, 4 ]
初見だとちょっと何やってるかわからない…特にわからなかったのが以下の部分。
[...Array(5)]
なんで配列作って展開してまた配列に詰めなおしてるの…?
元記事の解説
この謎の操作の意図は元記事できちんと説明されています。
パート1: Array(5)
これは要素数が5つ(=スロットが5つあるだけ)の空っぽの配列を作る。undefinedが5つ入るわけでない。
Array(5) //=> [ <5 empty items> ]
わかります。
Array()
の機能そのまんまです。
###パート2: [...Array(5)]の[... ]の部分
要素数が5つある配列をspread operatorにかけて、undefined5つの配列に変形している。
[...Array(5)] //=> [ undefined, undefined, undefined, undefined, undefined ]
なるほど、空っぽのスロットをundefined
で埋めるためだったんですね。
Array.prototype.map()
は値の入っていない空き枠は処理せずにスルーしてしまうので、何かしらの値を入れておかねばならないってことですね。
でも待って、なんでこれで空き枠埋まるの?
「この程度説明するまでもない」的な?
この記事のポイントはここです。
「なぜスプレッド構文1で空き枠を埋められるのか?」
QiitaでもQiita以外でもこのイディオムを解説している記事は多数ヒットするのですが、空き枠が埋まる理由を説明している記事は見つかりませんでした2。
連番配列作るイディオムがわからんので読解してみたよ、という内容の記事もあったのですが、この点については言及なし。
なんでみんなスルーするの…
なぜスプレッド構文で空き枠を埋められるのか?
みんなが当たり前のようにスプレッド構文をスルーしていく中、ただ一人動作機序が理解できない…
悔しかった私はからくりを調べました。
スプレッド構文は何をしているのか
スプレッド構文は、配列などの反復可能オブジェクトを展開する機能を持ちますが、この機能は反復処理プロトコルを利用して実現されています。
この反復処理プロトコルは、スプレッド構文やfor-of文など、オブジェクトを反復する場面で使われる共通の3しくみです。
[...iterableObj]
のような式を例にざっくりと流れを説明すると、
- まず対象のオブジェクトの
@@iterator
メソッドが呼ばれます。例の場合ではiterableObj[Symbol.iterator]()
のような呼び出しになります。 - 対象の
@@iterator
メソッドは反復子と呼ばれるオブジェクトを返します。 - 反復子は
next
メソッドを実装しています。反復子.next()
という呼び出しを繰り返すことで対象のオブジェクトが反復されます。 -
next
メソッドは、呼び出されるたびにvalue
とdone
というプロパティを持つオブジェクトを返します。value
は反復子によって返される具体的な値で、done
は反復が完了したかを示す真偽値です。done
がtrue
になったら反復は終了します。 - 最終的に、
done
がtrue
になるまでの各value
を要素とする配列が返されます。
…という感じ。
ちょっとややこしいですが、ともあれスプレッド構文はこういう仕組みのもとで動いているわけです。
[ 0, 1, 2, 3, 4 ]
を手動で反復してみる
スプレッド構文の内部動作を考えるため、まずは手動で[ 0, 1, 2, 3, 4 ]
を反復してみます。
まず@@iterator
メソッドを呼んで反復子をゲットしましょう。
const arr = [ 0, 1, 2, 3, 4 ]
const iter = arr[Symbol.iterator]()
ではこの反復子のnext
メソッドを呼びます。
iter.next() //=> { value: 0, done: false }
iter.next() //=> { value: 1, done: false }
iter.next() //=> { value: 2, done: false }
iter.next() //=> { value: 3, done: false }
iter.next() //=> { value: 4, done: false }
iter.next() //=> { value: undefined, done: true }
done
がtrue
になったので反復は終了。実際に反復で返された値はvalue
を参照すればOK。
ややこしく感じたわりに意外とシンプルですね。
問題のArray(5)
を手動で反復
それでは本丸のArray(5)
の攻略です。
先ほどと同じ要領で反復してやりましょう。
const arr = Array(5)
const iter = arr[Symbol.iterator]()
iter.next() //=> { value: undefined, done: false }
iter.next() //=> { value: undefined, done: false }
iter.next() //=> { value: undefined, done: false }
iter.next() //=> { value: undefined, done: false }
iter.next() //=> { value: undefined, done: false }
iter.next() //=> { value: undefined, done: true }
出た、undefined
だ!
空き枠の部分のvalue
がundefined
として取得されています!
Array(5)
をスプレッド構文にかけたとき、このundefined
が要素になっていたんですね。
[...Array(5)] //=> [ undefined, undefined, undefined, undefined, undefined ]
そのundefined
はどこから?
じゃあなんで空き枠に対応するvalue
がundefined
として返されるの?
スロットは空っぽで何の値も入ってないのに?
JavaScriptでは、返す値がそもそもないときは便宜上undefined
を返すことが多いので、たぶん今回も同じノリなんでしょう。
…というふわふわした結論では気持ち悪かったので、仕様書を読んでみました。
仕様書によると、配列の反復子は、next()
するたびにarr[0]
arr[1]
arr[2]
…のように4順番に要素へアクセスして、返ってきた値をvalue
にセットしているようです。
空き枠だろうが何だろうがとにかくインデックス0から一つずつ参照してvalue
に放り込むのです。
JavaScriptでは値が設定されていないプロパティ(≒空きスロット)を参照するとundefined
になりますので、このundefined
がvalue
に入り、最終的に配列の要素になっていた…というのが真相のようです。
結論
スプレッド構文で配列の空き枠をundefined
で埋められる理由は、配列の反復子がundefined
を返すからで、そのundefined
は値のないプロパティにアクセスした結果でした。
そして、幾多の記事がこの点を解説していないのは反復子とかの説明が面倒だっただけなんじゃないか説が浮上しました。