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は値のないプロパティにアクセスした結果でした。
そして、幾多の記事がこの点を解説していないのは反復子とかの説明が面倒だっただけなんじゃないか説が浮上しました。
元記事ではspread operator(スプレッド演算子)と書かれていますが、この...は仕様書では演算子とされていませんので、スプレッド構文と呼ぶことにします。 ↩
軽く調べただけなので、どこかにあるのかもしれません。 ↩
「共通の」というのはつまり、文字列でも配列でもその他のオブジェクトでも、このプロトコルに準拠してさえいればスプレッド構文やfor-of文などで同じように反復できるということです。 ↩
実際にはプロパティアクセサーではなく、[[Get]]内部メソッドというものが使われています。 ↩
↧