JavaScriptの反復処理(イテレーション)について

こんばんは、south37です。

最近、JavaScriptについて知れば知る程、ダメなところが目につく様になってきました。 もちろん良いところもいっぱいあって、関数が第一級オブジェクトである事や、シングルスレッドのイベントループという形式と非同期APIによって多数の処理を高速にさばける事なんかは本当にクールだと思います。

ただ、イケてない部分が多いのは事実で、その内の一つであるJavaScriptの反復処理に付いて今日は書きたいと思います。

反復処理の書き方

さて、JavaScriptにおいて反復処理をしたい場合はどんなコードを書いたら良いのでしょうか? 結論を先に述べると、Arrayに対しては

var array = [1, 2, 3];
for (var i = 0, n = array.length; i < n; i++ ) {
  console.log(array[i]);
}
// 出力
// 1
// 2
// 3

のように単純なforループを用いると良いでしょう。また、単なるObjectに対しては、

var obj = {name: 'Taro', age: 18, gender: 'male'};
for (var key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key + ': ' + obj[key]);
  }
}
// 出力
// name: Taro
// age: 18
// gender: male

のようにfor in構文を用いながらも、hasOwnPropertyメソッドで直接のプロパティであるかを確認するようにすると良いでしょう。こうすれば、ほどんどの場合は想定通りの振る舞いになるはずです。

何故こんな書き方?

それでは、何故こんな書き方をしなければ行けないのでしょうか?特に、for in構文で何故hasOwnPropertyを用いたチェックが必要になるのでしょうか?

実は、JavaScriptにおけるfor inでは、オブジェクトのプロトタイプチェーンを遡って全てのプロパティを列挙しようとします。その為、例えばユーティリティ関数としてeachメソッドをObject.prototypeに追加したとすると、for inを使った際にはそのeachメソッド自身が列挙される事になります。

Object.prototype.each = function(iterator) {
  for (var key in this) {
    iterator(key, this[key]);
  }
};
var obj = {name: 'Taro', age: 18, gender: 'male'};
obj.each(function(k, v) {
  console.log(k + ': ' + v);
});
// 出力
// name: Taro
// age: 18
// gender: male
// each: function (iterator) {
//     for (var key in this) {
//       iterator(key, this[key]);
//     }
//   }

これは、どう考えても望ましい挙動ではありません。

一方、hasOwnPropertyメソッドでチェックを行っておけば、プロトタイプチェーンを遡ったりはせず、オブジェクト自身のプロパティに対してのみ処理が実行されます。

Object.prototype.each = function(iterator) {
  for (var key in this) {
    if (this.hasOwnProperty(key)) {
      iterator(key, this[key]);
    }
  }
};
var obj = {name: 'Taro', age: 18, gender: 'male'};
obj.each(function(k, v) {
  console.log(k + ': ' + v);
});
// 出力
// name: Taro
// age: 18
// gender: male

こちらの方が直感通りの挙動ですね!

実際には、Object.prototypeにプロパティを追加する事自体が望ましくないのですが、意図せず書き換えられるケースも考慮してhasOwnProperyによるチェックを行った方が良いと思います。

尚、ECMA5環境であれば、Object.keysメソッドを使うといった方法も考えられます。こちらは、オブジェクトの直接のプロパティのkeyをArrayにして返すメソッドです。

var obj = {name: 'Taro', age: 18, gender: 'male'};
var keys = Object.keys(obj);
for (var i = 0, n = keys.length; i < n; i++) {
  console.log(keys[i] + ': ' + obj[keys[i]]);
}
// 出力
// name: Taro
// age: 18
// gender: male

想定通りの振る舞いですね。

Arrayの反復について

次に、Arrayの場合について考えてみたいと思います。Arrayもオブジェクトである為、for in構文自体は使えるのですが、Objectの場合と同じ理由からあまり推奨されていません。 代わりに使われるのが、単純なforループです。Arrayはlengthプロパティを持つ事やkeyとして数字を持つ事が保証されている為、最初の例のようにforループが使えます。

var array = [1, 2, 3];
for (var i = 0, n = array.length; i < n; i++ ) {
  console.log(array[i]);
}
// 出力
// 1
// 2
// 3

また、forに加えてECMA5ではforEachメソッドという便利なメソッドが用意されました。これは、iteration用の関数を受け取って、Arrayの各要素を引数として実行してくれるメソッドです。

var array = [1, 2, 3];
array.forEach(function(el) {
  console.log(el);
};
// 出力
// 1
// 2
// 3

forEachメソッドを用いた方が処理そのものに注目しやすい為、ECMA5環境では積極的に使った方が良いでしょう。iteration用の関数には第二引数としてインデックスも渡せる為、forループを用いる場合に比べて出来る事に変わりはありません。

var array = [1, 2, 3];
array.forEach(function(e, i) {
  console.log('No.' + i + ': ' + e);
});
// 出力
// No.0: 1
// No.1: 2
// No.2: 3

結論

最初に述べた通りです。もっと言えば、underscore.jsの_.each(list, iterator)メソッドでは第一引数がArrayかObjectかに応じて上記のコードを使い分けて実行してくれる為、underscore.js使っとけば間違いないです。

って事で、underscore.jsのステマでした!!!!

補足

for inでプロトタイプのプロパティが列挙されるといいましたが、システムでデフォルトで定義されているプロパティは列挙されません。後から追加したプロパティだけが列挙されます。なんでやねんって思うかもですが、そういう仕様です。

が、実は列挙不可のプロパティを追加する方法は存在します。ECMA5で定義されたObject.definePropertyメソッドを使う事でそれが可能です。

Object.defineProperty(Object.prototype, 'each', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: function(iterator) {
    for (var key in this) {
      iterator(key, this[key]);
    }
  }
});

var obj = {name: 'Taro', age: 18, gender: 'male'};
obj.each(function(k, v) {
  console.log(k + ': ' + v);
});
// 出力
// name: Taro
// age: 18
// gender: male

Object.definePropertyは第一引数にプロパティを追加したいオブジェクト、第二引数にプロパティのキー、第三引数にディスクリプタと呼ばれるプロパティの詳細を記述したオブジェクトを渡します。valueがプロパティの実際の値になります。

ディスクリプタ中のenumerableという項目をfalseに設定する事で、for inによる列挙が行われなくなっています。

ただ、このObject.definePropertyメソッドを使うという方法は、ECMA5以降でしか動かない上かなりトリッキーな部類に入るので、よっぽどの理由が無い限り使わない方が良いと思います。大人しく_.eachを使っときましょう。

Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方

Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方