イベントハンドラの落とし穴 this編
私はかつて以下のようなコードを書いてしまいました。(前回のコードを一部流用しています。)
getElementById()を簡略するための定義。
$ = function(id) { if (window.document.getElementById(id)) { return window.document.getElementById(id); } return null; }
問題となるコード。スタティックなオブジェクとリテラルで関数を定義。
var Someone = { init: function() { this.registerEvent(); }, registerEvent: function() { $('button').addEvent('click', function(e) { this.whoami(); }); }, whoami: function() { alert('This is someone.'); } } window.addEvent('domready', function(e) { Someone.init(); });
clickイベントに対して処理を割り当てているところにthis.whoami()が記述されています。ここがこのコードのエラー箇所になります。これを解決するためにどうすればいいのか、悩んだ経験があります。
なにが問題になるのか、考えて見ましょう。
domreadyイベントで、clickイベントに対しwhoami()メソッドを呼ぶように処理が書かれています。プログラマの意図としては、IDが「button」の要素がクリックされたら、アラートダイアログに「This is someone.」と表示されて欲しい。ですがナリません。
ここで注意が必要なのがイベントが発生したときに、そのイベントに登録したオブジェクトを参照することはできないということです。ここでいうSomeoneというオブジェクトをイベントに設定した関数内からthisで参照することはできません。
ですが、この場合Someoneの定義は静的(スタティック)なため、以下のように書きなおせば、動きます。
var Someone = { // 省略 registerEvent: function() { $('button').addEvent('click', function(e) { Someone.whoami(); }); }, // 省略 }
ですが、インスタンス化するようなオブジェクトでは、この方法は通用しません。またこのような場合でもthis参照が可能になるとオブジェクト名の変更等での修正が少なくなります。
そこで、使用するのがFunctionオブジェクトに定義されているbind()メソッドを使う方法です。
これは「thisを束縛する」などと呼ばれていますが、bind()メソッドの引数にthisを渡すだけで、イベント発生時にこのイベントを登録した処理が入っているオブジェクトを参照できるようになります。
それでは、prototypeオブジェクトの例とともにbind()を使用した方法を見てみましょう。
var Person = function(name) { this.name = name; this.registerGreeting(); } Person.prototype = { registerGreeting: function() { $('hello').addEvent('click', function(e) { this.greet('Hello'); }.bind(this)); }, greet: function(msg) { alert(msg + ', ' + this.name); } } var Someone = { init: function() { this.registerEvent(); }, registerEvent: function() { $('button').addEvent('click', function(e) { console.log(this); this.whoami(); }.bind(this)); }, whoami: function() { alert('This is someone.'); } } // Main process window.addEvent('domready', function(e) { alice = new Person('alice'); Someone.init(); });
どうですか。これだとイベント発生してもthisはプログラマが意図したところを見ているはずです。
function(){}の定義とは、Functionオブジェクトの生成と同義のためfunction(){}.bind()という書き方できます。わかりにくい場合は(function(){}).bind()と考えていただければいいです。
JavaScriptではイベントにかぎらず、タイマー処理やAjaxなどの非同期処理でのthis参照は非常に面倒くさいので、注意が必要です。ここで、本質を言ってしまえば、ライブラリを使ったほうが確かだということです。