JavaScriptのnew Array(n)をmapしたいとき fillをはさむ理由

経緯

配列をオブジェクトで初期化したい場合、

new Array(3).fill({foo: "ふう", bar:"ばあ"})

というコードだと、全てのインデックスが同じオブジェクトを指してしまうので、 調べたら

new Array(3).fill().map(() => ({foo: "ふう", bar: "ばあ"}))

という方法が出てきたので試したらうまく行きました。

疑問

このfill()要らなくない?new Array(3).map(() => ({foo: "ふう", bar: "ばあ"})) って書けばよさそうな気がします。

答え

fill()は必要。

理由はここに書いてありますが、英語なので日本語で&自分で実行しながら書いていきます。

itnext.io

まず、JavaScriptの配列は、実質的には数値をキーとしたオブジェクトです。

例えば、以下のように配列を初期化します。

const array = ['りんご', 'みかん', 'バナナ']

console.log("array", array)

これを実行すると以下のようになります。

f:id:yucatio:20190407105938p:plain

{
  0: "りんご"
,
  1: "みかん"
,
  2: "バナナ"
,
  length: 3

}

上記配列はこのオブジェクトを宣言したのと同じになります。

さて、今度は今度はArrayのコンストラクタを使った場合をみてみます。

const array = new Array(3)

console.log("array", array)

実行結果です。

f:id:yucatio:20190407110103p:plain

{
  length: 3

}

この場合はlengthのみ含まれているオブジェクトになっています。数値のキーはありません。

コンストラクタで作成された配列に対して、map()を呼び出してみます。

const array = new Array(3).map(() => ({foo: "ふう", bar: "ばあ"}))

console.log("array", array)

実行結果です。

f:id:yucatio:20190407110103p:plain

{
  length: 3

}

何も設定されていません。mapに渡したコールバックが呼ばれていないことがわかります。

公式ドキュメントのArray.prototype.map()のページには、下記のように書いてあります。

callback は、値が代入されている配列のインデックスに対してのみ呼び出されます(undefined が代入されているものも含みます)。すでに削除されたインデックスや、まだ値が代入されていないインデックスに対しては呼び出されません。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map

つまり、new Array(3)だけした状態でmapを呼び出しても、インデックス(オブジェクトのキー)がないので1度もコールバックが呼ばれないのです。 この挙動はforEachやreduce、filterなども同様です。

そこで、先人の教えの通り、一旦fill()します

const array = new Array(3).fill()

console.log("array", array)

実行結果です。

f:id:yucatio:20190407110128p:plain

{
  0: undefined
,
  1: undefined,

  2: undefined,
  length: 3
}

インデックスのキーが作成されました。fillに引数を渡していないので、各値はundefinedです。

この状態でmap()をすれば、各キーに対してコールバックが呼ばれそうです。

const array = new Array(3).fill().map(() => ({foo: "ふう", bar: "ばあ"}))

console.log("array", array)

実行結果です。

f:id:yucatio:20190407110144p:plain

{
  0: {foo: "ふう", bar: "ばあ"},
  1: {foo: "ふう", bar: "ばあ"},
  2: {foo: "ふう", bar: "ばあ"},
  length: 3
}

配列に初期値を設定できました。

あとがき

オブジェクトで配列の初期化をする方法は複数ありますが、どれも初心者にとって直感的ではないのでなんとかならないかなと思います。