QA@IT

JavaScriptで配列をループで処理するベストな書き方は?

122619 PV

JavaScriptで配列をループを使って処理する場合、

  • for...in
  • for
  • ネイティブのArray.forEach()
  • jQuery.each()
  • Underscoreなどの _.each_.mapなど
  • Lo-Dashの _.each_.map など

などが使えるかと思います。何を使えばいいのかよく分からなくなりました。基準や注意点などがあれば教えて下さい。

以下、自分で調べたところを書いておきます。

まず、 for...in は、

  • オブジェクトのenumerable属性がtrueなプロパティを列挙するので、Array.prototypeにプロパティが追加されていたら、それもイテレートしてしまう
  • 順序が保証されない
  • 遅い

という問題があるので配列のイテレート用途としては使わないほうがいいと理解しました。

まず、1点目、よく分からないのは、jQueryとUnderscore.jsなどを併用している場合に、どちらのeachを使うべきかという点です。それなりの規模のプロジェクトならルールに従え、そうでなければ「気にすんな、好きにしろ」が答えかなと想像しつつ、どうもスッキリしません。

Underscore.jsを使うなら、Underscoreのほうがよりリッチなコレクションの機能が使えるので、Underscoreで統一しておけばいいか、と思っていますが、パフォーマンスなどに違いはあるでしょうか?

もっと分からないのは、Array.forEach と for です。

Underscoreなど一般にpolyfillっぽいことをやるユーティリティJavaScriptライブラリは、ネイティブのArray.forEach関数があれば、それを使い、そうでなければ、forで展開した実装にフォールバックするということになっていると思います。

「それは理想的。悩みは解決じゃん!」と素朴に思っていたのですが、実はネイティブのforEachはforを回すよりも遅いというではありませんか。

考えてみたら、forEachでは関数を渡していて、これがイテレーションのたびに毎回invokeされるからその分がオーバーヘッドで遅いのかな、というのが私の想像です(関数型とか抽象度の高いループを提供するRubyのイテレータとかも全部そう? 遅い?)。さらに、関数リテラルで関数を渡すとしたら、イテレーションのたびにスタックを作って壊してというだけでなく、関数オブジェクトも作ることになるから遅いのかな? というのが薄っすらとした私の想像です。

Underscoreからフォークした「Lo-Dash」というユーティリティライブラリは、API的にUnderscoreの上位互換を維持したまま、中の each や map をシンプルな forに展開して書くことで、Undersocre(ネイティブのforEach)の数十%から2倍といったパフォーマンスを叩き出しているようです。

配列のイテレーションがパフォーマンスに影響するようなケースでない限り(ゲームとグラフィックスとか?)、使いやすく、読みやすいものを使っておけばいいだけという話かもしれませんが、最速なのはどういう書き方だろうかと気になりだしました。

それで検索してみたら、「Speed Up Your JavaScript」という2009年のGoogle Talkが出てきて、さらに、よく分からない話が出て来ました。

まず、

var arr = new Array(1000000);
for (var i = 0, len = arr.length; i < len; i++) {
    process(arr[i]);
}

という良くあるイディオムです。forの初期化で配列長をキャッシュしていますが、これは、いまどきのJavaScriptエンジンだと自動で最適化されるので、こんなことを明示的に書かなくてもいいという話も聞きましたが、そういうものでしょうか?

もう1つ。このループは、まだ速くできるというのですね。というのも、i < len のところで、

  • i < len
  • (i < len) == true

の2つの評価が起こっていて、これは1つにできるからです。より速い書き方は、

var i = arr.length;
do {
    process(arr[i]);
} while (i--);

だというではありませんか。同じことですが、

for (var i = arr.length; i--; ) {
    process(arr[i]);
}

と書くことで、これで50%ほど高速化するという話です。processが重たければ当然差はでないでしょうけど。

……というように、あれこれ見てたら、「結局オレは何をどうすりゃええんや……」と分からなくなってしまいました。単純に、

for (var i = 0, i < arr.length; i++) {
    process(arr[i]);
}

と書いて、後は悩まなくて良し、mapやselectしたければ、Underscoreとかでいいんじゃない? ということでいいのでしょうか?

色々質問文に入れ込んでしまいましたが、「こうしている」「こうしたほうがいい」「そこの理解は違う」「こう考えたほうがいい」などあれば、是非教えて下さいませ。

回答

「基本はArray.prototype.forEachを使う。ただし、Array.prototype.forEachは古いブラウザで利用できないので、古いブラウザに対応したコードを書く必要があるのならUnderscore.js、Lo-Dash等のライブラリの関数を使う(あるいはそのような関数を自作する)」というようなスタンスがベターかなと思います。

for文については、ただ単に速いだけであればよかったのですが、現行のJavaScriptにはブロックレベルのスコープが無いため、ループ内の変数が外から見えてしまうという大きな欠点があります。このため、通常の配列のループ処理であれば、for文よりもforEachのような関数を用いることのほうが好まれているように思います。これはRubyでfor式よりもブロック付きメソッドのほうが好まれるのと同じですね。特にJavaScriptでは変数宣言のホイスティングによって変数のスコープが関数内全体にまで広がってしまうので、for文を用いる場合のループ内変数のスコープについてはより注意深く考える必要があります。

forEachは遅いという話については、あくまでこれはfor文と比べればという話であり、普通の配列処理であればforEachを用いるのでも十分に高速なので気にする必要はないと思います。

関数の生成や呼び出しのオーバーヘッドが気になるいう話もありますが、これも微々たるものなので気にする必要はないですし、むしろ気にするべきではないと思います。

JavaScriptは関数オブジェクトが無いと生きていけないような言語です。関数の動的生成を利用してコールバックや高階関数を多用するというのがJavaScriptらしいスタイルであるといえます。なので、よほどの事情や状況でもない限り、関数の生成や呼び出しのコストのことは考えてはならないと思っておくのがいいと思います。


どのライブラリの関数を使えばいいかについては、どれも大して違わないのでどれでもいいと思います。せいぜい速度がほしいならLo-Dashを使うのがいい(それ以上速度が必要なら諦めてfor文を使う)というぐらいです。

jQueryのeachはネイティブのforEachとは大分動作が違うのでここでは特に説明しません。

Underscore.jsやLo-DashのUnderscore互換バージョンのforEach

  • 配列中のプロパティがセットされていない要素も巡回する(たとえば[1, , , 4]の2、3番目の要素やnew Array(100)の全要素など)
  • lengthプロパティがNumberでないオブジェクトを渡すと別の動きをする

など、ネイティブのforEachと細かいところで少し違った動作をする点があるようなので注意が必要かもしれません。


パフォーマンスについては、もし速度が必要なら実際に実行時間を計測してみるほかないと思います。ひとつ注意点をあげるとするならば、ある意味では当たり前のことですが「ブラウザ付属のJavaScript用コンソールにコードを貼り付けて実行した結果をベンチマークとして信用しない」ということです。

たとえば次のようなコード:

console.time('1');
function foo() {
  var arr = new Array(1000000);
  for (var i = 0; i < arr.length; i++) {
      arr[i] = arr[i] || i;
  }
}
foo();
console.timeEnd('1');

console.time('2');
var arr = new Array(1000000);
for (var i = 0; i < arr.length; i++) {
    arr[i] = arr[i] || i;
}
console.timeEnd('2');

このコードをいろいろなブラウザのJavaScript用コンソールに貼り付けて実行してみると、多くのブラウザでは('1')の方が大幅に実行時間が短くなります。しかし、このコードをHTMLファイルのscript要素から読み込んだり、直接埋め込んだりした場合は('1')も('2')も実行時間はほとんど同じになります。

このように、JavaScriptの処理系の最適化のトリガはいろいろなところにあるため、ある状況で実行速度の速かったものが別の状況ではそれほど速くならないというようなことがよくあります。なので手元での簡易なベンチマークの結果はあてにせずに、きちんと実際にコードが動いている環境で性能を測り、パフォーマンスを改善していくということが大切になります。

そしてもちろんブラウザの種類によっても最適化のかかり方が異なるため、あるブラウザでパフォーマンスを向上できても、別のブラウザでも同じようにパフォーマンスが向上するとは限らない、ということを頭に置いておくことも大切です。

編集 履歴 (3)
  • 非常に丁寧なご回答、ありがとうございます。全体の本当の環境で測れ、そうしたら違いはないというのに納得です。そもそもネットワークやDOM操作に比べたら誤差ですよね。forのフラットなスコープで気になることはありませんでしたが、そこも大きな考慮点ですね。 -

基本はforじゃないですかね
自分の感覚ではインデックスを利用することが多いので

利用しないのならforeachも素敵ですね

編集 履歴 (0)

通常用途には forEach を、特殊用途には for をと使いわけるのがよいと思います。

通常用途(単純に各要素を列挙するだけ)の場合、forEach の方がシンプルで使いやすいです。
インデックス変数のインクリメントなど余計なことを考える必要がないですし、
なによりループ内で添字によるアクセスが必要なくなるので見通しが良くなります。
速度が気になるようですが、forとforEachの差はせいぜい10倍くらいです。
通常の用途ではほとんど気にならない速度差だと思います。

一方、複雑に break/continue したい場合や、1つおきに要素を列挙したい場合など、
少し込み入った処理をしたい場合には自由度の高い for に軍配が上がります。
forEach で出来ないということはないですが、逆に冗長なコードになったり
読みにくいコードになったりします。

また、forEach だけでなく、Array#every, Array#some, Array#filter, Array#map, Array#reduce
など特定の用途の場合に効率的にかけるメソッドもありますので、
そちらを使うことを検討することも重要です。

その上で、Hiraku 10 さんの補足をさせていただきたいと思います。

break, continue について

forEach での break は確かにできませんが、Array#some を使うことで擬似的に再現できます。
多少コードが読みにくい(初見で何をしているのか分かりにくい)のが欠点なので、
try ... catch を使ってゴチャゴチャするよりはましというくらいであり、
複雑なものは素直に for を使ったほうがよいと思いますが。

例えば、回答中の例を書き換えてみると、以下のようになります。

//forEachでbreak
[1, 2, 3].some(function(e, i, ar){
   if(e === 2) return true;
   console.log(e);
});
// 2でループから脱出する
//forEachで多重continue
var arr1 = [1,2,3];
var arr2 = [2,4,6];

arr1.forEach(function(e){
   if(
      arr2.some(function(v){
         if((e + v) === 5) return true;
         console.log(v);
      })
   ) return;

   console.log(e);
});

forEach 内の this の値について

ご存じかもしれませんが、forEach の第二引数によって関数内での this の値を指定できます。
forEach の中と外で this の値が違うことによるメリットはあまり無いので、
常に指定しておいても問題はないかと思います。

最後に、JavaScript の実行速度についてベンチマークを取りたい場合には、
http://jsperf.com/ を使うと便利です。
既存のベンチマークについては http://jsperf.com/search から検索できます。

編集 履歴 (0)

.forEach()が優勢っぽいので反論を書きます。

疑問メモ: JavaScriptで配列やオブジェクトのキーを反復するイディオム - 虎塚
この辺で似たようなコメントを書いたことがあるのですが、

forと.forEach()を比較すると、 .forEach()の方が機能面で劣っています 。この点を注釈せずにforEachを勧めるのはフェアじゃないと思います。

breakとcontinue

.forEach()はbreakやcontinueが使えません。
returnでcontinue相当のことができますが、break相当のことを行うには例外を投げる必要があります。

//forEachでcontinue

[1,2,3].forEach(function(v){
  if (v == 2) return;
  console.log(v);
});
// 2のときだけ処理がスキップされる

//forEachでbreak
try {
  [1,2,3].forEach(function(v){
    if (v == 2) throw v;
    console.log(v);
  });
} catch (e) {}
// 2でループから脱出する

多重breakと多重continue

ループが入れ子になった場合、多重にループを脱出したり、外側のループでcontinueする機能がfor文にはあります。しかしforEachにはありません。例外を使うと再現できなくはないですが、tryとreturnとthrowが入り乱れてひどいことになります。

//forEachで多重continue
var arr1 = [1,2,3];
var arr2 = [2,4,6];

arr1.forEach(function(v1){
  try {
    arr2.forEach(function(v2){
      if (v1 + v2 == 5) throw arr1;
      console.log(v2);
    });
  } catch (e) { return; }

  console.log(v1);
});
//これが読みやすいか?

//forで同等のことを書くなら、↓で済む
outer: for (var i=0,l1=arr1.length; i<l1; i++) {
  for (var j=0,l2=arr2.length; j<l2; j++) {
    if (arr1[i] + arr2[j] == 5) continue outer;
    console.log(arr2[j]);
  }

  console.log(arr1[i]);
}

私の主張

forは記法が冗長ですが複雑な制御が可能です。何らかのアルゴリズムを実装するような場合には必要になるでしょう。

forEachは簡潔で読みやすいですが、複雑な制御には向いていません。また、関数のコンテキストが変わるという性質から、ブロック内でreturn構文の意味やthisキーワードに影響がある点にも注意しなければなりません。

読みやすさと機能を天秤にかけて、適切な方を選ぶのが正解だと思います。
…もっとも、実際にはforEachで充分なことが多いとは思いますが。

[].forEachを使うべきかライブラリのイテレータメソッドを使うべきか

現状、何の前提もなしにあらゆる環境で使えるforEachは存在しません。プログラムが何を前提にできるか(要件)を考えれば、ある程度使うべきメソッドは絞られると思います。

■ jQueryプラグインならjQueryを使うべき
もし「jQueryプラグイン」を作るという要件なら、$.eachを使うべきです。[].forEachは古いIEで実装されていないため、別途shimライブラリが必要になります。「jQueryプラグインなのに、jQuery以外に無駄に別のライブラリが必要」というのは不便です。

これはjQuery以外の場合でも同じで、ライブラリに依存してよいかどうかはプログラムの要件ですので、要件と照らし合わせて考えればいいでしょう。

■ 選べる場合は[].forEachがおすすめ
特に制約がなく何を選んでもよいのなら、shimなどを併用しての[].forEachを薦めます。
標準と言うのもありますが、ネイティブ実装は最適化により高速になる可能性があるという点もあります。

編集 履歴 (0)

for文はスコープを生成しない、forEachの実行速度が問題になることはほぼない、という点からArray.prototype.forEachを使用すればいいと思います。この点はi09158knctさんがすごい詳しく解説されてますね。

Array.prototype.forEachはIE8以下のブラウザには実装されていませんが、古いブラウザでも使用したい場合はes5-shimというライブラリを利用するのがオススメです。

es5-shimはECMAScript5のメソッドを実装していないブラウザでも使えるように出来るライブラリで、Array.prototype.forEachだけでなくArray.prototype.map、Array.prototype.filterなども使えるので便利です。

私はできるだけブラウザ標準で実装されたものを使用したいので、よくes5-shimを使ってます。

編集 履歴 (2)
  • 「できるだけ標準で」という方針、なるほどです。ありがとうございます。 -
ウォッチ

この質問への回答やコメントをメールでお知らせします。