2021年12月15日

変数スコープ、クロージャ

JavaScript は非常に関数指向な言語であり、これにより多くの自由があります。ある時点で作成した関数を別の変数にコピーしたり別の関数に引数として渡し、後でまったく別の場所から呼ぶことができます。

私たちはすでに関数がその外(“外側” の変数)にアクセスできることを知っています

ですが、関数が作成されたあとで外側の変数を変更すると何が起きるでしょうか?関数は最新の値 or 古い値どちらを取得するのでしょうか?

また、関数がパラメータとして渡され、別のコード部分から呼び出される場合、新しい場所での外側の変数にアクセスできるのでしょうか?

これらのシナリオやより複雑なシナリオを理解するために、知識を広げていきましょう。

ここでは let/const の変数について話します

JavaScript では変数を宣言する方法が3通りあります: let, const(モダンな方法),とvar (過去の名残)です。

  • この記事では、例には let を使用します。
  • const で宣言された変数は同じように動作するため、この記事は const についても当てはまります。
  • 古い var はいくつか大きな違いがあり、それらは 古い "var" の記事で説明しています。

コードブロック

変数がコードブロック {...} の中で宣言された場合は、そのブロックの中でだけ見えます。

例:

{
  // 外には見せる必要のない、ローカル変数で処理をする

  let message = "Hello"; // このコードブロックでだけ見えます。

  alert(message); // Hello
}

alert(message); // Error: message is not defined

これを使用して、独自のタスクを実行するコード部分を、そのタスクにのみ属する変数で分離することができます。:

{
  // メッセージを表示
  let message = "Hello";
  alert(message);
}

{
  // 別のメッセージを表示
  let message = "Goodbye";
  alert(message);
}
ブロックがないとエラーが発生します

存在する変数名で let を使用しようとすると、分離するブロックがないとエラーになることに注意してください。:

// メッセージヲ表示
let message = "Hello";
alert(message);

// 別のメッセージヲ表示
let message = "Goodbye"; // Error: variable already declared
alert(message);

if, for, while などの場合も、{...} の中で宣言された変数は、その内側でだけ見えます:

if (true) {
  let phrase = "Hello!";

  alert(phrase); // Hello!
}

alert(phrase); // Error, そのような変数は有りません!

ここでは if が終わったあとで、次の alertphrase は見えないため、エラーになります。

これにより、if 分岐固有のブロックレベルのローカル変数が作成できるので、素晴らしいことです。

同様のことが forループとwhileループにも当てはまります。

for (let i = 0; i < 3; i++) {
  // 変数 i はこの for の中でだけ見えます
  alert(i); // 0, 1, 2
}

alert(i); // Error, no such variable

視覚的には let i{...} の外側に見えますが、ここでは for 構文は特別です。この中で宣言された変数はブロックの一部とみなされます。

ネストされた関数

別の関数の内部で作成される関数は、“ネストされた” 関数と呼ばれます。

JavaScript では簡単に実現できます。

これは、次のようにコードを整理するのに利用できます:

function sayHiBye(firstName, lastName) {

  // 下で使うネストされたヘルパー関数です
  function getFullName() {
    return firstName + " " + lastName;
  }

  alert( "Hello, " + getFullName() );
  alert( "Bye, " + getFullName() );

}

ここでの ネストされた 関数 getFullName() は利便性のために作られています。それは外部変数にアクセスすることができるので、フルネームを返すことができます。

さらに興味深い点は、新しいオブジェクトのプロパティ(外部関数がメソッドを持つオブジェクトを作成する場合)またはその自身の結果として、ネストされた関数を返すことができることです: それは他の場所で使うことができます。どこにいても、同じ外部変数には依然としてアクセスできます。

下記の makeCounter は実行ごとに次の数を返す “counter” 関数を作成します。

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

単純ですが、このコードをわずかに変更したバリアントは実践で使用されています。例えば、自動テスト用の乱数を生成するための乱数ジェネレータ(random number generator) があります。

これはどのように動作するでしょうか?複数の counter を作成する場合、これらは独立しますか?変数はどのようになっているでしょうか。

このようなことを理解するのは、JavaScriptの全体的な知識にとって素晴らしいことであり、より複雑なシナリオでは有益です。では、より詳しくみていきましょう。

レキシカル/語彙環境(Lexical Environment)

Here be dragons!

この先には詳細な技術的説明があります。

低水準言語の詳細は避けたいところですが、それを抜きにして理解することは不十分で不完全なものになるため、準備してください。

わかりやすくするために、複数のステップに分けて説明しています。

Step 1. 変数

JavaScript では、実行中のすべて関数、コードブロック {...} およびスクリプト全体には、レキシカル環境 と呼ばれる内部の(隠れた)関連オブジェクトがあります。

レキシカル環境オブジェクトは2つの部分から構成されます:

  1. 環境レコード(Environment Record) 。プロパティとしてすべてのローカル変数をもつオブジェクトです(this の値など、他の情報もいくらか持っています)。
  2. 外部のレキシカル環境 への参照。通常、直近の外部のレキシカルなコードに関連付けられています(現在の波括弧の外側)。

“変数” は単に、特別な内部オブジェクトである 環境レコード のプロパティです。“変数を取得または変更する” とは、“そのオブジェクトのプロパティを取得または変更する” ことを意味します。

例えば、この簡単なコードでは、レキシカル環境は1つだけあります。:

これは、スクリプト全体に関連付けられた、いわゆるグローバルレキシカル環境です。

上の図の長方形は環境レコード(変数ストア)で、矢印(outerの部分)は外部参照を意味します。グローバルレキシカル環境は外部参照を持っていないので、 null です。

コードが実行されていくと、レキシカル環境は変化していきます。

ここでは少し長いコードを紹介します:

右側の長方形は、実行の間でどのようにグローバルレキシカル環境が変わるかを示しています。:

  1. スクリプトを開始すると、レキシカル環境には宣言されたすべての変数があらかじめ用意されています。
    • 最初は、“未初期化” の状態です。これは特別な内部状態で、エンジンは変数は知っていますが、let で宣言されるまでは参照できないことを意味します。変数が存在しないのとほぼ同じです。
  2. その後 let phrase 定義が現れました。今は初期値がないので、 undefined が格納されます。この時点からこの変数を使用することができます。
  3. phrase に値が割り当てられます。
  4. phrase の値を変更します。

今のところすべてシンプルに見えますね。

  • 変数は特別な内部オブジェクトのプロパティで、現在の実行ブロック/関数/スクリプトと関連付けられています。
  • 変数を使った作業は、実際にはそのオブジェクトのプロパティを使って作業しています。
レキシカル環境は仕様上のオブジェクトです

“レキシカル環境” は仕様上のオブジェクトです。これは 言語仕様 の中で動作の仕組みを説明するために、「理論的に」存在するものです。このオブジェクトをコード上で取得したり直接操作することはできません。

また、JavaScript エンジンは、目に見える振る舞いはそのままな上で、最適化したり、メモリを節約するために未使用の変数を破棄したり、他の内部的なトリックを実行することがあります。

Step 2. 関数宣言

関数も変数のように値です。

違いは、関数宣言は即座に完全に初期化されることです

レキシカル環境が作られたとき、関数宣言はすぐに使用できる関数(宣言まで使用できない let とは異なります)になります。

そのため、宣言自体の前でも関数宣言として宣言された関数を呼び出すことができます。

例えば、以下は関数を追加したときのグローバルレキシカル環境の初期状態です:

当然、この動作は関数宣言にのみ適用され、let say = function(name)... のように変数に関数を割り当てる関数式にはあてはまりません。

Step 3. 内外のレキシカル環境

関数が実行されると、呼び出しの先頭では新しいレキシカル環境が自動的に作られ、ローカル変数と呼び出しパラメータを格納します。

例えば、say("John") の場合、このようになります(実行は矢印でラベル付けされた行です):

関数呼び出し中は2つのレキシカル環境があります: 内部のもの(関数呼び出し用)と外部のもの(グローバル)です:

  • 内部のレキシカル環境は現在の say の実行に対応しています。1つ変数(name)を持っており、それは関数の引数です。私たちは say("John") を呼び出したので、 name"John" です。
  • 外部のレキシカル環境はグローバルレキシカル環境で、変数 phrase と関数自身があります。

内部のレキシカル環境は外部のものへの 外部 参照を持っています。

コードが変数にアクセスしたいとき、最初に内部のレキシカル環境を探します。その次に外側を探し、チェーンの最後になるまで繰り返します。

もし変数がどこにもない場合、strict モードではエラーになります。use strict がなければ、未定義変数への代入は下位互換性のために新しいグローバル変数を作成します。

今回の例でどのように探索されるか見てみましょう:

  • say の内側にある alertname にアクセスしたいとき、関数のレキシカル環境の中からすぐに見つけます。
  • phrase にアクセスしたいとき、ローカルには phrase がないので、続いて 外部 参照を行い、グローバルでそれを見つけます。

Step 4. 関数を返す

makeCounter の例に戻りましょう。

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

makeCounter() 呼び出しの最初に、新しいレキシカル環境オブジェクトが作成され、この makeCounter の実行のための変数が格納されます。

なので、ちょうど上の例のように、2つのネストしたレキシカル環境があります:

違いは、makeCounter() の実行中に return count++ という1行だけの小さな入れ子の関数が作られることです。まだ実行はしていませんが、作成だけはしています。

すべての関数は、それらが作成されたレキシカル環境を記憶しています。すべての関数は [[Environment]] という名前の隠しプロパティをもち、その関数が作成されたレキシカル環境への参照を保持します。

なので、counter.[[Environment]] には {count: 0} レキシカル環境への参照があります。このようにして、関数はどこで呼ばれても、どこで作成されたかを覚えているのです。[[Environment]] への参照は,関数の生成時に一度だけ,そして永遠に設定されます.

その後、counter() が呼ばれたとき、その呼び出しに対する新たなレキシカル環境が作成され、その外部レキシカル環境の参照は counter.[[Environment]] から取得されます:

これで、counter() 内のコードが変数 count を探すとき、最初に自身のレキシカル環境を検索します(ここにはローカル変数はないので空です)。次に外部の makeCounter() 呼び出しのレキシカル環境を探し、変数を見つけ更新します。

変数は、その変数が存在するレキシカル環境で更新されます。

実行後の状態がこちらです:

counter() を何度も呼び出すと、変数 count2, 3 と増えていきます。

クロージャ

開発者が一般的に知っておくべき、一般的なプログラミング用語 “クロージャ” があります。

クロージャ(closure) は外部変数を記憶し、それらにアクセスできる関数です。いくつかの言語ではそれは不可能、もしくはそれを実現するために特別な方法で関数を書く必要があります。しかし、上で説明したとおり、JavaScriptにおいては、すべての関数は自然にクロージャです(1つだけ例外があります。それについては "new Function" 構文 で説明します)。

つまり: それらは隠された [[Environment]]プロパティを使ってどこに作成されたのかを自動的に覚えていて、すべてが外部変数にアクセスできます。

面接でフロントエンドの開発者が「クロージャは何ですか?」という質問を受けたとき、有効な回答は、クロージャの定義と、JavaScriptにおいてはすべての関数がクロージャであること、また [[Environment]] プロパティとレキシカル環境の仕組みと言った技術的に詳細な用語です 。

ガベージコレクション

通常、関数呼び出しが終わった後、すべての変数のとともに、レキシカル環境はメモリから削除されます。これはそこへの参照が存在しないためです。他のJavaScriptオブジェクトと同様に、到達可能な間だけメモリに保持されます。

しかし、関数の終了後も依然として到達可能なネストされた関数がある場合、それはレキシカル環境への参照である [[Environment]] プロパティを持ちます。

この場合、レキシカル環境は関数の完了後も依然として到達可能なので、存在し続けます。

例:

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // g.[[Environment]] は、対応する f() 呼び出しのレキシカル環境
// への参照を保持します

もし f() が何度も呼ばれ、結果の関数が保持される場合、対応するレキシカル環境オブジェクトもまたメモリに残ります。下のコードでは3つすべて:

function f() {
  let value = Math.random();

  return function() { alert(value); };
}

// 配列に3つの関数があり、それぞれが対応する f() からの
// レキシカル環境と関連づいています
let arr = [f(), f(), f()];

レキシカル環境オブジェクトは到達不能になったときに死にます。つまり、少なくとも1つの入れ子になった関数がそれを参照している間だけ存在します。

下のコードでは、 ネストされた関数が削除された後、それを囲んでいたレキシカル環境(と value)はメモリからクリアされます:

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // g が生きている間、値はメモリに残り続けます

g = null; // ...今、メモリはクリーンアップされます

現実の最適化(Real-life optimizations)

これまで見てきたように、理論的には関数が生きている間、すべての外部変数も保持されます。

しかし、実際にはJavaScriptエンジンはそれを最適化しようとします。変数の使用状況を分析し、外部変数が使用されていないことがわかりやすい場合は削除されます。

V8(Chrome, Edge, Opera)の重要な副作用はこのような変数はデバッグでは利用できなくなることです。

Chromeで Developer Tools を開いて下の例を実行してみてください。

一時停止したとき、console で alert(value) を入力してください。

function f() {
  let value = Math.random();

  function g() {
    debugger; // in console: type alert( value ); No such variable!
  }

  return g;
}

let g = f();
g();

ご覧の通り、このような変数はありません! 理論的にはアクセスできるはずですが、エンジンが最適化しています。

これは(それほど時間がかからないとしても)おかしなデバッグの問題につながる可能性があります。期待している変数の代わりに、同じ名前の外部変数が参照されます:

let value = "Surprise!";

function f() {
  let value = "the closest value";

  function g() {
    debugger; // in console: type alert( value ); Surprise!
  }

  return g;
}

let g = f();
g();

V8 のこの機能は知っておくと良いです。もしも Chrome/Edge/Opera でデバッグしている場合、遅かれ早かれこれに遭遇するでしょう。

これはデバッガのバグではなく、V8の特別な機能です。時々変わるかもしれません。このページの例を実行することで、いつでもチェックすることができます。

タスク

重要性: 5

関数 sayHi は外部の変数を使用しています。関数実行時、どの値が利用されるでしょう?

let name = "John";

function sayHi() {
  alert("Hi, " + name);
}

name = "Pete";

sayHi(); // 何が表示される?: "John" or "Pete"?

このような状況はブラウザとサーバサイドの開発両方で共通です。例えばユーザ操作やネットワークリクエストなど、関数は作成されたときよりも後に実行するようスケジュールすることができます。

したがって、問題は: 最新の変更を取得しますか?

答えは: Pete です。

関数は “今” の外部変数を取得し、最新の値を利用します。

古い変数の値はどこにも保存されません。関数が変数を必要とするとき、自身、あるいは外部のレキシカル環境から現在の値を取得します。

重要性: 5

以下の関数 makeWorker は別の関数を作りそれを返します。その新しい関数はどこからでも呼ぶことが可能です。

これは作成された場所から外部の変数へアクセスするでしょうか?それとも実行された場所から?あるいはその両方?

function makeWorker() {
  let name = "Pete";

  return function() {
    alert(name);
  };
}

let name = "John";

// 関数を作成
let work = makeWorker();

// 呼び出し
work(); // 何が表示されるでしょう?

“Pete” or “John” どちらの値が表示されるでしょう?

答えは: Pete です。

以下のコードの work() 関数は、外部のレキシカル環境参照を介して、元の場所から name を取得します。:

したがって、結果は "Pete" になります。

しかし、makeWorker()let name が無かった場合は、検索は外部に向かい、上のチェーンの通りグローバル変数を取ります。この場合、結果は "John" になります。

重要性: 5

ここで2つのカウンタを作ります: 同じ makeCounter 関数を使って countercounter2 を作ります。

それらは独立していますか?2つ目のカウンタは何が表示されるでしょうか? 0,1 or 2,3 or その他?

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();
let counter2 = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1

alert( counter2() ); // ?
alert( counter2() ); // ?

答え: 0,1.

関数 countercounter2 は異なる makeCounter の呼び出しで作られています。

そのため、それらは独立した外部のレキシカル環境を持っており、それぞれ独自の count を持ちます。

重要性: 5

これはコンストラクタ関数の助けを借りて作られたカウンタオブジェクトです。

これは動作するでしょうか?何が表示されるでしょう?

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };
  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // ?
alert( counter.up() ); // ?
alert( counter.down() ); // ?

もちろん正常に動作します。

両方の入れ子関数は同じ外部レキシカル環境内で作られているので、同じ count 変数へのアクセスを共有します。:

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };

  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // 1
alert( counter.up() ); // 2
alert( counter.down() ); // 1

このコードを見てください。最後の行の呼び出しの結果は何でしょうか?

let phrase = "Hello";

if (true) {
  let user = "John";

  function sayHi() {
    alert(`${phrase}, ${user}`);
  }
}

sayHi();

結果は エラー です。

関数 sayHiif の内側で宣言されているので、その中でのみ生きています。外部に sayHi はありません。

重要性: 4

sum(a)(b) = a+b のように動作する関数 sum を書いてください。

正確にこの通り2つの括弧で指定します(タイプミスではありません)。

例:

sum(1)(2) = 3
sum(5)(-1) = 4

2つ目の括弧が動作するために、1つ目は関数を返さなければなりません。

このようになります:

function sum(a) {

  return function(b) {
    return a + b; // 外部のレキシカル環境から "a" を取る
  };

}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1) ); // 4
重要性: 4

このコードの結果はどうなるでしょう?

let x = 1;

function func() {
  console.log(x); // ?

  let x = 2;
}

func();

P.S. このタスクには落とし穴があります。

結果は: エラー です。

実行してみましょう:

let x = 1;

function func() {
  console.log(x); // ReferenceError: Cannot access 'x' before initialization
  let x = 2;
}

func();

この例では、“存在しない” 変数と “初期化されていない” 変数の変わった違いを見ることができます。

記事 変数スコープ、クロージャ でご覧になったかもしれませんが、変数は実行がコードブロック(または関数)に入った時点から、“初期化されていない” 状態 で始まります。そして対応する let 文まで初期化されません。

つまり、変数は技術的には存在しますが、let 以前では利用できません。

上のコードを説明します。

function func() {
  // エンジンは関数の最初の時点でローカル変数 x を知っています。
  // が、let までは "初期化されていません" (利用できません)
  // なのでエラーです

  console.log(x); // ReferenceError: Cannot access 'vx before initialization

  let x = 2;
}

変数が一時的に利用できないゾーン(コードブロックの先頭から let まで)は “デッドゾーン” と呼ばれることがあります。

重要性: 5

私たちは、配列に対する組み込みのメソッド arr.filter(f) を持っています。それはすべての要素を関数 f を通してフィルタします。もし true を帰す場合、その要素は結果の配列に入れられます。

“すぐに使える” フィルタを作りましょう:

  • inBetween(a, b)ab の間、またはそれらと等しい(包括的)
  • inArray([...]) – 与えられた配列に存在する

使用方法はこのようになります:

  • arr.filter(inBetween(3,6)) – 3 から 6 までの値だけを選び出します
  • arr.filter(inArray([1,2,3]))[1,2,3] のメンバの1つにマッチする要素だけを選び出します

例:

/* .. your code for inBetween and inArray */
let arr = [1, 2, 3, 4, 5, 6, 7];

alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

alert( arr.filter(inArray([1, 2, 10])) ); // 1,2

テストと一緒にサンドボックスを開く

Filter inBetween

function inBetween(a, b) {
  return function(x) {
    return x >= a && x <= b;
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

Filter inArray

function inArray(arr) {
  return function(x) {
    return arr.includes(x);
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
function inArray(arr) {
  return x => arr.includes(x);
}

function inBetween(a, b) {
  return x => (x >= a && x <= b);
}

サンドボックスでテストと一緒に解答を開く

重要性: 5

ソートするオブジェクトの配列を持っているとします。:

let users = [
  { name: "John", age: 20, surname: "Johnson" },
  { name: "Pete", age: 18, surname: "Peterson" },
  { name: "Ann", age: 19, surname: "Hathaway" }
];

それをするための通常の方法はこのようになります:

// by name (Ann, John, Pete)
users.sort((a, b) => a.name > b.name ? 1 : -1);

// by age (Pete, Ann, John)
users.sort((a, b) => a.age > b.age ? 1 : -1);

私たちはこのように冗長さを無くすことはできますか?

users.sort(byField('name'));
users.sort(byField('age'));

つまり、関数を角換わりに byField(fieldName) を置きます。

このために使う byField 関数を書いてください。

let users = [
  { name: "John", age: 20, surname: "Johnson" },
  { name: "Pete", age: 18, surname: "Peterson" },
  { name: "Ann", age: 19, surname: "Hathaway" }
];

function byField(field) {
  return (a, b) => a[field] > b[field] ? 1 : -1;
}

users.sort(byField('name'));
users.forEach(user => alert(user.name)); // Ann, John, Pete

users.sort(byField('age'));
users.forEach(user => alert(user.name)); // Pete, Ann, John
重要性: 5

次のコードは shooters の配列を作ります。

すべての関数は、その番号を出力するためのものです。しかし、どこか間違っています…

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function() { // 射手(shooter) 関数
      alert( i ); // その番号を表示するべき
    };
    shooters.push(shooter);
    i++;
  }

  return shooters;
}

let army = makeArmy();

army[0](); // 射手 番号 0 表示 10
army[5](); // また 番号 5 ですが表示は 10...
// ... すべての射手は 0, 1, 2, 3... の代わりに 10 が表示されます

なぜすべての射手(shooters)は同じものが表示されるのでしょう?期待通りに動作するようコードを直してください。

テストと一緒にサンドボックスを開く

makeArmy の中で行われていることを検査してみましょう、それで解決策が明白になるでしょう。

  1. 空の配列 shooters を作ります:

    let shooters = [];
  2. ループで、shooters.push(function...) を通してそれを埋めます。

    すべての要素は関数なので、結果の配列はこのように見えます:

    shooters = [
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); }
    ];
  3. 配列が関数から返却されます。

次に、army[5]() の呼び出しは、その配列から army[5] の要素を取得(それは関数になります)し、呼び出します。

さて、なぜすべての関数は同じものを表示するのでしょう?

それは、shooter 関数の内側にローカル変数 i がないからです。このような関数が呼ばれるとき、i はその外部のレキシカル環境から取られます。

i の値は何になるでしょう?

ソースを見ると:

function makeArmy() {
  ...
  let i = 0;
  while (i < 10) {
    let shooter = function() { // shooter function
      alert( i ); // should show its number
    };
    ...
  }
  ...
}

i は現在の makeArmy() の実行に関連付けられたレキシカル環境で生きているのがわかります。しかし、army[5]() が呼ばれたとき、makeArmy はすでにその処理を終えているので、i は最後の値である 10 (while の最後) です。

結果として、すべての shooter 関数は外部のレキシカル環境から同じ最後の値 i=10 を取ります。

修正はとてもシンプルです。:

function makeArmy() {

  let shooters = [];

  for(let i = 0; i < 10; i++) {
    let shooter = function() { // shooter function
      alert( i ); // should show its number
    };
    shooters.push(shooter);
  }

  return shooters;
}

let army = makeArmy();

army[0](); // 0
army[5](); // 5

これで、正しく動きます。なぜなら、for (..) {...} で毎回コードブロックが実行され、i の値に対応する新しいレキシカル環境が作られるからです。

従って、i の値は今は少し近くにあります。makeArmy レキシカル環境ではなく、現在のループイテレーションに対応するレキシカル環境の中です。shooter は作られたレキシカル環境から値を取り出します。

これは、whilefor で書き直しました。

別のトリックでも可能です。この話題をより理解するために次のコードを見てみましょう。:

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let j = i;
    let shooter = function() { // shooter function
      alert( j ); // should show its number
    };
    shooters.push(shooter);
    i++;
  }

  return shooters;
}

let army = makeArmy();

army[0](); // 0
army[5](); // 5

ちょうど for と同じように while ループは各実行に対するレキシカル環境を作ります。従って、shooter の正しい値を取得することが確認できます。

私たちは let j = i のコピーをしています。これはループ本体のローカル j を作り、i の値をコピーします。プリミティブは “値によって” コピーされるので、現在のループイテレーションに属する実際に完全に独立した i のコピーになります。

function makeArmy() {

  let shooters = [];

  for(let i = 0; i < 10; i++) {
    let shooter = function() { // shooter function
      alert( i ); // should show its number
    };
    shooters.push(shooter);
  }

  return shooters;
}

サンドボックスでテストと一緒に解答を開く

チュートリアルマップ