1. ホーム
  2. javascript

[解決済み] ループ内のJavaScriptクロージャ - シンプルな実用例

2022-03-15 08:45:49

質問

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 infor 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(var f of fns){
  f();
}

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

どのように解決するのか?

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

ES6で解決。 let

ECMAScript 6 (ES6) では、新しい letconst とは異なるスコープを持つキーワード。 var -をベースにした変数です。例えば、ループの中で let -をベースとしたインデックスが作成されると、ループの各反復処理で新しい変数 i をループスコープに設定することで、あなたのコードは期待通りに動作するはずです。いろいろな資料がありますが、私がお勧めするのは 2alityのブロックスコープの記事 を参考にしてください。

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"の値が意図したとおりになることを保証しているのです。