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におけるスコープとクロージャについて