forループの中で配列にループ変数扱う関数を埋め込みたいが…
// sample1.js var funcs = []; for (var i = 0; i < 3; i++) { funcs[i] = function() { console.log("My value: " + i); }; } for (var j = 0; j < 3; j++) { funcs[j](); }
以下の通り出力することを期待しているが
My value: 0 My value: 1 My value: 2
実際には以下の通り。
My value: 3 My value: 3 My value: 3
なぜか?
内部を見てみる
funcs[j]();
を実行する前に、funcs[]の要素の中身がどうなっているか見てみる。
// sample2.js var funcs = []; for (var i = 0; i < 3; i++) { funcs[i] = function() { console.log("My value: " + i); }; } for (var j = 0; j < 3; j++) { console.dir(funcs[j]); // 対象のプロパティを表示 console.log(funcs[j].toString()); // 対象を文字列化 funcs[j](); }
sample2.jsの実行結果
[Function] function() { console.log("My value: " + i); } My value: 3 [Function] function() { console.log("My value: " + i); } My value: 3 [Function] function() { console.log("My value: " + i); } My value: 3
funcsの要素の中身は関数式である。
関数名はない。(無名関数)
関数リテラルが入っていることがわかる。
クロージャ
javascriptの関数はすべて「クロージャ(closures)」である。(または、javascriptの関数はすべてクロージャに「なりうる。」)
(MDN クロージャより) この理由は、JavaScript の関数はクロージャとなるためです。クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。この環境は、クロージャが作られた時点でスコープ内部にあったあらゆる変数によって構成されています。
よって funcsの要素はクロージャである。
このことをもう少しわかりやすくするために以下の通り変形する。
// sample-3.js var funcs = []; var i = 0; // forループから移動 function createFunc() { return function() { console.log("My value: " + i); }; } for (; i < 3; i++) { // var i = 0;を上に移動 funcs[i] = createFunc(); } console.log("i = " + i); for (var j = 0; j < 3; j++) { funcs[j](); }
クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。この環境は、クロージャが作られた時点でスコープ内部にあったあらゆる変数によって構成されています。
上記から、createFunc()で生成しfuncs[]の要素として代入する
function() { console.log("My value: " + i); }; }
と
var i;
は組み合わさってクロージャとなっていることがわかる。(一般的には、「関数が対象変数を「束縛(バインド)」している」という。)
つまり
funcs[0] funcs[1] funcs[2]
の中身の関数式はいずれも変数iを束縛していることがわかる。
ところで、プログラム中に
console.log("i = " + i);
を追加した。
forループを脱出すると変数iの中身は「3」になるため、sample-3.jsの実行結果は最初に
i = 3
を出力する。
for (var j = 0; j < 3; j++) { funcs[j](); }
を実行するときには変数iの中身は3になっているため、変数iを束縛している
funcs[0] funcs[1] funcs[2]
のいずれの中身(関数式)も「My value:3」を出力する。
sample-3.jsの実行結果は以下の通り
i = 3 My value:3 My value:3 My value:3
では
My value: 0 My value: 1 My value: 2
と出力するためにはどうしたらよいか。
方法1:束縛対象を「関数式を作る関数の引数」に変更する
問題の原因は、グローバル変数である変数iを束縛していることである。
もっとスコープが狭く、かつループが行われても影響を受けない変数を束縛対象にすれば良い。
例えば関数の引数。
よって以下の通り修正する。
// sample-4.js var funcs = []; var i = 0; //i スコープはこのプログラムの末尾まで function createFunc(x) { // 変数xのスコープはcreateFunc内 console.log("x = " + x); return function() { console.log("My value: " + x); }; } for (; i < 3; i++) { funcs[i] = createFunc(i); } console.log("i = " + i); for (var j = 0; j < 3; j++) { funcs[j](); }
これでfuncs[]の要素の関数は、自身を生み出すcreateFunc関数の引数xを束縛した。
for (var i = 0; i < 3; i++) { funcs[i] = createFunc(i); }
createFunc(0)として呼ばれて関数式を作成したときは、「0」が入った変数xを束縛する。
createFunc(1)として呼ばれて関数式を作成したときは、「1」が入った変数xを束縛する。
createFunc(2)として呼ばれて関数式を作成したときは、「2」が入った変数xを束縛する。
createFunc()を抜けても変数xを束縛をし続ける。
また、
createFunc(0)として呼ばれときの関数の引数である変数xと
createFunc(1)として呼ばれときの関数の引数である変数xと
createFunc(2)として呼ばれときの関数の引数である変数xは
それぞれ別の環境であるため、お互いに影響を受けない。
sample-4.jsの実行結果は以下の通り。
x = 0 x = 1 x = 2 i = 3 My value: 0 My value: 1 My value: 2
方法2:束縛対象をletに変更する
そもそもforループでグローバル変数を生み出すvarを使うことが問題なので、varの代わりに「let」を使う。
(MDN letより) let を使用することで、変数のスコープをそれが使用されたブロック、文または式に限定することができます。これは var キーワードとは異なり、グローバル変数を定義したり、ブロックスコープに留まらない関数全体でのローカル変数を定義したりしません。
// sample-5.js var funcs = []; for (let i = 0; i < 3; i++) { // iのスコープはfor内 funcs[i] = function() { console.log("My value: " + i); }; } for (var j = 0; j < 3; j++) { funcs[j](); }
1回目のforループで関数式を作成したときは、「0」が入った変数iを束縛する。
2回目のforループで関数式を作成したときは、「1」が入った変数iを束縛する。
3回目のforループで関数式を作成したときは、「2」が入った変数iを束縛する。
forループブロックを抜けても変数iを束縛をし続ける。
1回目のforループ変数iと
2回目のforループ変数iと
3回目のforループ変数iは
それぞれ別の環境であるため、お互いに影響を受けない。
sample-5.jsの実行結果は以下の通り。
My value: 0 My value: 1 My value: 2
参考資料:
以下にそのものズバリ説明してある。
- MDN クロージャの「よくある間違い: ループ内でクロージャを作成する」
- JavaScript closure inside loops – simple practical example