JavaScript: forループの中で配列にループ変数扱う関数(クロージャ)を埋め込みたい

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

参考資料:

以下にそのものズバリ説明してある。