ちょっと高度にJavaScript/クロージャでイベントハンドラを作る
前回の解説では、クロージャの「変数が保持される」「変数が隠蔽される」という特徴を使って、アクセサメソッドを作りました。
今回は、「呼ぶたびにクロージャが生まれる」特徴を使ってみます。
繰り返し処理内でイベントハンドラを作る
まずは、何の工夫もなくやってみます。今回はjQueryも使います。
$(function() { var data = [ {label:'リンク1', value:1}, {label:'リンク2', value:2}, {label:'リンク3', value:3}, {label:'リンク4', value:4}, {label:'リンク5', value:5} ]; var ul = $('<ul/>'); for(var i = 0, l = data.length; i < l; i++) { var item = data[i]; var li = $('<li/>').text(item.label).click(function(e) { console.log(e.type, item.value); }); ul.append(li); } $('body').append(ul); });
data配列内のオブジェクトからリストを作成し、ulノード内の各liノードに、クリックで各々に対応したvalue値を出力させるイベントハンドラをバインドしたように見えます。お察しの通り、これは正しく動作しません。実際にクリックしてみると、どのノードをクリックしても5が出力されてしまいます。これは、この初期処理を終えた時点の変数itemにdata[4]が代入されているためです。イベントハンドラをバインドした時点でのitemではなく、処理を終えた後のitemを参照してしまっているのです。
では、クロージャをつかって正しく動くように修正してみます。前回、前々回と読んで頂いた方には、もう簡単かもしれませんね。
$(function() { var data = [ {label:'リンク1', value:1}, {label:'リンク2', value:2}, {label:'リンク3', value:3}, {label:'リンク4', value:4}, {label:'リンク5', value:5} ]; var ul = $('<ul/>'); for(var i = 0, l = data.length; i < l; i++) { var item = data[i]; var li = $('<li/>').text(item.label).click(function () { //エンクロージャ var closureItem = item; return function(e) { //クロージャ console.log(e.type, closureItem.value); } }()); ul.append(li); } $('body').append(ul); });
繰り返しの度に、イベントハンドラとしてクロージャが生まれます。各々のクロージャ内には、生成された時点でのitemがclosureItemに保存されています。
17行目の()に違和感を感じる方もいるかもしれませんが、これは、定義した匿名関数を直ちに実行する記述です。
また、今回はレキシカル変数としてエンクロージャ内でclosureItemを宣言していますが、このように引数を使う形に書き換えることもできます。
var li = $('<li/>').text(item.label).click(function (closureItem) { //エンクロージャ return function(e) { //クロージャ console.log(e.type, closureItem.value); } }(item));
まとめ
今回の例は、なかなか実用的だったのではないでしょうか。イベントハンドラ以外にも、ajaxの非同期処理などにも応用できます。
注意点として、「呼ぶ度にクロージャが生まれる」ということは、そのぶんメモリをたくさん消費するということでもあります。レキシカル変数にあまり巨大なオブジェクトを取り込むのは、避けたほうが良いでしょう。特にイベントハンドラに使用した場合、対象のノードへの参照が残っている限り、付随するイベントハンドラも残留し続けます。
クロージャについて、3回にわたって解説しました。同じような解説は他にも数多くあるにも関わらず、思いがけず多くの方に読んで頂いたようで、この場を借り、お礼を申し上げます。