2022年6月13日

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

JavaScriptは、非常に関数指向の言語です。それは私たちに多くの自由を与えてくれます。関数はいつでも作成でき、別の関数の引数として渡され、後でコードのまったく異なる場所から呼び出すことができます。

関数がその外部の変数(「外側の」変数)にアクセスできることは既に知っています。

しかし、関数が作成されてから外部の変数が変更された場合はどうなるでしょうか?関数は新しい値を取得するのでしょうか、それとも古い値を取得するのでしょうか?

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

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

ここでは let/const 変数について説明します

JavaScriptでは、変数を宣言する方法は3つあります。letconst(現代的なもの)、および var(過去の残存物)です。

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

コードブロック

変数がコードブロック {...} の中で宣言されている場合、そのブロック内でのみ可視になります。

例:

{
  // do some job with local variables that should not be seen outside

  let message = "Hello"; // only visible in this block

  alert(message); // Hello
}

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

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

{
  // show message
  let message = "Hello";
  alert(message);
}

{
  // show another message
  let message = "Goodbye";
  alert(message);
}
ブロックがないとエラーが発生します

既存の変数名で let を使用する場合、別のブロックがないとエラーが発生することに注意してください。

// show message
let message = "Hello";
alert(message);

// show another message
let message = "Goodbye"; // Error: variable already declared
alert(message);

ifforwhile などについては、{...} で宣言された変数も内部でのみ可視になります。

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

  alert(phrase); // Hello!
}

alert(phrase); // Error, no such variable!

ここで、if が完了した後、下の alertphrase を認識しないため、エラーになります。

これにより、if ブランチに固有のブロックローカル変数を作成できるため、これは非常に便利です。

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

for (let i = 0; i < 3; i++) {
  // the variable i is only visible inside this for
  alert(i); // 0, then 1, then 2
}

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

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

ネストされた関数

関数が別の関数の内部で作成された場合、その関数は「ネストされた」と呼ばれます。

JavaScriptでは、これを簡単に行うことができます。

これを使って、以下のようにコードを整理できます

function sayHiBye(firstName, lastName) {

  // helper nested function to use below
  function getFullName() {
    return firstName + " " + lastName;
  }

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

}

ここで、ネストされた関数 getFullName() は利便性のために作成されています。外側の変数にアクセスできるため、フルネームを返すことができます。ネストされた関数はJavaScriptでは非常に一般的です。

さらに興味深いことに、ネストされた関数は返すことができます。新しいオブジェクトのプロパティとして、または結果として単独で返すことができます。その後、別の場所で使用できます。どこで使用しても、同じ外側の変数にアクセスできます。

以下では、makeCounter は呼び出しごとに次の数を返す「カウンター」関数を作成します。

function makeCounter() {
  let count = 0;

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

let counter = makeCounter();

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

簡単に見えますが、そのコードをわずかに変更したバリエーションには、たとえば、疑似乱数ジェネレーターとして、自動化されたテスト用の乱数値を生成するなど、実用的な用途があります。

これはどのように機能するのでしょうか?複数のカウンターを作成した場合、それらは独立するのでしょうか?ここでは変数に何が起きているのでしょうか?

このようなことを理解することは、JavaScript全体の知識に役立ち、より複雑なシナリオに役立ちます。したがって、少し深く掘り下げてみましょう。

レキシカル環境

ここにはドラゴンがいるかもしれません!

詳細な技術的な説明がこれからあります。

低レベルの言語の詳細を避けたいためですが、それらなしでは理解が不十分で不完全になるため、準備をしてください。

明確にするために、説明は複数のステップに分割されています。

ステップ 1. 変数

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

レキシカル環境オブジェクトは、次の2つの部分で構成されています

  1. 環境レコード – ローカル変数をプロパティとして(および this の値のようなその他の情報)保存するオブジェクト。
  2. 外部レキシカル環境への参照、つまり外側のコードに関連付けられているものです。

「変数」は、特別な内部オブジェクトである Environment Record のプロパティにすぎません。「変数を取得または変更する」とは、「そのオブジェクトのプロパティを取得または変更する」ことを意味します。

関数なしのこの単純なコードでは、レキシカル環境は1つしかありません

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

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

コードの実行が開始され、続行されるにつれて、レキシカル環境は変化します。

もう少し長いコードを次に示します

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

  1. スクリプトが開始すると、レキシカル環境は宣言されたすべての変数で事前に入力されます。
    • 最初は、それらは「未初期化」状態です。それは特別な内部状態であり、エンジンが変数を認識していることを意味しますが、let で宣言されるまで参照することはできません。それは、変数が存在しないのとほぼ同じです。
  2. 次に let phrase 定義が表示されます。まだ代入がないため、その値は undefined です。この時点から変数を前方で使用できます。
  3. phrase に値が代入されます。
  4. phrase の値が変更されます。

今のところ、すべてが単純に見えますね?

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

「レキシカル環境」は仕様オブジェクトです。言語仕様で、物事がどのように機能するかを説明するために「理論的に」のみ存在します。コード内でこのオブジェクトを取得して直接操作することはできません。

JavaScriptエンジンは、メモリを節約するために未使用の変数を破棄したり、記述されているように目に見える動作が維持されている限り、他の内部トリックを実行したりして、最適化することもできます。

ステップ 2. 関数宣言

関数も、変数のような値です。

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

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

そのため、関数宣言として宣言された関数は、宣言自体よりも前に使用することができます。

たとえば、関数を追加するときのグローバルレキシカル環境の初期状態を次に示します

当然ながら、この動作は関数宣言にのみ適用され、関数を変数に代入する関数式、たとえば let say = function(name)... などには適用されません。

ステップ 3. 内部および外部のレキシカル環境

関数が実行されると、呼び出しの開始時に、呼び出しのローカル変数とパラメーターを格納するために新しいレキシカル環境が自動的に作成されます。

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

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

  • 内部レキシカル環境は、say の現在の実行に対応します。単一のプロパティである関数引数 name があります。say("John") を呼び出したため、name の値は "John" です。
  • 外部のレキシカル環境は、グローバルレキシカル環境です。そこには phrase 変数と関数自体があります。

内部のレキシカル環境には、outer への参照があります。

コードが変数にアクセスしようとすると、最初に内部のレキシカル環境が検索され、次に外部のレキシカル環境、次にさらに外部のレキシカル環境が検索され、グローバルなレキシカル環境まで検索されます。

変数がどこにも見つからない場合、厳格モードではエラーになります(use strict がない場合、存在しない変数への代入は、古いコードとの互換性のために新しいグローバル変数を作成します)。

この例では、検索は次のように進みます

  • name 変数については、say 内の alert は、内部のレキシカル環境ですぐにそれを見つけます。
  • phrase にアクセスしようとすると、ローカルに phrase がないため、外部レキシカル環境への参照をたどり、そこでそれを見つけます。

ステップ 4. 関数の返却

makeCounter の例に戻りましょう。

function makeCounter() {
  let count = 0;

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

let counter = makeCounter();

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

そのため、上の例と同じように、2つのネストされたレキシカル環境があります

異なる点は、makeCounter() の実行中に、1行だけの小さなネストされた関数、return count++ が作成されることです。まだ実行するのではなく、作成するだけです。

すべての関数は、作成されたレキシカル環境を記憶します。技術的には、ここに魔法はありません。すべての関数には [[Environment]] という名前の非表示プロパティがあり、関数が作成されたレキシカル環境への参照が保持されます

さて、counter.[[Environment]]{count: 0} のレキシカル環境への参照を持っています。これが、関数がどこで呼び出されたかに関わらず、どこで作成されたかを記憶する方法です。[[Environment]] 参照は、関数作成時に一度だけ設定され、その後は変わりません。

後で counter() が呼び出されると、その呼び出しのために新しいレキシカル環境が作成され、その外側のレキシカル環境への参照は counter.[[Environment]] から取得されます。

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

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

以下は実行後の状態です。

もし counter() を複数回呼び出すと、count 変数は同じ場所で 23 のように増加します。

クロージャ

プログラミングには一般的に「クロージャ」と呼ばれる用語があり、開発者は通常知っておくべきものです。

クロージャは、外側の変数を記憶し、それにアクセスできる関数です。一部の言語では、それは不可能であったり、そうするために関数を特別な方法で記述する必要があったりします。しかし、上で説明したように、JavaScript では、すべての関数が自然にクロージャです(例外は 1 つだけで、「new Function」構文で説明します)。

つまり、関数は隠された [[Environment]] プロパティを使って、どこで作成されたかを自動的に記憶し、そのコードは外側の変数にアクセスできます。

面接でフロントエンド開発者が「クロージャとは何か?」という質問を受けた場合、妥当な回答は、クロージャの定義と、JavaScript のすべての関数がクロージャであるという説明、そして技術的な詳細についてのいくつかの言葉([[Environment]] プロパティとレキシカル環境の仕組み)かもしれません。

ガベージコレクション

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

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

その場合、レキシカル環境は関数の完了後も到達可能であるため、生き続けます。

例:

function f() {
  let value = 123;

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

let g = f(); // g.[[Environment]] stores a reference to the Lexical Environment
// of the corresponding f() call

もし f() が何度も呼び出され、結果の関数が保存されると、対応するすべてのレキシカル環境オブジェクトもメモリに保持されることに注意してください。以下のコードでは、それらはすべて 3 つあります。

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

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

// 3 functions in array, every one of them links to Lexical Environment
// from the corresponding f() run
let arr = [f(), f(), f()];

レキシカル環境オブジェクトは、(他のオブジェクトと同様に)到達不可能になったときに消滅します。言い換えれば、少なくとも 1 つのネストされた関数がそれを参照している間だけ存在します。

以下のコードでは、ネストされた関数が削除された後、それを取り囲むレキシカル環境(したがって value)がメモリからクリーンアップされます。

function f() {
  let value = 123;

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

let g = f(); // while g function exists, the value stays in memory

g = null; // ...and now the memory is cleaned up

実際の最適化

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

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

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

以下の例を Chrome で開発者ツールを開いた状態で実行してみてください。

一時停止したら、コンソールで 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();

ご覧のとおり、そのような変数はありません!理論上は、アクセスできるはずですが、エンジンがそれを最適化して削除しました。

これは、(時間を費やすものではないにしても)面白いデバッグの問題につながる可能性があります。その1つは、期待されるものではなく、同じ名前の外側の変数が見えてしまうことです。

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` は外部変数 `name` を使用します。関数が実行されるとき、どの値を使用するでしょうか?

let name = "John";

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

name = "Pete";

sayHi(); // what will it show: "John" or "Pete"?

このような状況は、ブラウザとサーバーサイドの開発の両方でよくあります。関数は、ユーザーアクションやネットワークリクエストの後など、作成されたときよりも後に実行されるようにスケジュールされている場合があります。

したがって、問題は、最新の変更を拾うかどうかです。

答えは:Pete

関数は外側の変数を現在の状態のまま取得し、最新の値を使用します。

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

重要度: 5

以下の関数 makeWorker は別の関数を作成して返します。その新しい関数は別の場所から呼び出すことができます。

それは、作成場所または呼び出し場所、あるいはその両方から外側の変数にアクセスできるでしょうか?

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

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

let name = "John";

// create a function
let work = makeWorker();

// call it
work(); // what will it show?

どの値を表示しますか?「Pete」または「John」?

答えは:Pete

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

したがって、ここでの結果は "Pete" です。

しかし、もし makeWorker()let name がなければ、検索は外側に行って、上記のチェーンからわかるようにグローバル変数を取得します。その場合、結果は "John" になります。

重要度: 5

ここでは、同じ makeCounter 関数を使用して、2 つのカウンター countercounter2 を作成します。

それらは独立していますか?2 番目のカウンターは何を表示しますか?0,1 または 2,3、または何か別のものですか?

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
重要度: 5

コードを見てください。最後の行の呼び出しの結果はどうなりますか?

let phrase = "Hello";

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

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

sayHi();

結果は エラー です。

関数 sayHiif の内部で宣言されているため、その内部でのみ有効です。外側には sayHi は存在しません。

重要度: 4

sum(a)(b) = a+b のように機能する関数 sum を記述してください。

はい、まさにこのように、二重括弧を使用して(タイプミスではありません)。

例えば

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

2 番目の括弧が機能するためには、最初の括弧が関数を返す必要があります。

こんな感じです

function sum(a) {

  return function(b) {
    return a + b; // takes "a" from the outer lexical environment
  };

}

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

このコードの結果はどうなりますか?

let x = 1;

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

  let x = 2;
}

func();

追伸。このタスクには落とし穴があります。解決策は明らかではありません。

結果は: エラー

実行してみてください

let x = 1;

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

func();

この例では、「存在しない」変数と「未初期化」変数の間の特有の違いを観察できます。

記事変数のスコープ、クロージャで読んだように、変数はコードブロック(または関数)が実行される瞬間から「未初期化」状態で開始されます。そして、対応する let ステートメントまで未初期化のままです。

言い換えれば、変数は技術的には存在しますが、let の前には使用できません。

上記のコードはそれを示しています。

function func() {
  // the local variable x is known to the engine from the beginning of the function,
  // but "uninitialized" (unusable) until let ("dead zone")
  // hence the error

  console.log(x); // ReferenceError: Cannot access 'x' 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] のメンバーのいずれかと一致する要素のみを選択します。

例えば

/* .. 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

テスト付きのサンドボックスを開きます。

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

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

サンドボックスでテスト付きの解決策を開きます。

重要度: 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 を記述してください。

テスト付きのサンドボックスを開きます。

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

サンドボックスでテスト付きの解決策を開きます。

重要度: 5

次のコードは、shooters の配列を作成します。

すべての関数は、その番号を出力することを目的としています。しかし、何か問題があります…

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function() { // create a shooter function,
      alert( i ); // that should show its number
    };
    shooters.push(shooter); // and add it to the array
    i++;
  }

  // ...and return the array of shooters
  return shooters;
}

let army = makeArmy();

// all shooters show 10 instead of their numbers 0, 1, 2, 3...
army[0](); // 10 from the shooter number 0
army[1](); // 10 from the shooter number 1
army[2](); // 10 ...and so on.

なぜ、すべてのシューターが同じ値を表示するのでしょうか?

意図したとおりに機能するようにコードを修正してください。

テスト付きのサンドボックスを開きます。

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](これは関数)を取得して、それを呼び出します。

    では、なぜそのような関数がすべて同じ値、10 を表示するのでしょうか?

    それは、shooter 関数の内部にローカル変数 i がないためです。そのような関数が呼び出されると、外側のレキシカル環境から i を取得します。

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

    ソースを見ると

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

    すべての shooter 関数は makeArmy() 関数のレキシカル環境で作成されていることがわかります。しかし、army[5]() が呼び出されると、makeArmy はすでにそのジョブを完了しており、i の最終値は 10 です(whilei=10 で停止します)。

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

    上でわかるように、while {...} ブロックの各反復で、新しいレキシカル環境が作成されます。したがって、これを修正するには、次のように while {...} ブロック内の変数に i の値をコピーできます

    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();
    
    // Now the code works correctly
    army[0](); // 0
    army[5](); // 5

    ここで、let j = i は「反復ローカル」変数 j を宣言し、i をコピーします。プリミティブは「値で」コピーされるため、実際には現在のループ反復に属する i の独立したコピーを取得します。

    シューターは正しく機能します。これは、i の値が少し近い場所に存在するようになったためです。makeArmy() のレキシカル環境ではなく、現在のループ反復に対応するレキシカル環境に存在します

    最初から for を使用した場合も、このような問題を回避できます。

    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 を持つ新しいレキシカル環境を生成するためです。したがって、すべての反復で生成された shooter は、その反復自身の i を参照します。

さて、これだけ読んでいただき、最終的な解決策がとてもシンプルであるため(for を使用するだけ)、それだけの価値があったのだろうかと思われるかもしれません。

まあ、その質問に簡単に答えられたのなら、解決策を読まなかったでしょう。したがって、このタスクが物事をもう少し理解するのに役立ったことを願っています。

それに、for よりも while を好む場合や、このような問題が実際に発生するその他のシナリオもあります。

サンドボックスでテスト付きの解決策を開きます。

チュートリアルマップ

コメント

コメントする前にこちらをお読みください…
  • 改善点についてご提案がある場合は、コメントではなく、GitHub issue を提出するか、プルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • コードを数語挿入する場合は <code> タグを、複数行の場合は <pre> タグで囲み、10行を超える場合はサンドボックス (plnkr, jsbin, codepen…) を使用してください。