はじめに

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.pushArray.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 の実行条件(配列サイズと実行回数)は異なっています。