JavaScript のクロージャは便利な機能だが、ループの中で定義すると意図しない値になることがある。たとえば、for ループの中でクロージャを定義して、ループ変数の値を利用したら、すべてループの最後の値になってしまったり、すべての関数の中で同じオブジェクトを参照してしまうなど、失敗例は枚挙にいとまがない。無名関数を利用すると記述が複雑になって、いつも以前のコードを探し回って書くことになるので、忘れないようにメモを残すことにした。
クロージャの定義を言葉で説明するのは難しく、Wikipediaにも、「クロージャ(クロージャー、英: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数で実現している。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。」と書いてあるが読んでも直感的に理解できない。ざっくりと、関数の中にローカルなスコープの変数を定義できると考えておけばいいだろうか。MOZILLA DEVELOPER NETWORK のクロージャの使用の方がわかりやすいかもしれない。
最近は jQuery をよく使うので、jQuery を使ったサンプルを残しておくことにした。基本となる HTML は、以下のように単純なものを利用する。各 a タグをクリックしたときの動作を、jQuery で後から定義していくことにする。
<html> <head> <script src="jquery.js" type="text/javascript" charset="utf-8"></script> </head> <body> <a href="javascript:void(0);">a</a> <a href="javascript:void(0);">b</a> <a href="javascript:void(0);">c</a> </body> </html>
グローバル変数を参照する
最も単純なパターンは、グローバル変数を参照する方法だ。この場合、リンクの a b c どれをクリックしても、i の値はインクリメントされる。変数が整数だと使い道はあまりないかもしれないが、グローバルに定義したオブジェクトの値を変更するときなどは、このパターンを使うこともある。ただし、XMLHttpRequest を非同期で利用する場合は、非同期にグローバル変数を更新されることになるため値が不定になる。
var i = 0; $('a').each(function() { $(this).click(function() { console.log(i++); }); });
ローカル変数を参照する
関数の内部で定義したローカル変数を参照するパターンで、a を連続してクリックすると、1 2 3とコンソールに出力する。ただし、次に b をクリックしても4にはならず、1になる。つまり、各 a タグごとに変数 i がローカルに定義されて、値を保持している形になる。
$('a').each(function() { var i = 0; $(this).click(function() { console.log(i++); }); });
よくある失敗
下記のコードだと、各 a タグをクリックした結果は必ず3になる。よくある失敗の例だ。ループの中で関数を定義しているわけだが、ループ変数は関数の外部で定義されている。クリックしたときに実行する関数は、実行時に i を参照するが、それはループが終わったときの値だ。従って、今回の例の場合は3が表示される。
var array = new Array(); $('a').each(function() { array.push($(this)); }); for(var i = 0; i < array.length; i++) { array[i].click(function() { console.log(i); }); }
ループ内で関数を定義する場合は return function が必要かもしれない
ループ内で関数を定義していて、ループ変数の値を表示するには、関数を戻す関数を定義する必要がある。下記の例では、クリックしたときに実行される関数は、関数を戻しており、かつ、関数自体を呼び出すように指定して引数にループ変数を渡している。console.log で渡している引数は、最初の関数の引数と同じものを参照するので、ループしている間の i の値を表示することができる。
var array = new Array(); $('a').each(function() { array.push($(this)); }); for(var i = 0; i < array.length; i++) { array[i].click((function(n) { return function() { console.log(n); } }).call(this, i)); }
この記述方法をいつも忘れてしまうので、メモで残しておきたかったのです。無名関数にしないで変数に関数を割り当てたりすればよいのかもしれないけど、短い処理を書くのに無名関数を使わないとスマートにかけないので。