JavaScript の関数はとても柔軟だった

1年以上放置していたO'Reilly Japan - 初めてのJavaScriptを最近またやり始めました。「5章 関数」を読んだので、覚えたことを書いてみます。タイトルの通り、JavaScript の関数が持つ柔軟性についてフォーカスを当てます。
なお、動作検証にはRhino - MDC(1.7 release 2 2009 03 22)を使っています。

関数がオブジェクト

関数がオブジェクトなので、変数や配列要素に関数を代入できたり、関数を引数として渡したりできる。JavaScript の関数はファーストクラスオブジェクト

  • ruby のブロックみたいに使える
js> [1,2,3,4,5].filter(function(element, index, array){ return element % 2 == 0; })
2,4


関数は() をつけると呼び出せる。() つけないとオブジェクトとして扱うことができ、返り値として使えたりする。

  • 引数として、データとそのデータを使って処理を行なう関数を渡すと関数を実行してくれる関数
js> function exec(elem, func){
  >   return func(elem);
  > }
js> exec(1, function(e){ return e * 3;})
3


すごー( ゚д゚)

  • ruby のeach みたいなのを書いてみる
js> var MyArray = function(array){
  >   this.array = array;
  >   this.each = function(func){
  >     for(var i = 0; i < this.array.length; i++){
  >       func(this.array[i])
  >     }
  >     return this;
  >   }
  > }
js> var myarr = new MyArray([1,2,3,4,5,6,7]);
js> myarr.each(function(e){ print(e); })
1
2
3
4
5
6
7
[object Object]

関数は引数の数を考慮しない

関数を定義した時の仮引数の数と、呼び出す際に渡す引数の数が異なっていてもエラーにならない。

  • 呼び出す際に値を指定しないと、その引数の値はundefined になる
js> var x = function(a, b){
  >   var sum = 0;
  >   if(a) sum += a;
  >   if(b) sum += b;
  >   return sum;
  > }
js> x()
0
js> x(1)
1
js> x(1, 2)
3
js> x(1, 2, 4)
3
  • 引数の数を考慮しないので、一番最初に示した例はこう書ける。このようなコールバック関数の時とか便利。必要なものだけ受けとれる。
js> [1,2,3,4,5].filter(function(e){ return e % 2 == 0; })
2,4
  • 関数は引数を保持するarguments というオブジェクトを持つ
js> var sum = function(){
  >   var res = 0;
  >   for(var i = 0; i < arguments.length; i++){
  >     res += arguments[i];
  >   }
  >   return res;
  > }
js> sum()
0
js> sum(1)
1
js> sum(1,2,3,4)
10


どことなく、Ruby が持つ柔軟性に似た部分を感じる。

まとめとして、prototype.js のソースの一部を読んでみる

Prototype JavaScript framework: Easy Ajax and DOM manipulation for dynamic web applicationsの1.6.0.1 のEnumerable のeach の部分のソースを読む。

1. まず元になるeach がArray にありそうなので、そこを読む

あった.。゚+.(・∀・)゚+.゚。

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  },


さっき自分で書いたのと処理的には同じ。prototype とかはまだ勉強していない。既存のクラスに関数を追加したりする際に使いそう。
length を最初に変数に代入しているのは、毎回評価することの無駄を省くためか、それともコールバック関数の内部で破壊的動作をした時にもループが最後まで回るようにするためだと思う。

2. 次にこのeach を使うEnumerable を読む

意外なことに、Enumerable にもeach が定義されていたので、今回はここを読むとする。each はArray やHash だけに定義されていて、Enumerable はそのeach を使って処理を行なうmap とかが定義されているのかと思った。
each が2つある謎にせまる。

var Enumerable = {
  each: function(iterator, context) {
    var index = 0;
    iterator = iterator.bind(context);
    try {
      this._each(function(value) {
        iterator(value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  },


主要な部分だけ抽出すると、以下のようになる。

var Enumerable = {
  each: function(iterator, context) {
    var index = 0;
    this._each(function(value) {
      iterator(value, index++);
    });
    return this;
  },


index をEnumerable 側のeach で管理している。共通化している意味はこの辺りにありそう。一端Hash のeach を見てみる。

    _each: function(iterator) {
      for (var key in this._object) {
        var value = this._object[key], pair = [key, value];
        pair.key = key;
        pair.value = value;
        iterator(pair);
      }
    },


各要素を取得する処理にfor .. in 形式のfor を使っている。もしここでインデックスの値を扱いたいなら、この中でインデックスの値を保持する変数を用意しないといけない。他にも、Range のeach はsucc 関数を使って要素を取り出したりしているのでこの場合も別途インデックスの値を保持する変数が必要になってしまう。
この辺の処理をEnumerable のeach で共通化している。今回は省略してしまったけど、エラー処理とかもEnumerable のeach で共通化している。each はEnumerable の他の関数内で使われるのでここでエラー処理をきちんと行なっておくのは理にかなってそう。

Enumerable のeach はクロージャを使っていたり、関数が引数の数を気にしない性質を使っていたりして、これだけでも勉強になりました。これならeach_with_index いらないもんね。