prototype.jsのbind()メソッドは便利。

todaapply,bind(),bindAsEventListener(),JavaScript,prototype.js,this,イベント,コールバック,シンタックスシュガー,便利

昔から買い続けた雑誌や書籍に部屋を侵略され続けて早○年。いい加減部屋を広げなければならないと、一念発起してドキュメントスキャナと裁断機を購入して、せっせと雑誌を裁断 → ドキュメントスキャナでPDF化にいそしんでます。
おかげで今月もAmazonの請求が7万を超えてしまいました。こんにちはtodaです。

さて、最近仕事でJavaScriptを書く機会が増えましたが、PHPなどの(まがりなりにも)クラスベースのオブジェクト指向言語とは似て非なるところが多くて、その挙動の違いに驚かされます。
今回はJavaScriptを書いていて、ハマったポイントとその解決法をを紹介します。

JavaScriptで、あるクラスを作っていて、そのメソッド内でAjax.Reuqest()で通信した結果をオブジェクトの変数に格納したいことがあります。

以下のPersonクラスはコンストラクタで名前を受け取り、身長を表示するときに、名前をキーにして身長をオンデマンドで取得する例です。

var Person = Class.create();
Person.prototype = {
	initialize: function(name){
		this.name = name;
		this.height = null;
	},

	showHeight: function(){
		new Ajax.Request("/get_height", {
			method: 'get',
			parameters: "name=" + this.name,

			onSuccess: function(http, result){
				// これはダメ。Person.nameは参照できない。
				// このthisはPersonオブジェクトではないから。
				this.name = result['name'];
			}
		});

		alert('身長は' + this.height + 'cm');
	}
}

var toda = new Person('toda');
toda.showHeight();

これはうまくいきません。Person.showHeight()で、Ajax.Requestにより通信を行うときに、
結果はonSuccessコールバック関数で処理されるため、thisがPersonのオブジェクト(toda)ではなくなるからです。

本来であれば、コールバック関数にthis(Personのオブジェクト)を追加で渡したいところですが、
コールバック関数の引数はすでに決まっていますので、あとから追加はできません。

この問題を解決するための仕組みがprototype.jsに用意されています。
コールバック関数の定義に".bind(this)"を追加することで、コールバック関数内で大本のオブジェクトをthisとして参照できます。

Person.prototype = {
	// コンストラクタ省略

	showHeight: function(){
		new Ajax.Request("/get_height", {
			method: 'get',
			parameters: "name=" + this.name,

			onSuccess: function(http, result){
				// これでOK。
				this.name = result['name'];
			}.bind(this)		// ← この".bind(this)"がポイント!
		});

		alert('身長は' + this.height + 'cm');
	}
}

この".bind(this)"のカラクリを解き明かしてみましょう。

まずは、prototype.jsのbind()の定義を参照します。
bind()はFunctionクラスのメソッドとして定義されていて、__methodに自分自身(this)を保存したうえで、apply()メソッドを適用した関数をreturnしていることがわかります。

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

このapply()メソッドはFunctionクラスに標準的に用意されているメソッドで、thisを第一引数に設定したうえで、第二引数に指定された配列を引数にして関数を呼び出すメソッドです。わかりづらいので例を見てください。add_two_value()関数にapply()メソッドを適用して、グローバル変数のbaseが関数内のthisに割り当てられているのがポイントです。

var base = 1;

function add_two_value(v1, v2){
	alert(this + v1 + v2);
}

// thisをbaseにした上で、add_two_value関数を引数(2, 3)で呼び出す。
add_two_value.apply(base, [2, 3]);		// → alert(this + v1 + v2)
						// → alert(base + 2 + 3)
						// → alert(1 + 2 + 3)
						// → alert(6)

bind()の定義を再度見ると、いちばん最初の引数をargs.shift()で取得して、apply()メソッドの第一引数に、残りの引数(および実際のコール時に指定された引数を連結したもの)を第二引数に指定していることがわかります。

つまり、コールバック関数であるonSuccess関数にbind(this)を適用することにより、onSuccess関数のthisを、オブジェクトのthisに置き換えた関数を準備することが可能になります。これにより、Ajax.Requestがコールバック関数を呼び出した時点でthisはオブジェクトのthisに切り替わっているので、めでたくオブジェクトのプロパティを参照できるようになります。

もちろんprototype.jsが用意するbind()はただのシンタックスシュガーですので、常にapply()に書き換えることができますが、かなり面倒なコードになってしまい可読性が下がることが必至です。ここは素直に先人の知恵を拝借するのが賢い選択でしょう。

似たようなメソッドとしてbindAsEventListener()というメソッドがあります。こちらはEvent.observe()のコールバック関数にthisをbindするために使用します。

わざわざ別名をつけているのは、Event.observe()のコールバックはeventを受けとって、このeventはUAによって取得しかたが異なるため、これを吸収する部分が含まれているからです。

このbindAsEventListener()を使うことで、コンストラクタでイベントの割り当てができるようになります。結果、グローバル空間の汚染を防止できます。こちらもぜひ活用しましょう。

var Person = Class.create();
Person.prototype = {
	initialize: function(name){
		this.name = name;
		this.height = null;

		Event.observe('button', 'click',
			this.showHeight.bindAsEventListener(this));
	},

	showHeight: function(){
		new Ajax.Request("/get_height", {
			method: 'get',
			parameters: "name=" + this.name,

			onSuccess: function(http, result){
				this.name = result['name'];
			}.bind(this)
		});

		Element.update('show', '身長は' + this.height + 'cm');
	}
}