javascriptのfor-in文は、for-each文の代わりじゃない

例えばJAVAには、for-each文という便利な構文があります。

class Test {
  public static void main(String args[]) {
    int [] ary = new int[] { 1, 2, 3, 4, 5 };
    for(int num : ary) {
      System.out.println(num + ",");
    }
  }
}

実行結果

1,2,3,4,5,

これは拡張for文とも言われ、配列のすべての要素にアクセスすることができます。


Javascriptにも似たような構文のfor-in文があります。
が、このfor-in文実に曲者で、JAVAのfor-each文と同じ感覚で使うと、必ずどこかで痛い目を見ます。

例文を見てみましょう。

<html>
<head> 
<title>for inテスト</title> 
</head>
<body>
<ul id="fruits">
  <li>りんご</li>
  <li>みかん</li>
  <li>もも</li>
</ul>

<script type="text/javascript">
function test(){
  var cnt = 0;
  var li = document.getElementsByTagName('li');
  for (var i in li) {
  	cnt++;
  }
  alert(cnt);
}
test();
</script>

</body>
</html>

HTML中の<li>要素の数を表示するコードです。*1
実行してみましょう。

え、5?!
<li>要素の数は3つですが、なぜか5が表示されます。何故だ。。。orz

for-in文とは

for-in文は配列の反復処理をする構文ではなく、オブジェクトの反復処理を行う処理です。
この場合、getElementsByTagName()の戻すDOM Elementオブジェクトのすべての要素にアクセスします。
オブジェクトの中身を見てみましょう。

全部で5つのプロパティがあります。
0〜2が<li>のElement、lengthはNodeListの長さ、__proto__はプロトタイプオブジェクトです。
for-in文は、これらの要素に順番にアクセスします。
そのため先ほどは期待値の3ではなく、プロパティ数の5が表示されたのです。

この仕様を理解しないでfor-inを使うと、予期せぬ結果が表示されて危険です。

forEachメソッド

他に手は無いんだろうか。
Foreach文 - Wikipedia はてなブックマーク - Foreach文 - Wikipediaによると、

ECMA-262第5版 (JavaScript 1.6, ActionScript 3.0) からは、ArrayインスタンスにforEachメソッドが追加された。

らしいので、for-in文のところを以下のように変えて実行してみます。

li.forEach(function(element){cnt++;});

実行結果


>Uncaught TypeError: Object #<a NodeList> has no method 'forEach'
NodeListには使えませんでした。orz*2

for each-in文

次!
同じくwikipediaによると。

ECMA-262第5版からは、オブジェクトの値で反復処理をするfor each-in文が導入された(スペースで分けられていることに注意)。

for-in文を以下のように変えて実行します。

for each (var i in li) {cnt++;}

実行結果


Uncaught SyntaxError: Unexpected identifier
Chromeでは未実装?でした。orz

自分で何とかしてみる

javascript 1.6の新しいArrayメソッド « ku はてなブックマーク - javascript 1.6の新しいArrayメソッド « kuによると、forEachの他にもfilter、every、map、someなど色々あるようですが、すべて対象はArrayインスタンスとなっており、NodeListには使えません。
仕方が無いので、prototypeを拡張して自力で実装してみます。

Object.prototype.each = function(func){
  for (var i = 0; i < this.length; i++){
    func(this[i]);
  }
}

使い方

li.each(function(node){
  cnt++
});

これで結果は3になります。ヤッター。

。。。が、この方法は下策でしょう。
すべてのObjectが汚染され、for-inで列挙されるようになってしまいます。
prototype汚染は非常にデリケートな問題です。

結局

素直にfor文を使いましょう。

for (var i = 0; i < li.length; i++){
  alert(li[i].innerHTML);
}

はい解決。

for-in文がこの先生きのこるには

そんな配列やオブジェクトには使えないfor-in文ですが、そんな彼にも使い道があります。
連想配列です。
連想配列は添え字が数字ではなく文字の配列*3でlengthプロパティを持たないため、普通のfor文は使えません。
そういう場合はfor-in文を使います。

var test = {"hoge":"fuga","foo":"bar"};
for (var i in test){
  alert(test[i]);
}


参考
Foreach文 - Wikipedia はてなブックマーク - Foreach文 - Wikipedia
JavaScript の配列と連想配列の違い - IT戦記 はてなブックマーク - JavaScript の配列と連想配列の違い - IT戦記
javascript 1.6の新しいArrayメソッド « ku はてなブックマーク - javascript 1.6の新しいArrayメソッド « ku

*1:例なのでfor文使ってますが、実際はli.lengthで十分です。

*2:当然通常の配列には使えます。例:[1,2,3].forEach(function(num){c+=num;});

*3:正確には配列ではなくオブジェクトですが、ここでは気にしません。