ループ内のJavaScriptクロージャ - 簡単な実例 質問する

ループ内の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>

… または非同期コード(例:Promise を使用):

// 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));
}

for inこれは、 およびfor ofループでも明らかです。

const arr = [1,2,3];
const fns = [];

for (var i in arr){
  fns.push(() => console.log("index:", i));
}

for (var v of arr){
  fns.push(() => console.log("value:", v));
}

for (const n of arr) {
  var obj = { number: n }; // or new MyLibObject({ ... })
  fns.push(() => console.log("n:", n, "|", "obj:", JSON.stringify(obj)));
}

for(var f of fns){
  f();
}

この基本的な問題の解決策は何でしょうか?

ベストアンサー1

問題は、i各匿名関数内の変数が、関数外の同じ変数にバインドされていることです。

ES6ソリューション:let

ECMAScript 6 (ES6)では、 ベースの変数とは異なるスコープを持つ新しいキーワードletとキーワードが導入されました。たとえば、 ベースのインデックスを持つループでは、ループの各反復でループスコープを持つ新しい変数が作成されるため、コードは期待どおりに動作します。多くのリソースがありますが、constvarleti2ality のブロックスコープの投稿素晴らしい情報源として。

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

ただし、IE9 ~ IE11 および Edge 14 より前の Edge は をサポートしていますletが、上記は間違っていることに注意してください (毎回新しい を作成しないためi、 を使用した場合と同様に、上記のすべての関数は 3 をログに記録しますvar)。Edge 14 では最終的に正しく実行されます。


ES5.1 ソリューション: 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()関数により同様の機能が提供されます。


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

やりたいことは、各関数内の変数を、関数外の別の不変の値にバインドすることです。

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」の値が意図したとおりに維持されることが保証されます。

おすすめ記事