All Articles

Function.prototype.bindで困った話

Array.prototype.forEachを使っても怒られない環境で開発するようになった僕ですどうも。
え、querySelectorAllも使っていんですかヤッター。

Function.prototype.bindで困った

以下のようなコードを書いててハマりました:

function Foo(element) {
    this.element = element;
}

Foo.prototype.init = function() {
    this.element.addEventListener('click', this.handle.bind(this));
};

Foo.prototype.handle = function(evt) {
    // do something
};

Foo.prototype.cancel = function() {
    this.element.removeEventListener('click', this.handle.bind(this));
};

便宜上色々省略していますが、要はイベントハンドラのthisはクラス(のようなオブジェクト)に束縛したいケースです。もうお分かりでしょうが、クリックイベントが解除されないんですね。うーん。

戻る関数は匿名関数扱いらしい

で、MDNを見てみると、次のように書かれています。

Creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

はい、「Creates a new function」と書かれてますね。つまり新しいクロージャができるんですが、なぜか私はthisを内部で束縛させた自身の関数が戻る、つまり破壊的なメソッドだと思ってました。実際にbindで戻る関数を見ると、匿名関数のような扱いになるんですね(Function.nameが空文字)。

なので、

this.element.addEventListener('click', this.handle.bind(this));

と書くと、このイベントは解除できなくなってしまいますね。投げっぱなしイベントならそれでいいかもですが、パフォーマンスの関連でこまめにイベントの発火を制御するケースでコレはちょっと困ります。 ※そもそも無名関数クロージャでイベントをその場でセットするのが好きじゃないってのもありますが

じゃあどうするか

意外とみんな使ってないイメージなんですが、handleEvent使うといいと思うんです。

function Foo(element) {
    this.element = element;
}

Foo.prototype.init = function() {
    this.element.addEventListener('click', this, false);
};

Foo.prototype.handleEvent = function(evt) {
    // do something
};

Foo.prototype.cancel = function() {
    this.element.removeEventListener('click', this);
};

これならイベントハンドラが集約されてクラス内でthisが束縛されて、さらにイベント解除にもthisを渡せばいいので便利ですね。

おまけ

そういえばiOS 6でhandleEventメソッドがクラスメソッド(のようなもの)で定義されるとハンドルできないって問題があったんですけど、iOS 7ってこれもう修正されてるのだろうか。検証してたコードがgistにあります。

https://gist.github.com/ysugimoto/3970836

現場からは以上です。