JavaScriptで、メソッドをコールバックとして渡す方法(コールバック関数でthisをbindさせる方法)

JavaScriptで、関数の引数として関数を渡す(コールバックと呼ばれる)ことはよくあります。

例えばsetTimeoutがそうです。
setTimeout(function(){alert(1)}, 1000);  // 1秒後にalert
下記のように書くことも当然できます。
var say = function(){ alert('hello');}

setTimeout(say, 1000);  // 1秒後に'hello'と表示
ここまでは簡単ですね。
では問題です。

問題:オブジェクトのメソッドをコールバックとして渡すにはどうすればよいか?

これが意外と難しいのです。
var obj = {
    name : 'pikachu',

    say : function(){
       alert('I am ' + this.name); 
    }
}

setTimeout(obj.say, 1000); // => 'I am'とだけ表示される。
これは期待通りに動いてくれません。
コールバックとしてメソッドを渡した場合、それはオブジェクトと切り離された単なる関数(メソッドではない)となってしまうのです。
つまり下記と等価なのです。
setTimeout(
        function(){alert('I am ' + this.name);}
        , 1000);
関数がオブジェクトと切り離されているため、このthisはグローバルオブジェクト(windowオブジェクト)を指してしまっており、'I am'とだけ表示されます。

ではどうすればよいでしょうか?
bindメソッド
IE9,Firefox, Google ChromeではFunctionクラスのbindメソッドというのが使えます。
これはECMAScript5で追加された新仕様です。
setTimeout(obj.say.bind(obj) , 1000); // 'I am pikachu'と表示
ただ、IE7,8,Safari5,iOS5のSafariなどでは使えません。
どうすればよいでしょう?
MDN(MOZILLA DEVELOPER NETWORK)から借りてくる
MDNのbindのページ に Function.prototype.bind のJS実装があります。
これを借りてくればよいでしょう。
if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5 internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1), 
        fToBind = this, 
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP
                                 ? this
                                 : oThis || window,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}
上記コードをあなたが実行したいコードの前に張り付けておけば、IE6,7,8やSafariでもbindが実用的に使えるようになります。
しかもFirefox・Chromeのようにあらかじめ組み込みbindがある環境では上記コードは何の副作用ももたらしません。

(厳密に言うと上記bindはあくまで「bindもどき」で、ECMAScript5組み込みのbindとは微妙に挙動が異なります。
詳細は後述の参考サイトをご覧ください。)

どういう仕組みでthisを束縛しているのか?

上記コードはどうやってコールバック関数内のthisをオブジェクトに束縛しているのでしょうか?

昔のprototype.jsの中に、よりシンプルな実装があります。
prototype.js 1.5.0
Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}
これでもまだ難しいですね。
難しい原因は引数(arguments)の処理なのでそこは無視しましょう。

ポイントはapplyです。
applyはFunctionクラスの組み込みメソッド(Function.prototype.apply)です。
f.apply(o);
このように書くと、"this = o とした上で f を呼び出す" ということができます。 つまり、
var obj = {name : 'pikachu'};
var say = function(){ 
       alert('I am ' + this.name); 
};

say.apply(obj); // => 'I am pikachu'
このように、任意のオブジェクトを関数のthis値として設定することができます。

最初の問題に戻ると、
var obj = {
    name : 'pikachu',

    say : function(){
       alert('I am ' + this.name); 
    }
}

setTimeout(function(){obj.say.apply(obj)}, 1000); // => 'I am pikachu'
これで問題が解けました!

え?コードが見づらいですか?

束縛を実現するためのヘルパ関数を作って書き換えるとこうなります。
var obj = {
    name : 'pikachu',

    say : function(){
       alert('I am ' + this.name + counter++); 
    }
}

function bind(f,o) {
  return function(){ return f.apply(o); };
}

var bound_say = bind(obj.say, obj);
setTimeout(bound_say, 1000);
これはクロージャを使った例です。
bindは「関数を返す関数」、bound_sayは「関数bind内で生まれた新たな関数」です
1秒後にbound_say関数が実行された瞬間に、f.apply(o)が実行されます。

MDNのbindよりは、仕組みとしては簡単ですね。
クロージャについては下記をご覧ください。
[JavaScript] 猿でもわかるクロージャ超入門 まとめ

まとめ

  • コールバックとしてメソッドを渡したいときはbind
  • IE6,7,8,Safari5にはbindがない
  • MDNのbindを使うといいかも
  • コンパクトにやるなら自前でapply

参考

もっと知りたい人は下記記事をご覧ください。

bindのJS実装についてはいろいろやり方がありますが、どうやってもESMAScript5の組み込みのbindを100%再現することはできないそうです。

お勧めは下記記事です。
jQueryの作者John Resig氏が、prototype.jsのbindをわかりやすく解説してくれています。ちょっと長いですけど。
Learning Advanced JavaScript

それではEnjoy!
カテゴリ:

人気記事