はじめに
JavaScript ネイティブのメソッドには JavaScript エンジンごとの微妙な違いによってクロスブラウザではないものもあります。
apply
もその一つです。
ここでは、apply
で利用されることの多い Array.prototype.push
, Array.prototype.unshift
, Math.max
, Math.min
を対象に、なぜ利用しないほうが良いのかを書きます。
RangeError を投げる実装
Google Chrome 15 と Safari 5 では apply
の第二引数の length
が規定値以上になったところで RangeError
例外を投げます。
(この規定値は手元の Google Chrome 15 では 130,827 個、 Safari 5 では 65,537 個 でした。)
なお、ここでは RangeError
を投げる仕様がただしいかどうかはこの場では問題にしません。
速度
一般的にネイティブ実装の方が高速であると考えがちですが、速度を測ってみると必ずしもそうではないケースがあります。 以下の jsdo.it のコードはここで対象とするメソッドの実行速度を計測します。
for-loop vs apply (push, unshift, max, min) - jsdo.it - share JavaScript, HTML5 and CSS
Google Chrome 15
Google Chrome 15 で実行した結果、以下のような結果になりました。
for-loop | apply | etc1 | etc2 | |
---|---|---|---|---|
push | 16ms | 20ms | - | 18ms |
unshift | 8466ms | 21ms | 79ms | 19ms |
Math.max | 6ms | 20ms | - | - |
Math.min | 4ms | 24ms | - | - |
各項目の説明については jsdo.it の説明(とコード)を読んでいただくとして、unshift
を除いた結果では for-loop による実装の方が高速に動作しています。
もちろん、計測誤差もあるので必ずしも for-loop にすることで高速になるとは言えませんが、少なくとも apply
から for-loop の実装に変えたところで誤差レベルの損失であると言えます。
Safari 5
Safari 5 で実行した結果、以下のような結果になりました。
for-loop | apply | etc1 | etc2 | |
---|---|---|---|---|
push | 265ms | 174ms | - | 521ms |
unshift | 614ms | 213ms | 837ms | 579ms |
Math.max | 27ms | 105ms | - | - |
Math.min | 31ms | 161ms | - | - |
Array.prototype.push
と Array.prototype.unshift
に関しては apply
が最速で、それ以外は若干微妙な気がします。
ただ、Safari 5 において RangeError
が出るサイズは 65,537 以上と Chrome よりも出やすい環境なので、
やはり unshift
以外は for-loop で unshift
は 分割 apply
が良さそうです。
どうするべきか
Array.prototype.push
, Math.max
, Math.min
に関しては for-loop による実装におきかえる事で、RangeError
例外の発生を考慮しないで済むようになります。
Array.prototype.unshift
に関しては for-loop にするととてつもなく遅くなることが前述の計測結果からわかりますので、適切なサイズのブロックに分けて apply
を繰り返すという方法が良いと思います。
この際、ブロックサイズが適切じゃなかった( RangeError
がでてしまう)事態に備えて、以下のようなコードにするのが良いのではないでしょうか。
( * jsdo.it の unshift: etc2 のコードです)
function unshift_(dst, src) {
var buffer = 0x8000, blocks, complete = false;
while (!complete) {
try {
// block
blocks = [];
for (i = 0, l = src.length; i < l; i += buffer) {
blocks.push(src.slice(i, i + buffer));
}
// unshift
for (i = blocks.length - 1; i >= 0; i--) {
Array.prototype.unshift.apply(dst, blocks[i]);
}
complete = true;
} catch (e) {
if (e instanceof RangeError) {
buffer >>>= 1;
} else {
throw e;
}
}
}
return dst.length;
}
まとめ
apply
を使う際に、第二引数に大きな配列が来る可能性のある場合は RangeError
例外を考慮したコードにした方が良い。
unshift
以外では for-loop に置き換えるのがオススメ。
unshift
の場合は分割して apply
するのがオススメ。
補足
Google Chrome と Safari での計測結果は各手法の差異を計るための物で、Chrome と Safari の実行条件(配列サイズと実行回数)は異なっています。