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(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 |
ここまでやって、ふと気が付いた
やりたかったのって、
だったのですが、
その問題そのものは、
(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 では、この辺が修正されてますね
- [0,,2] のように省略記法で生成した配列は Array.map で期待した結果が得られない。
- IE
- Array(n) で生成した配列は Array.map で期待した結果が得られない。indexOf, lastIndexOf, every, some, forEach, filter, reduce, reduceRight も同様にダメ。
- 要素数 n の密な配列が欲しい場合は、for(;;) ループで回しましょう。
- もし IE9 があるなら、ECMAScript 5th 相当の機能を入れてくるだろうから、この辺の仕様は大改編されるかも。
調査コストは4時間。