JavaScript

飛び飛びArrayのイテレート

More than 1 year has passed since last update.

ブラウザのコンソールで直接JavaScriptをゆるゆるいじってるときに、
range関数が欲しいなーと思って、さくっとnew Array(length).mapとやったら失敗。

// こういうのが欲しい
// range(10);
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

function range(num){
  return new Array(num).map(function(_, i){
    return i;
  });
}

range(10);
// => [undefined × 10]

???

なんで?どうして?教えてMDN!

map は、与えられた callback 関数を配列の各要素に対して順番に一度ずつ呼び出し、その結果から新しい配列を生成します。callback は値が代入されている配列のインデックスに対してのみ呼び出されます。つまり、すでに削除されたインデックスや、まだ値が代入されていないインデックスに対しては呼び出されません。
Array.prototype.map() - JavaScript | MDN

はえー。
「インデックスが飛び飛びのArrayオブジェクト」はイテレートも飛び飛びになるようだ。
for文と食い違って混乱するから使わないほうが無難だね。
(というより、そもそも飛び飛びのArrayオブジェクトはあまり使わない方が良いのだろうが。)

// undefinedのとこだけ(undefinedという)「値がある」状態。
// 他は「値が無い(undefined)」状態。
var arr = [,,,,,,,,undefined,,,,,,,,]; 

// lengthも適当に入れてみる
arr.length = 100;

// イテレートしても呼ばれるのは一回だけ!
arr.forEach(function(value, index){
  console.log(value + ',' + index);
});
// => undefined,8

// もちろんfor文なら全部回る
for(var i=0, len=arr.length; i<len; i++){
  console.log(arr[i] + ',' + i);
}
// => undefined,0
// => undefined,1
// => ....
// => undefined,99

「飛び飛びのArrayオブジェクト」から「みっちり詰まったArrayオブジェクト」に変換するには
Array.fromが使えた。(非対応ブラウザではPolyfillが必要)

Array.fromはArrayのようでArrayではない、ArrayもどきオブジェクトをArrayにする関数。
argumentsやNodeListでArray.prototypeの関数使いたいときによく使う。
これでnew ArrayしたオブジェクトをArrayに変えられる。

new Array(length)はArrayもどきオブジェクトだった...?

// コンソール上の表示も変わる
new Array(3); // => [undefined × 3]
Array.from(new Array(3)); // => [undefined, undefined, undefined]

Array.from(new Array(10)).map(function(_, i){
  return i;
});
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// lengthさえあればOK
Array.from({length:10}).map(function(_, i){
  return i;
});
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

なお、argumentsのArray化でよく見かけるArray.prototype.slice.callは、
飛び飛びのArrayに対しては使えないようだ。

Array.prototype.slice.call(new Array(10)).map(function(_, i){
  return i;
});
// => [undefined × 10]

rangeの作り方調べてたら、さらなる黒魔術もあるのを知った。なにこれ怖い!

Array.apply(null, {length: 10}).map(Number.call, Number);
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

ちなみに、数字をpushなりして作る方がはるかに速いので、
真面目にrange使うなら普通に作りましょーね。(台無し)

追記:
Spread operator使ってもみっちり化できた。だからどーした!

(function(...arr){
  return arr.map(function(_, i){
    return i;
  });
}).apply(null, {length: 10});
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]