cloned.log

2007-03-01 (Thu) prototype.jsのbindを理解する このエントリーを含むブックマーク このエントリーのブックマークコメント

prototype.js使っていてうれしいことの一つにbindが使えるというのがある。$()とかAjaxのクロスブラウザ対策とかもいいけれど、thisをbindできるのは大きなメリットだと思う。bindがないとどういう時に苦労するかというと、以下のような場面。

var Foo = function(name) {
  this.name = name;
}
Foo.prototype.talk = function(message) {
  alert(this.name + ": " + message);
}
function inScope() {
  var foo = new Foo("foo");
  setTimeout('foo.talk("Hello World!")', 0);
}
inScope();

これは実行できない。なぜかというとsetTimeoutに渡しているfooという変数はinScopeというFunction内でのみ有効だから。メソッドの引数がなければ、関数を直接setTimeoutに登録することで実行自体はできるけれど、今度はthis参照が期待通りに動かなくなる。

function inScope() {
  var foo = new Foo("foo");
  setTimeout(foo.talk, 0);
}

message変数の部分は諦めたにしても「this.name」までも使えなくなっている。これはsetTimeout自体がthisをwindowとしてcallするため。このthisの問題(仕様だから問題じゃないけれど)は、イベントリスナーに登録した場合も同じで、インスタンス(と呼ぶとJavaScript的には多分間違いだけど)使ったプログラミングをしているとどうしても弊害になることが多い。

そこでbindが必要になる。

function inScope() {
  var foo = new Foo("foo");
  setTimeout(foo.talk.bind(foo, "Hello World!"), 0);
}

これで「foo: Hello World!」と表示されて、期待通りの動きになる。bindの第一引数にthisとなって欲しいものを渡して、あとは引数を書けば良い。

じゃあprototype.jsがどんなHackをしているかというと、実はHackらしいことはほとんどなくて、JavaScript本来にあるapplyをラップしているだけ。prototype.jsのbindの部分を見てみよう。

Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}

$Aもprototype.jsの機能だけど、簡単に言えば指定されたFunction(上の例ではtalk)のapplyを実行するFunctionをreturnしていて、実行するapplyに対してthisになって欲しいものを第一引数に、受け取った引数(thisとして使うものは省く)を配列にして第二引数に渡している。配列にしているのは、apply自体が第二引数に配列を要求するため。

prototype.jsにはよく似たやつにbindAsEventListenerというのがあって、それは以下のように書かれている。

Function.prototype.bindAsEventListener = function(object) {
  var __method = this, args = $A(arguments), object = args.shift();
  return function(event) {
    return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
  }
}

何のことはなく、イベント発生時のevent引数をブラウザ互換をとった上でbindと同じことをしているだけ。

じゃあ、prototype.jsがなかったらどう書けば期待通りに動くかというと、以下のように書けば最初の例が動く。

function inScope() {
  var foo = new Foo("foo");
  setTimeout(function(){
    foo.talk.apply(foo, ["Hello World!"])
  }, 0);
}

動くけど・・・というコードだ。

という訳でprototype.jsのbindは素敵だねという話でした。


[追記]

コメントで指摘してもらって気付いたけれど、確かに上の例だと

setTimeout(function(){foo.talk("Hello World!")}, 0);

が書けちゃうので、上の例だと変に短く書けるっぽいアピールになっているので、その辺は訂正します(元コードはそのままにしてる)。コメントで書いてもらってるコードでちゃんと動きます。一応、言いたかったbindのことを、this参照の例で追記しとく。

var Hoge = Class.create();
Hoge.prototype = {
  initialize: function(msg) {
    this.msg = msg;
    Event.observe($("hoge"), "mousedown",
      this.mousedown.bindAsEventListener(this)
    );
  },
  mousedown: function(event) {
    alert(this.msg);
  }
}
var h = new Hoge("hoge");

bind使うとこんなときにmousedown関数内でもthisが継続して使えて、それらしく書ける。勿論、

  initialize: function(msg) {
    this.msg = msg;
    var self = this;
    Event.observe($("hoge"), "mousedown",
      function(){self.mousedown()}
    );
  },

としても動くけれど、一つfunction()を入れ子にするとthisが見えなくなるから、変数に入れる手間が出てくる。


[追記2]

通りすがり2さんのご意見。http://jigen.aruko.net/archives/533/とかのコード見せたほうが良くねって話かな。今のところまだまだ勉強不足なので、凄すぎる使い方が思い付かないけれど、より理解が深まったのでどちらのコメントもどうもです。

通りすがり通りすがり 2007/03/01 13:51 setTimeout(function(){foo.talk(”Hello World!”)}, 0); と比べてメリットがあるようには思えませんが。

通りすがり2通りすがり2 2007/03/01 16:19 Elementとクラスを同一視できるbindAsEventListenerの凄さを説明した方が分かりいいと思うが。