JavaScriptのクロージャは便利だが、forなどのループ内で繰り返し呼び出したい場合は、変数のスコープに注意する必要がある。特に、ループカウンタの値を参照するようなケースでは、ループ内で擬似的にfunction()を定義して、新しいスコープを作る必要がある。
1秒ごとに何かを実行する例で考えてみる。例えば、1秒ごとに「fire! 0」「fire! 1」「fire! 2」と出力したいとする。
(例1)うまくいかない方法
function closure_sample1(){ for(var i=0;i<3;i++){ setTimeout(function(){ console.log('fire! ' + i); // メッセージ出力 },1000*i); // 1000ミリ秒ごとに処理を実行 } }
この例では、タイマー自体は1秒ごとにセットされるが、メッセージの出力部分(4行目)はループが回り終わった後で実行されるため、コンソールには全て「fire! 3」と表示されてしまう。
これを回避するためには、クロージャに頼らず別の関数を定義すれば問題ない。
(例2)クロージャを使わない方法
function setTimerLog(x){ setTimeout(function(){ console.log('fire! ' + x); },1000*x); } function closure_sample2(){ console.log('closure_sample2'); for(var i=0;i<3;i++){ setTimerLog(i); // ループカウンタを引数にして別関数をコール } }
通常はこれで解決するが、どうしてもクロージャを使いたい場合は、次のようにする。
(例3)function()により新しいスコープを作る方法
function closure_sample3(){ console.log('closure_sample3'); for(var i=0;i<3;i++){ (function(){ // 無名関数を定義 var ii = i; // この関数内にスコープを有する変数iiを定義 setTimeout(function(){ console.log('fire! ' + ii); // ii はループごとに異なる値を持つ },1000*i); })(); // 関数定義ここまで。最後の()が重要。 } }
ループ内で定義されている関数の中で変数iiは定義されている。すなわち、ループごとに別の変数として扱われるので、当然値も異なっており(0,1,2)、その結果、期待どおりのコンソール出力が得られる。
今回、FileAPIのイベントハンドラを定義しようとして、例2のパターンが使えなくて困った。通常は以下のように記述する。
{ // ループ内部 reader.onload = handler; // 読み込みが完了したら呼び出す関数 } function handler(event){ // event.target.resultで 読んだ内容を取り出す }
ここで、handlerの動作をループカウンタの値に依存するような形に書き換えようとして、
reader.onload = handler(i); // ループカウンタの値とともに呼び出す関数
としてしまうと、handler(event) 側で i が取り出せない。
そこで、以下のようにして解決した。
{ // ループ内部 (i:ループカウンタ) (function(){ var ii = i; reader.onload = function(event){ // iiを使った処理を記述 } })(); }
参考にしたサイト:javascriptにおけるスコープとクロージャについて