解決編: JavaScriptで配列やオブジェクトのキーを反復するイディオム

先日の続きです。

escape_artistさんから詳細なコメントを頂きました。ありがとうございます。

また、身内に「ネイティブのforを使ってはいけない」の真意を聞いてみました。

その結果、疑問が解決したのでメモしておきます。

疑問1: for inで配列をループしてはいけないの?

使わないほうがよいそうです。理由は次のとおりです。

  1. Arrayのprototypeが拡張されているリスクがあるから
  2. 配列要素以外のプロパティが追加されているリスクがあるから
  3. ふつうのforの方がパフォーマンスがよいから
理由1: Arrayのprototypeが拡張されているリスクがあるから

escape_artistさんに頂いたコメントが分かりやすいので、そのまま引用させていただきます。

inは仰るとおりプロトタイプを辿るため、Array.prototypeに何かメソッドが追加されているとそれも列挙されてしまいます。

Array.prototype.alertLength = function(){ alert(this.length); };
↑こういう変な拡張をしているライブラリを使っていれば、すべての配列のfor inに"alertLength"が出現するようになってしまう。

もちろんArray.prototypeを無計画に拡張するのは行儀が悪いのですが、、このリスクに備えるという意味です。

たしかに、これは危なそうです。

理由2: 配列要素以外のプロパティが追加されているリスクがあるから

理由1と似ています。

配列はオブジェクトなので、要素以外のプロパティを追加できてしまいます。

var ar = [1, 2, 3];
ar.pet = "cat";

こんなところに、謎のネコが。。。

なお、このときの配列のlengthは、要素のインデックス上限 + 1 の値になります。

console.log(ar.length);
3

隠し持っているプロパティ「pet」はノーカウントです。まずい、ネコに気づかないかも。

しかし、for inを使うと、(存在する要素数 + 存在するプロパティ数)回、ループが実行されます。

for (var n in ar) { console.log(n); }
0
1
2
pet

(ネコきたー)

配列をfor inで回す時、for inの中に書くのは、配列の要素に対して実行したい処理だと思います。しかし、配列オブジェクトがプロパティを持っていると、プロパティに対しても同様の処理が適用されます。はたしてそれは意図した処理かという話です。

また、配列要素とプロパティとで、型がずれている可能性があります。処理によってはエラーになるでしょう。

理由3: ふつうのforの方がパフォーマンスがよいから

for inよりもふつうのforの方が、やや高速なのだそうです。

といっても、escape_artistさんが書いてくださったとおり、その差はあまり重要ではないと思います。ユーザコードでこのレベルのマイクロチューニングが本当に必要な場面は、少ないのではないでしょうか。


疑問2: for inを別のもので代用した方がいいのか? もしするとしたら、何を使えばいいのか?

今度は、配列ではなく、オブジェクトのプロパティをindexingする際の話です。

  • プロトタイプチェーンをさかのぼってもよいならば、for inでもよい。
  • しかし、変数のスコープが広くなるのを気にするならば、Underscore.jsやUglifyJSを使おう。

というのが、答えのようです。以下の懸念を理解した上で使うなら問題ないということです。

大量のkeyを列挙することの懸念

for inでプロトタイプチェーンをさかのぼった場合、うっかり継承の深いオブジェクトをループすると、大量のkeyを列挙してしまう可能性があります。もちろん、パフォーマンスも劣化します。

具体的には、DOMをカジュアルにfor inすると酷いことになると教えてもらいました。

スコープに関する懸念

JavaScriptは、関数を使うことでしか、スコープを区切ることができません。

そのため、for inで取り出したオブジェクトを受け取るための変数は、for inが書かれたスコープ内のどこからでもアクセスできてしまいます。

これを回避するために、各種ライブラリのベンリ関数を使うのがオススメとのことでした。

Underscore.jsの_.eachや_.map、また、配列の中身を別の配列にコピーするようなときには、_.reduceを使えば、ループ時に実行する処理を関数にできるので、変数を外に漏らさずに済みます。

// 3つ目の引数の配列は、mの初期値であり、戻り値
_.reduce([1,2,3], function(m, n, i) { m[i] = n; return m; }, []);
余談

余談ですが、前回、

_.keysは、ネイティブのfor inと違って、プロトタイプチェーンをさかのぼっては見てくれないんですね。

などと書きました。が、これは、そもそもの考え方がおかしかったようです。

_.keysは、そのオブジェクトのプロパティしか見ず、ネイティブのfor inよりも探索範囲が狭いので、代用として不足ではないかと思ったので、上のように書きました。しかし、むしろ「検索先を自分自身に限定するために」使う道具だったんですね。

いろいろ教えてくださった皆さん、ありがとうございます m(_ _)m