JavaScriptのクロージャをループ内で定義する時の技

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

カテゴリー: Tips タグ: パーマリンク