JavaScriptのループ内のクロージャ - シンプルな実用例

var funcs = [];
// let's create 3 functions
for (var i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value: " + i);
  };
}
for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

このように出力されます。

私の値です。3 私の値3 私の価値3

一方、出力してほしいのは 私の値:0 私の値: 1 私の値:2


イベントリスナーを使用しているために関数の実行が遅れる場合にも同様の問題が発生します。

var buttons = document.getElementsByTagName("button");
// let's create 3 functions
for (var i = 0; i < buttons.length; i++) {
  // as event listeners
  buttons[i].addEventListener("click", function() {
    // each should log its value.
    console.log("My value: " + i);
  });
}
<button>0</button>
<br />
<button>1</button>
<br />
<button>2</button>

... また、Promisesなどを使った非同期コードもあります。

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for (var i = 0; i < 3; i++) {
  // Log `i` as soon as each promise resolves.
  wait(i * 100).then(() => console.log(i));
}

この基本的な問題を解決するにはどうしたらいいのでしょうか?

ソリューション

さて、問題は、それぞれの無名関数の中の変数iが、関数の外の同じ変数に束縛されていることです。

古典的な解決策クロージャ

各関数内の変数を、関数外の独立した不変の値に束縛したいのです。

var funcs = [];

function createfunc(i) {
  return function() {
    console.log("My value: " + i);
  };
}

for (var i = 0; i < 3; i++) {
  funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

JavaScriptにはブロックスコープがなく、ファンクションスコープしかないので、関数の生成を新しい関数でラップすることで、「i」の値が意図した通りになるようにしています。


2015年の解決策:forEach

Array.prototype.forEach関数が比較的広く利用できるようになった(2015年)ことで、主に値の配列に対する反復を伴う状況において、.forEach()`は、反復ごとに個別のクロージャを得るためのクリーンで自然な方法を提供していることに注目したいと思います。つまり、値を含む何らかの配列(DOM参照、オブジェクト、その他)があると仮定して、各要素に固有のコールバックを設定するという問題が発生した場合、次のようにすることができます。

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

.forEach`ループで使用されるコールバック関数の呼び出しは、それぞれ独立したクロージャになるということです。ハンドラに渡されるパラメータは、反復の特定のステップに固有の配列要素です。これを非同期のコールバックで使用すれば、反復の他のステップで確立された他のコールバックと衝突することはありません。

jQueryをお使いの場合は、$.each()関数で同様の機能を利用できます。


ES6ソリューション: let

ECMAScript 6 (ES6) では、新しい let および const キーワードが導入され、var ベースの変数とは異なるスコープで扱われるようになりました。例えば、letベースのインデックスを持つループでは、ループを繰り返すたびに新しいiの値が得られ、各値はループ内にスコープされるので、あなたのコードは期待通りに動作するでしょう。多くの資料がありますが、2ality's block-scoping postが素晴らしい情報源としてお勧めです。

for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log("My value: " + i);
  };
}

ただし、IE9-IE11とEdge 14以前のEdgeはletをサポートしていますが、上記のように間違っているので注意してください(毎回新しいiを作成しないので、上記のすべての関数はvarを使用したときのように3を記録します)。Edge 14ではようやくそれが実現しました。

解説 (23)

試してみてください。

var funcs = [];

for (var i = 0; i < 3; i++) {
    funcs[i] = (function(index) {
        return function() {
            console.log("My value: " + index);
        };
    }(i));
}

for (var j = 0; j < 3; j++) {
    funcs[j]();
}

編集(2014年)。

個人的には、@Austさんの.bindの使用に関する最近の回答が、この種のことをするのに最も良い方法だと思います。lo-dash/underscoreの_.partialも、bindthisArgを使う必要がない、あるいは使いたくない場合には有効です。

解説 (3)

最初の例がうまくいかなかったのは、ループ内で作成したクロージャがすべて同じフレームを参照していたからです。事実上、1つのオブジェクトに3つのメソッドがあり、変数は1つの i だけです。これらはすべて同じ値を出力します。

解説 (0)