Array.forEach や Array.map は Firefox2〜3 や IE で利用できないケースがある

元々のタイトルは「要素数 n の密な配列を作ろうとして空回りした(JScript の Array をクロスブラウザ化)」だったんですが、ちょっと変えました。
Firefox2〜3 や IE6 〜 IE8 では、ECMAScript 5th で追加された Array.map などの便利メソッドの利用に制限があります。うっかりやっちゃうと、非常に面白くないことになります。

元々は、nanto_vi さんのつぶやきを見て、

@teramako Array.apply(null, Array(10)).map(function (e, i) i) とか。
http://twitter.com/nanto_vi/status/5014927085

そういえば以前、要素数 n の密な配列を作るのに Array.apply(null, Array(n)) が使えないかと思ったけど、JScriptでダメだったんだよね。
http://twitter.com/nanto_vi/status/3782301588

JScript? なして? とか思って調べ始めたのが発端です。

調査用のスクリプト(ちょっと長いのでよみとばし可)

調べてることは

  • Hash の in と Array の in は同じ?
  • 要素を delete で削除したり null や undefined を代入すると?
  • [0,,2] のように引数を省略すると?
  • Array(size) と Array(obj, obj, ...) は?
  • DontEnum属性とか付いてる? (属性そのものは見れないので、とりあえず列挙されなければ OK)
    • Array を for 〜 in でまわして調べてるので、Array.prototype を改変するタイプのライブラリ(Prototype.js, MooTools等)を読み込んでる環境だとアレなことに

です。

<!doctype html><html><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
<script>
window.onload = function() {
  test1();
  test2();
  test12();
}

function id(expr) {
  return document.getElementById(expr);
}

// Hash + in
function test1() {
  var rv = "",
      o1 = { 0: 0,            2: 2 },
      o2 = { 0: 0, 1: void 0, 2: 2 },
      o3 = { 0: 0, 1: void 0, 2: 2 }, // delete o3[1]
      o4 = { 0: 0, 1: void 0, 2: 2 }, // o4[1] = null
      o5 = { 0: 0, 1: void 0, 2: 2 }; // o5[1] = void 0

  delete o3[1];
  o4[1] = null;
  o5[1] = void 0;

  (1 in o1) && (rv += "o1, ");
  (1 in o2) && (rv += "o2, ");
  (1 in o3) && (rv += "o3, ");
  (1 in o4) && (rv += "o4, ");
  (1 in o5) && (rv += "o5, ");

  id("out").innerHTML += "<h4>Hash + in:</h4>" + rv;
}

// Array + in
function test2() {
  var rv = "",
      o0 = Array(3),
      o1 = [0,       , 2],
      o2 = [0, void 0, 2],
      o3 = [0, void 0, 2], // delete o3[1]
      o4 = [0, void 0, 2], // o4[1] = null
      o5 = [0, void 0, 2]; // o5[1] = void 0

  delete o3[1];
  o4[1] = null;
  o5[1] = void 0;

  (1 in o0) && (rv += "o0, ");
  (1 in o1) && (rv += "o1, ");
  (1 in o2) && (rv += "o2, ");
  (1 in o3) && (rv += "o3, ");
  (1 in o4) && (rv += "o4, ");
  (1 in o5) && (rv += "o5, ");

  id("out").innerHTML += "<h4>Array + in:</h4>" + rv;
}

// test DontEnum attribute
function test12() {
  var rv = "", i,
      o0 = Array(3),
      o1 = [0,       , 2],
      o2 = [0, void 0, 2],
      o3 = [0, void 0, 2], // delete o3[1]
      o4 = [0, void 0, 2], // o4[1] = null
      o5 = [0, void 0, 2]; // o5[1] = void 0

  delete o3[1];
  o4[1] = null;
  o5[1] = void 0;

  for (i in o0) { rv += i; } rv += ", ";
  for (i in o1) { rv += i; } rv += ", ";
  for (i in o2) { rv += i; } rv += ", ";
  for (i in o3) { rv += i; } rv += ", ";
  for (i in o4) { rv += i; } rv += ", ";
  for (i in o5) { rv += i; }

  id("out").innerHTML += "<h4>Array + in + DontEnum:</h4>" + rv;
}
</script>
</head><body><div id="out"></div></body></html>
結果

気になるポイントに色を付けておきました。

Hash + in

Browser o1 o2 o3 o4 o5
Safari3.2 x x x
Safari4 x x x
Chrome3 x x x
Chrome4 x x x
Opera9.2 x x x
Opera10 x x x
Firefox2 x x x
Firefox3 x x x
Firefox3.5 x x x
IE6 x x x
IE8 x x x

Hash はブラウザ毎の差が見られませんでした。

Array + in

Browser o0 o1 o2 o3 o4 o5 DontEnum
Safari3.2 x x x ---, 0-2, 012, 0-2, 012, 012
Safari4 x x x ---, 0-2, 012, 0-2, 012, 012
Chrome3 x x x ---, 0-2, 012, 0-2, 012, 012
Chrome4 x x x ---, 0-2, 012, 0-2, 012, 012
Opera9.2 x x x ---, 0-2, 012, 0-2, 012, 012
Opera10 x x x ---, 0-2, 012, 0-2, 012, 012
Firefox2 x x x x ---, 012, 012, 0-2, 012, 012
Firefox3 x x x x ---, 012, 012, 0-2, 012, 012
Firefox3.5 x x x ---, 0-2, 012, 0-2, 012, 012
IE6 x x ---, 0-2, 0-2, 0-2, 021, 021
IE8 x x ---, 0-2, 0-2, 0-2, 021, 021

Firefox

  • Firefox 3.5 は 3.0 までの実装と異なっています。
  • Firefix2, 3 は 省略記法 [0,,2] で生成した要素が 1 in [0,,2] で true になってしまうようです。

IE

  • 1 in [0, void 0, 2] が false になる
  • [0, void 0, 2] の 2番目の要素(void 0)に null や void 0 を代入し for 〜 in でループすると、他のブラウザと列挙される順番が異なる
    • ECMAScript の仕様上、for in で列挙される順番は「不定」なので、これは仕様通り

といった特徴が目立ちます。

Array.apply(null, Array(3)).join(",") を実行するとどうなるか

IE ",,"
IE以外のブラウザ ",,"

同じですね

じゃあ Array.apply(null, Array(10)).map(function(v, i){ return i; }).join(","); を実行するとどうなるか

予想では、"0,1,2,3,4,5,6,7,8,9" が取得できるはずです。

っとその前に、IE や ちょっと古めのブラウザには Array.prototype.map が実装されていないので、まずはそれを用意します。

// extend Array.map
Array.prototype.map || (Array.prototype.map =
  function(fn, fn_this) {
    for (var iz = this.length, rv = Array(iz), i = 0; i < iz; ++i) {
      (i in this) && (rv[i] = fn.call(fn_this, this[i], i, this));
    }
    return rv;
  });

実行すると…

Array.apply(null,Array(10)).map(function(v,i){return i}).join(",");

こうなります。

IE ",,,,,,,,,"
IE以外のブラウザ "0,1,2,3,4,5,6,7,8,9"

Array(10) で生成した [undefined, ...] な配列が、Array.map の (i in this) で false に評価されているのが原因で、IE では期待した結果が得られません。

ECMAScript 5th 互換の Array.prototype.map は in 演算子で回すのが MDC 的に正解のようなので、そこを改悪する訳にもいきません。

つまり IE で Array(10) として作成した配列は map に使えないということが分かります。

じゃあどうするか

IE なら配列の生成を自前で行うことで問題を解決できないか」と考えました。

window.xArray を追加し、Array.apply(null,Array(10)) を xArray.apply(null,Array(10)) に変えます。

// extend xArray
window.xArray = !document.uniqueID ? Array :
  (function(size) {
    var az = arguments.length,
        rv = (az === 1) ? Array(size) : [], i = 0;

    if (az > 1) {
      for (; i < az; ++i) {
        rv[i] = arguments[i];
      }
    }
    return rv;
  });


実行すると…

xArray.apply(null,Array(10)).map(function(v,i){return i}).join(","); → "0,1,2,3,4,5,6,7,8,9"

IE でも、やりたかったことができました。

xArrayのテスト
window.onload = function() {
  test3();
  test13();
}
function test3() {
  var rv = "",
      o0 = xArray(3),
//    o1 = xArray(0,       , 2),
      o2 = xArray(0, void 0, 2),
      o3 = xArray(0, void 0, 2), // delete o3[1]
      o4 = xArray(0, void 0, 2), // o4[1] = null
      o5 = xArray(0, void 0, 2); // o5[1] = void 0

  delete o3[1];
  o4[1] = null;
  o5[1] = void 0;

  (1 in o0) && (rv += "o0, ");
//(1 in o1) && (rv += "o1, ");
  (1 in o2) && (rv += "o2, ");
  (1 in o3) && (rv += "o3, ");
  (1 in o4) && (rv += "o4, ");
  (1 in o5) && (rv += "o5, ");

  id("out").innerHTML += "<h4>xArray + in:</h4>" + rv;
}
// test DontEnum attribute
function test13() {
  var rv = "", i,
      o0 = xArray(3),
//    o1 = xArray(0,       , 2),
      o2 = xArray(0, void 0, 2),
      o3 = xArray(0, void 0, 2), // delete o3[1]
      o4 = xArray(0, void 0, 2), // o4[1] = null
      o5 = xArray(0, void 0, 2); // o5[1] = void 0

  delete o3[1];
  o4[1] = null;
  o5[1] = void 0;

  for (i in o0) { rv += i; } rv += ", ";
//for (i in o1) { rv += i; } rv += ", ";
                             rv += "---, ";
  for (i in o2) { rv += i; } rv += ", ";
  for (i in o3) { rv += i; } rv += ", ";
  for (i in o4) { rv += i; } rv += ", ";
  for (i in o5) { rv += i; }

  id("out").innerHTML += "<h4>xArray + in + DontEnum:</h4>" + rv;
}

xArray + in

Browser o0 -- o2 o3 o4 o5 DontEnum
Safari3.2 x x x ---, ---, 012, 0-2, 012, 012
Safari4 x x x ---, ---, 012, 0-2, 012, 012
Chrome3 x x x ---, ---, 012, 0-2, 012, 012
Chrome4 x x x ---, ---, 012, 0-2, 012, 012
Opera9.2 x x x ---, ---, 012, 0-2, 012, 012
Opera10 x x x ---, ---, 012, 0-2, 012, 012
Firefox2 x x x ---, ---, 012, 0-2, 012, 012
Firefox3 x x x ---, ---, 012, 0-2, 012, 012
Firefox3.5 x x x ---, ---, 012, 0-2, 012, 012
IE6 x x x ---, ---, 012, 0-2, 012, 012
IE8 x x x ---, ---, 012, 0-2, 012, 012

# xArray は 省略記法( xArray(0,,2) ) が使えないので、o1 のテストはスキップしています。
若干テストケースが不足気味ですが、Array + in(↓) と比べると、それっぽく動いています。

Array + in (再掲)

Browser o0 o1 o2 o3 o4 o5 DontEnum
Safari3.2 x x x ---, 0-2, 012, 0-2, 012, 012
Safari4 x x x ---, 0-2, 012, 0-2, 012, 012
Chrome3 x x x ---, 0-2, 012, 0-2, 012, 012
Chrome4 x x x ---, 0-2, 012, 0-2, 012, 012
Opera9.2 x x x ---, 0-2, 012, 0-2, 012, 012
Opera10 x x x ---, 0-2, 012, 0-2, 012, 012
Firefox2 x x x x ---, 012, 012, 0-2, 012, 012
Firefox3 x x x x ---, 012, 012, 0-2, 012, 012
Firefox3.5 x x x ---, 0-2, 012, 0-2, 012, 012
IE6 x x ---, 0-2, 0-2, 0-2, 021, 021
IE8 x x ---, 0-2, 0-2, 0-2, 021, 021

ここまでやって、ふと気が付いた

やりたかったのって、

  • 素数 n の密な配列を作ろう
  • IE でも動くようにしよう

だったのですが、

その問題そのものは、

(function(z){for(var r=[],i=0;i<z;++i)r[i]=i;return r})(10).join(",");

これで十分だし、10倍は高速な気がするよ(in IE)

xArray を使ったコードって

xArray.apply(null,Array(10)).map(function(v,i){return i}).join(",");

2 byte 短いだけでパフォーマンスが悪すぎるし、xArray の説明で小一時間かかるのがちょっと。
「要素数 n の密な配列を作る」手段として、xArray は不向きですね。

まとめ(反省会)

  • Firefox2, Firefox3
    • [0,,2] のように省略記法で生成した配列は Array.map で期待した結果が得られない。
      • [0,,2].map(function(v,i){return i}) が [0,2] や [0,undefined,2] ではなく [0,1,2] になってしまう
      • ちなみに、map だけではなく、indexOf, lastIndexOf, every, some, forEach, filter, reduce, reduceRight もダメです。
      • Firefox3.5 では、この辺が修正されてますね
  • IE
    • Array(n) で生成した配列は Array.map で期待した結果が得られない。indexOf, lastIndexOf, every, some, forEach, filter, reduce, reduceRight も同様にダメ。
    • 素数 n の密な配列が欲しい場合は、for(;;) ループで回しましょう。
      • もし IE9 があるなら、ECMAScript 5th 相当の機能を入れてくるだろうから、この辺の仕様は大改編されるかも。

調査コストは4時間。