ループ内のJavaScriptクロージャ - 簡単な実用的な例 [Javascript]

このエントリーをはてなブックマークに追加

質問:

var funcs = [];
for (var i = 0; i < 3; i++) { // 関数を3つ作る
funcs[i] = function() { // それらをfuncsに入れる
console.log("My value: " + i); // 各関数はその値を記録する
};
}
for (var j = 0; j < 3; j++) {
funcs[j](); // 走らせる
}

でもこれは以下を出力してしまう:

My value: 3
My value: 3
My value: 3

でも僕が欲しいのは

My value: 0
My value: 1
My value: 2


同じ問題は、関数の実行がイベントリスナーを使用することによって遅延してしまう時にも起こる:

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

この基本的な問題の解決策ってある??

By nickf | 質問日時: 2009年4月15日 6:06



回答1:

えっと、問題は、それぞれの匿名の関数の中にある変数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++) {
funcs[j](); // 表示させてみる
}

JavaScriptにはブロックスコープがないので(関数スコープのみ)、新しい関数の中で関数の作成をラップすると、
"i"は君が意図した通りになるように気をつけないといけない。


Update:

var someArray = [ /* なんでもいい */ ];
// ...
someArray.forEach(function(arrayElement) {
// ...この要素のコードコードコード
someAsynchronousFunction(arrayElement, function() {
arrayElement.doSomething();
});
});

つまり、.forEachループと一緒に使われたコールバック関数の各呼び出しが独自のクロージャになる。そのハンドラに渡されたパラメータは、その特定の反復ステップに固有の配列要素になってる。非同期コールバックで使用されている場合、それはその反復の他のステップで確立された他のコールバックどれとも衝突しない。

もしjQueryで作業している場合なら、$ .each()関数で同じようなことができるよ。

Update 2: JavaScriptの最新バージョンであるECMAScript 6(ES6)は、多くのブラウザとバックエンドシステムで実装され始めてる。古いシステムで新しい機能を使用できるようにES6からES5に変換するBabelのようなトランスパイライザーとかももあります。

ES6では、varベースの変数とは違うスコープを持つ新しい letconstキーワードが導入されてる。たとえば、 letベースのインデックスを持つループでは、それぞれループを通る反復は新しい値「i」を持ち、各値はループの内側にスコープがあるので、君のコードは期待どおりに動作するはず。いろんなリソースがあると思うけど、僕は2alityのブロックスコープに関する記事の素晴らしいソースとしておすすめするよ。

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

でも注意すべきはIE9-IE11とEdge14以前のEdgeは、letをサポートしてるけど、上記の間違いをしてしまう(毎回新しい iを作成しないので、上で書いたどの関数もvarを使用した場合と同様に「3」を出力する)。Edge 14では最終的にそれは解決してる。

By harto | 回答日時: 2009年4月15日 6:18



回答2:

最初にループの外側で関数を作り、その結果をループ内でバインドさせる方がパフォーマンスがいいと思うよ。

function log(x) {
console.log('My value: ' + x);
}
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = log.bind(this, i);
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}

By Aust | 回答日時: 2013年10月11日 16:41



Source: JavaScript closure inside loops – simple practical example

共有 コメント