JavaScript は非常に関数指向の言語です。それは私たちに多くの自由を与えてくれます。関数はいつでも作成でき、引数として別の関数に渡し、後でコードのまったく別の場所から呼び出すことができます。
関数がその外部の変数 (「外部」変数) にアクセスできることはすでにわかっています。
しかし、関数の作成後に外部変数が変更された場合はどうなるでしょうか?関数は新しい値を取得しますか、それとも古い値を取得しますか?
そして、関数が引数として渡され、コードの別の場所から呼び出された場合、その関数は新しい場所で外部変数にアクセスできるのでしょうか?
これらのシナリオとより複雑なシナリオを理解するために知識を広げてみましょう。
ここではlet/const
変数について説明します。
JavaScript では、変数を宣言する方法が 3 つあります。 let
、 const
(最新のもの)、およびvar
(過去の名残) です。
この記事では、例でlet
変数を使用します。
const
で宣言された変数は同じように動作するため、この記事もconst
について説明します。
古いvar
にはいくつかの注目すべき違いがあり、それらについては「古い "var"」の記事で説明します。
変数がコード ブロック{...}
内で宣言されている場合、その変数はそのブロック内でのみ表示されます。
例えば:
{ // 外部に見られるべきではないローカル変数を使用して何らかのジョブを実行します let message = "こんにちは"; // このブロック内でのみ表示されます アラート(メッセージ); // こんにちは } アラート(メッセージ); // エラー: メッセージが定義されていません
これを使用して、独自のタスクを実行するコード部分を、それにのみ属する変数とともに分離できます。
{ // メッセージを表示 let message = "こんにちは"; アラート(メッセージ); } { // 別のメッセージを表示する let message = "さようなら"; アラート(メッセージ); }
ブロックがないとエラーになる
独立したブロックがないと、既存の変数名でlet
使用するとエラーが発生することに注意してください。
// メッセージを表示 let message = "こんにちは"; アラート(メッセージ); // 別のメッセージを表示する let message = "さようなら"; // エラー: 変数はすでに宣言されています アラート(メッセージ);
if
、 for
、 while
などの場合、 {...}
で宣言された変数も内部でのみ表示されます。
if (true) { let フレーズ = "こんにちは!"; アラート(フレーズ); // こんにちは! } アラート(フレーズ); // エラー、そのような変数はありません!
ここで、 if
終了すると、以下のalert
にはphrase
が表示されないため、エラーが発生します。
これは、 if
ブランチに固有のブロックローカル変数を作成できるため、素晴らしいことです。
同様のことが for for
とwhile
ループにも当てはまります。
for (i = 0; i < 3; i++) { // 変数 i はこの内部でのみ表示されます。 アラート(i); // 0、次に 1、次に 2 } アラート(i); // エラー、そのような変数はありません
視覚的には、 let i
{...}
の外側にあるとします。ただし、ここでのfor
構文は特別です。内部で宣言された変数はブロックの一部とみなされます。
関数が別の関数内に作成される場合、その関数は「ネストされた」と呼ばれます。
これは JavaScript を使用して簡単に実行できます。
次のように、これを使用してコードを整理できます。
function SayHiBye(名, 姓) { // 以下で使用するヘルパーのネストされた関数 関数 getFullName() { 名 + " " + 姓を返します。 } alert( "こんにちは、" + getFullName() ); alert( "さようなら、" + getFullName() ); }
ここでは、ネストされた関数getFullName()
が便宜上作成されています。外部変数にアクセスできるため、完全な名前を返すことができます。入れ子関数は JavaScript では非常に一般的です。
さらに興味深いのは、入れ子になった関数を、新しいオブジェクトのプロパティとして、またはそれ自体の結果として返すことができることです。その後、別の場所で使用できます。どこにいても、同じ外部変数にアクセスできます。
以下では、 makeCounter
呼び出しごとに次の数値を返す「カウンター」関数を作成します。
関数 makeCounter() { カウント = 0 とします。 戻り関数() { count++ を返します。 }; } let counter = makeCounter(); アラート(カウンター()); // 0 アラート(カウンター()); // 1 アラート(カウンター()); // 2
シンプルではありますが、コードをわずかに変更したバリアントは、自動テスト用の乱数値を生成する乱数ジェネレーターなど、実用的な用途があります。
これはどのように作動しますか?複数のカウンターを作成した場合、それらは独立しますか?ここの変数では何が起こっているのでしょうか?
このようなことを理解することは、JavaScript の全体的な知識にとって非常に役立ち、より複雑なシナリオにも役立ちます。それでは、もう少し詳しく見てみましょう。
ここにドラゴンがいます!
詳細な技術的な説明はこの先にあります。
低レベル言語の詳細については避けたいと思っていますが、それなしでは理解が不十分で不完全になるため、準備をしてください。
わかりやすくするために、説明は複数のステップに分かれています。
JavaScript では、実行中のすべての関数、コード ブロック{...}
、およびスクリプト全体に、語彙環境と呼ばれる内部 (非表示) 関連オブジェクトがあります。
LexicalEnvironment オブジェクトは 2 つの部分で構成されます。
環境レコード– すべてのローカル変数をそのプロパティ (およびthis
の値などのその他の情報) として保存するオブジェクト。
外部の字句環境への参照、つまり外部のコードに関連付けられた環境への参照。
「変数」は、特別な内部オブジェクトであるEnvironment Record
のプロパティにすぎません。 「変数を取得または変更する」とは、「そのオブジェクトのプロパティを取得または変更する」ことを意味します。
関数のないこの単純なコードには、字句環境が 1 つだけあります。
これは、スクリプト全体に関連付けられた、いわゆるグローバル語彙環境です。
上の図では、長方形は環境レコード (変数ストア) を意味し、矢印は外部参照を意味します。グローバル字句環境には外部参照がないため、矢印はnull
を指しています。
コードの実行が開始され、実行が進むにつれて、語彙環境が変化します。
少し長いコードを次に示します。
右側の四角形は、実行中にグローバル語彙環境がどのように変化するかを示しています。
スクリプトが開始されると、宣言されたすべての変数が字句環境に事前に設定されます。
最初は「初期化されていない」状態です。これは特別な内部状態であり、エンジンは変数について認識していますが、 let
で宣言されるまで参照できないことを意味します。変数が存在しない場合とほぼ同じです。
次に、 let phrase
定義が表示されます。まだ割り当てがないため、その値はundefined
です。この時点から変数を使用できます。
phrase
は値が割り当てられます。
phrase
値が変わります。
今のところ、すべてがシンプルに見えますよね?
変数は、現在実行中のブロック/関数/スクリプトに関連付けられた特別な内部オブジェクトのプロパティです。
変数を操作するということは、実際にはそのオブジェクトのプロパティを操作することになります。
字句環境は仕様オブジェクトです
「語彙環境」は仕様オブジェクトです。これは、物事がどのように機能するかを説明するために言語仕様に「理論的に」のみ存在します。このオブジェクトをコード内で取得して直接操作することはできません。
JavaScript エンジンは、目に見える動作が説明どおりである限り、それを最適化し、メモリを節約するために使用されていない変数を破棄し、他の内部トリックを実行することもあります。
関数も変数と同様に値です。
違いは、関数宣言が即座に完全に初期化されることです。
字句環境が作成されると、関数宣言はすぐに使用できる関数になります (宣言されるまで使用できないlet
とは異なります)。
そのため、関数宣言として宣言された関数を、宣言自体の前でも使用できるのです。
たとえば、関数を追加したときのグローバル語彙環境の初期状態は次のとおりです。
当然のことながら、この動作は関数宣言にのみ適用され、関数を変数に代入する関数式 ( let say = function(name)...
など) には適用されません。
関数が実行されるとき、呼び出しの開始時に、呼び出しのローカル変数とパラメーターを保存するために新しい語彙環境が自動的に作成されます。
たとえば、 say("John")
の場合、次のようになります (実行は矢印の付いた行で行われます)。
関数呼び出し中に、内側のもの (関数呼び出し用) と外側のもの (グローバル) の 2 つの語彙環境があります。
内部の語彙環境は、 say
の現在の実行に対応します。これには、関数の引数であるname
という単一のプロパティがあります。 say("John")
を呼び出したので、 name
の値は"John"
になります。
外側の語彙環境はグローバル語彙環境です。 phrase
変数と関数自体があります。
内部の語彙環境はouter
語彙環境への参照を持っています。
コードが変数にアクセスする必要がある場合、最初に内部の語彙環境が検索され、次に外部の語彙環境が検索され、次にさらに外側の語彙環境が検索され、グローバル環境が検索されるまで続きます。
変数がどこにも見つからない場合、それは strict モードでのエラーです ( use strict
ない場合、古いコードとの互換性のために、存在しない変数への代入により新しいグローバル変数が作成されます)。
この例では、検索は次のように進行します。
name
変数の場合、 say
内のalert
内部の語彙環境でその変数をすぐに見つけます。
phrase
にアクセスしたい場合、ローカルにはphrase
が存在しないため、外部の語彙環境への参照をたどり、そこでフレーズを見つけます。
makeCounter
例に戻りましょう。
関数 makeCounter() { カウント = 0 とします。 戻り関数() { count++ を返します。 }; } let counter = makeCounter();
各makeCounter()
呼び出しの開始時に、このmakeCounter
実行の変数を保存するために、新しい Lexical Environment オブジェクトが作成されます。
したがって、上の例と同様に、2 つのネストされた語彙環境があります。
異なる点は、 makeCounter()
の実行中に、 return count++
という 1 行だけの小さな入れ子関数が作成されることです。まだ実行していません。作成するだけです。
すべての関数は、その関数が作成された語彙環境を記憶しています。技術的には、ここに魔法はありません。すべての関数には、関数が作成された語彙環境への参照を保持する[[Environment]]
という名前の隠しプロパティがあります。
したがって、 counter.[[Environment]]
には{count: 0}
字句環境への参照があります。このようにして、関数はどこで呼び出されたとしても、作成された場所を記憶します。 [[Environment]]
参照は、関数の作成時に一度だけ設定され、永久に設定されます。
その後、 counter()
が呼び出されると、その呼び出しに対して新しい字句環境が作成され、その外部の字句環境参照がcounter.[[Environment]]
から取得されます。
counter()
内のコードがcount
変数を探すとき、最初にそれ自身の字句環境 (ローカル変数がないため空) を検索し、次に外側のmakeCounter()
呼び出しの字句環境を検索し、そこで検索して変更します。 。
変数は、それが存在する語彙環境で更新されます。
実行後の状態は次のとおりです。
counter()
複数回呼び出すと、同じ場所でcount
変数が2
、 3
などに増加します。
閉鎖
開発者が一般的に知っておくべき、一般的なプログラミング用語「クロージャ」があります。
クロージャは、その外部変数を記憶し、それらにアクセスできる関数です。一部の言語では、それが不可能であるか、それを実現するには関数を特別な方法で記述する必要があります。しかし、上で説明したように、JavaScript ではすべての関数は当然クロージャです (例外が 1 つだけあり、「新しい関数」構文で説明します)。
つまり、非表示の[[Environment]]
プロパティを使用して作成場所を自動的に記憶し、コードが外部変数にアクセスできるようになります。
フロントエンド開発者が面接で「クロージャとは何ですか?」という質問を受けたとき、有効な答えは、クロージャの定義と、JavaScript のすべての関数がクロージャであるという説明、そしておそらく技術的な詳細についてのいくつかの言葉です。 [[Environment]]
プロパティと語彙環境の仕組み。
通常、字句環境は、関数呼び出しが終了した後、すべての変数とともにメモリから削除されます。参考文献がないからです。他の JavaScript オブジェクトと同様、アクセス可能な間のみメモリ内に保持されます。
ただし、関数の終了後も到達可能な入れ子関数がある場合、その関数には字句環境を参照する[[Environment]]
プロパティがあります。
この場合、字句環境は関数の完了後もアクセス可能なため、生きたままになります。
例えば:
関数 f() { 値 = 123 とします。 戻り関数() { アラート(値); } } g = f(); とします。 // g.[[Environment]] は字句環境への参照を保存します // 対応する f() 呼び出しの
f()
が何度も呼び出され、結果の関数が保存される場合、対応するすべての Lexical Environment オブジェクトもメモリ内に保持されることに注意してください。以下のコードでは、3 つすべてが次のようになります。
関数 f() { let value = Math.random(); return function() { アラート(値); }; } // 配列内の 3 つの関数、それぞれが語彙環境にリンク // 対応する f() の実行から arr = [f(), f(), f()];
Lexical Environment オブジェクトは、(他のオブジェクトと同様に) 到達不能になると消滅します。言い換えれば、それを参照する入れ子関数が少なくとも 1 つある場合にのみ存在します。
以下のコードでは、ネストされた関数が削除された後、それを囲んでいる語彙環境 (したがってvalue
) がメモリから消去されます。
関数 f() { 値 = 123 とします。 戻り関数() { アラート(値); } } g = f(); とします。 // g 関数が存在する間、値はメモリに残ります g = null; // ...これでメモリがクリーンアップされました
これまで見てきたように、理論上、関数が生きている間は、すべての外部変数も保持されます。
しかし実際には、JavaScript エンジンはそれを最適化しようとします。彼らは変数の使用状況を分析し、外部変数が使用されていないことがコードから明らかな場合、その変数は削除されます。
V8 (Chrome、Edge、Opera) における重要な副作用は、そのような変数がデバッグで使用できなくなることです。
開発者ツールを開いた状態で、Chrome で以下の例を実行してみてください。
一時停止したら、コンソールにalert(value)
入力します。
関数 f() { let value = Math.random(); 関数 g() { デバッガ; // コンソールで: 「alert(value)」と入力します。そのような変数はありません! } gを返します。 } g = f(); とします。 g();
ご覧のとおり、そのような変数は存在しません。理論的にはアクセスできるはずですが、エンジンが最適化しました。
これにより、(それほど時間がかからないとしても) デバッグに関するおかしな問題が発生する可能性があります。それらの 1 つ – 予想される変数ではなく、同じ名前の外部変数が表示されます。
let value = "サプライズ!"; 関数 f() { let value = "最も近い値"; 関数 g() { デバッガ; // コンソールで: 「alert(value)」と入力します。驚き! } gを返します。 } g = f(); とします。 g();
V8 のこの機能は知っておくと良いでしょう。 Chrome/Edge/Opera でデバッグしている場合は、遅かれ早かれこの問題に遭遇するでしょう。
これはデバッガのバグではなく、V8 の特別な機能です。おそらくいつか変更されるでしょう。このページの例を実行することで、いつでも確認できます。
重要度: 5
関数sayHiは外部変数名を使用します。関数が実行されるとき、どの値が使用されますか?
name = "ジョン" とします。 関数sayHi() { alert("こんにちは、" + 名前); } 名前 = "ピート"; こんにちは(); // 何が表示されますか:「ジョン」または「ピート」?
このような状況は、ブラウザー側開発でもサーバー側開発でもよく見られます。関数は、たとえばユーザーアクションやネットワークリクエストの後など、関数の作成よりも後で実行するようにスケジュールされる場合があります。
そこで問題は、最新の変更が反映されるかどうかです。
答えは「ピート」です。
関数は外部変数を現状のまま取得し、最新の値を使用します。
古い変数値はどこにも保存されません。関数が変数を必要とする場合、関数はそれ自体の語彙環境または外部の語彙環境から現在の値を取得します。
重要度: 5
以下の関数makeWorker
別の関数を作成し、それを返します。その新しい関数は他の場所から呼び出すことができます。
作成場所、呼び出し場所、またはその両方から外部変数にアクセスできますか?
関数 makeWorker() { 名前を「ピート」にします; 戻り関数() { アラート(名前); }; } name = "ジョン" とします。 // 関数を作成する let work = makeWorker(); // それを呼び出します 仕事(); // 何が表示されるのでしょうか?
どの値が表示されるでしょうか? 「ピート」それとも「ジョン」?
答えは「ピート」です。
以下のコードのwork()
関数は、外部の字句環境参照を通じて、その起源の場所からname
を取得します。
ということで、結果はこちらの"Pete"
です。
しかし、 makeWorker()
にlet name
がなかった場合、上記のチェーンからわかるように、検索は外部に進み、グローバル変数を取得します。この場合、結果は"John"
になります。
重要度: 5
ここでは、同じmakeCounter
関数を使用して、 counter
とcounter2
の 2 つのカウンターを作成します。
彼らは独立していますか? 2 番目のカウンターは何を示すのでしょうか? 0,1
2,3
あるいはその他ですか?
関数 makeCounter() { カウント = 0 とします。 戻り関数() { count++ を返します。 }; } let counter = makeCounter(); let counter2 = makeCounter(); アラート(カウンター()); // 0 アラート(カウンター()); // 1 アラート( counter2() ); //? アラート( counter2() ); //?
答え: 0,1。
関数counter
とcounter2
makeCounter
の異なる呼び出しによって作成されます。
したがって、それらは独立した外部語彙環境を持ち、それぞれが独自のcount
持ちます。
重要度: 5
ここでは、コンストラクター関数を使用してカウンター オブジェクトが作成されます。
うまくいきますか?それは何を示しますか?
関数 Counter() { カウント = 0 とします。 this.up = function() { ++カウントを返します。 }; this.down = function() { --count を返します。 }; } let counter = new Counter(); アラート( counter.up() ); //? アラート( counter.up() ); //? アラート( counter.down() ); //?
きっとうまくいきますよ。
入れ子になった関数はどちらも同じ外側の字句環境内に作成されるため、同じcount
変数へのアクセスを共有します。
関数 Counter() { カウント = 0 とします。 this.up = function() { ++カウントを返します。 }; this.down = function() { --count を返します。 }; } let counter = new Counter(); アラート( counter.up() ); // 1 アラート( counter.up() ); // 2 アラート( counter.down() ); // 1
重要度: 5
コードを見てください。最終ラインでの通話の結果はどうなるでしょうか?
let フレーズ = "こんにちは"; if (true) { ユーザー = "ジョン" にします。 関数sayHi() { alert(`${phrase}, ${user}`); } } こんにちは();
結果はエラーになります。
関数sayHi
はif
内で宣言されているため、その中にのみ存在します。外ではsayHi
ありません。
重要度: 4
次のように機能する関数sum
を作成します: sum(a)(b) = a+b
。
はい、まさにこの方法で、二重括弧を使用します (タイプミスではありません)。
例えば:
合計(1)(2) = 3 合計(5)(-1) = 4
2 番目のかっこが機能するには、最初のかっこが関数を返す必要があります。
このような:
関数 sum(a) { 戻り関数(b) { a + b を返します。 // 外部の字句環境から「a」を取得します }; } アラート( sum(1)(2) ); // 3 アラート( sum(5)(-1) ); // 4
重要度: 4
このコードの結果はどうなるでしょうか?
x = 1 とします。 関数 func() { コンソール.ログ(x); //? x = 2 とします。 } 関数();
PS このタスクには落とし穴があります。解決策は明らかではありません。
結果は次のようになります。エラー。
実行してみてください:
x = 1 とします。 関数 func() { コンソール.log(x); // ReferenceError: 初期化前に 'x' にアクセスできません x = 2 とします。 } 関数();
この例では、「存在しない」変数と「初期化されていない」変数の間に独特の違いがあることがわかります。
「変数のスコープ、クロージャ」の記事で読んだことがあるかもしれませんが、変数は、実行がコード ブロック (または関数) に入った瞬間から「初期化されていない」状態で開始されます。そして、対応するlet
ステートメントまでは初期化されないままになります。
言い換えれば、変数は技術的には存在しますが、 let
前では使用できません。
上記のコードはそれを示しています。
関数 func() { // ローカル変数 x は関数の最初からエンジンに認識されています。 // ただし、let (「デッドゾーン」) までは「初期化されていない」(使用不可) // したがってエラーが発生します コンソール.ログ(x); // ReferenceError: 初期化前に 'x' にアクセスできません x = 2 とします。 }
変数が一時的に使用できなくなるこのゾーン (コード ブロックの先頭からlet
まで) は、「デッド ゾーン」と呼ばれることもあります。
重要度: 5
配列用の組み込みメソッドarr.filter(f)
があります。関数f
を通じてすべての要素をフィルターします。 true
を返す場合、その要素は結果の配列で返されます。
「すぐに使用できる」フィルターのセットを作成します。
inBetween(a, b)
– a
とb
間、またはそれらと等しい (両端を含む)。
inArray([...])
– 指定された配列内。
使用法は次のようにする必要があります。
arr.filter(inBetween(3,6))
– 3 から 6 までの値のみを選択します。
arr.filter(inArray([1,2,3]))
– [1,2,3]
のメンバーの 1 つと一致する要素のみを選択します。
例えば:
/* .. inBetween と inArray のコード */ arr = [1, 2, 3, 4, 5, 6, 7]; とします。 アラート( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
テストを含むサンドボックスを開きます。
関数 inBetween(a, b) { 戻り関数(x) { 戻り x >= a && x <= b; }; } arr = [1, 2, 3, 4, 5, 6, 7]; とします。 アラート( arr.filter(inBetween(3, 6)) ); // 3,4,5,6
関数 inArray(arr) { 戻り関数(x) { 戻り arr.includes(x); }; } arr = [1, 2, 3, 4, 5, 6, 7]; とします。 alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
サンドボックス内のテストを含むソリューションを開きます。
重要度: 5
並べ替えるオブジェクトの配列があります。
ユーザー = [ { 名前: "ジョン"、年齢: 20、姓: "ジョンソン" }、 { 名前: "ピート"、年齢: 18、姓: "ピーターソン" }、 { 名前: "アン"、年齢: 19、姓: "ハサウェイ" } ];
これを行う通常の方法は次のようになります。
// 名前で (アン、ジョン、ピート) users.sort((a, b) => a.name > b.name ? 1 : -1); // 年齢別 (ピート、アン、ジョン) users.sort((a, b) => a.age > b.age ? 1 : -1);
このように、さらに冗長にできませんか?
users.sort(byField('name')); users.sort(byField('年齢'));
したがって、関数を書く代わりに、 byField(fieldName)
を置くだけです。
それに使用できる関数byField
記述します。
テストを含むサンドボックスを開きます。
関数 byField(フィールド名){ return (a, b) => a[フィールド名] > b[フィールド名] ? 1 : -1; }
サンドボックス内のテストを含むソリューションを開きます。
重要度: 5
次のコードは、 shooters
の配列を作成します。
すべての関数はその数値を出力することを目的としています。しかし、何かが間違っています…
関数 makeArmy() { 射手 = []; にしましょう。 i = 0 とします。 while (i < 10) { letshooter = function() { // シューター関数を作成します。 アラート( i ); // その番号が表示されるはずです }; シューター.push(シューター); // そしてそれを配列に追加します i++; } // ...そして射手の配列を返します シューターを返す。 } let army = makeArmy(); // すべての射手は、数字 0、1、2、3... の代わりに 10 を表示します。 軍隊[0](); // 射手番号0から10 軍隊[1](); // シューター番号 1 から 10 軍隊[2](); // 10 ...など。
すべてのシューターが同じ値を示すのはなぜですか?
意図したとおりに動作するようにコードを修正します。
テストを含むサンドボックスを開きます。
makeArmy
内で正確に何が起こっているのかを調べてみましょう。そうすれば解決策は明らかになるでしょう。
空の配列shooters
を作成します。
射手 = []; にしましょう。
ループ内のshooters.push(function)
を介して関数を埋めます。
すべての要素は関数であるため、結果の配列は次のようになります。
射手 = [ 関数 () { アラート (i); }、 関数 () { アラート (i); }、 関数 () { アラート (i); }、 関数 () { アラート (i); }、 関数 () { アラート (i); }、 関数 () { アラート (i); }、 関数 () { アラート (i); }、 関数 () { アラート (i); }、 関数 () { アラート (i); }、 関数 () { アラート (i); } ];
関数から配列が返されます。
その後、任意のメンバーへの呼び出し、たとえばarmy[5]()
配列 (関数) から要素army[5]
を取得し、それを呼び出します。
では、なぜこのような関数はすべて同じ値10
を示すのでしょうか?
これは、 shooter
関数内にローカル変数i
存在しないためです。このような関数が呼び出されると、その外部の語彙環境からi
が取得されます。
では、 i
の値はどうなるでしょうか?
ソースを見てみると:
関数 makeArmy() { ... i = 0 とします。 while (i < 10) { letshooter = function() { // シューター関数 アラート( i ); // その番号を表示する必要があります }; シューター.push(シューター); // 配列に関数を追加します i++; } ... }
すべてのshooter
関数がmakeArmy()
関数の字句環境で作成されていることがわかります。しかし、 army[5]()
が呼び出されたとき、 makeArmy
すでにそのジョブを終了しており、 i
の最終値は10
です ( i=10
で停止しwhile
)。
結果として、すべてのshooter
関数は外部の語彙環境から同じ値、つまり最後の値i=10
を取得します。
上でわかるように、 while {...}
ブロックの反復ごとに、新しい字句環境が作成されます。したがって、これを修正するには、次のようにi
の値をwhile {...}
ブロック内の変数にコピーします。
関数 makeArmy() { 射手 = []; にしましょう。 i = 0 とします。 while (i < 10) { j = i とします。 letshooter = function() { // シューター関数 アラート( j ); // その番号を表示する必要があります }; シューター.push(シューター); i++; } シューターを返す。 } let army = makeArmy(); // コードが正しく動作するようになりました 軍隊[0](); // 0 軍隊[5](); // 5
ここでlet j = i
「反復ローカル」変数j
宣言し、そこにi
コピーします。プリミティブは「値によって」コピーされるため、実際には、現在のループ反復に属するi
の独立したコピーが取得されます。
i
の値が少し近くなったので、シューターは正しく動作します。 makeArmy()
字句環境内ではなく、現在のループ反復に対応する字句環境内にあります。
このような問題は、最初に次のようにfor
使用すると回避できます。
関数 makeArmy() { 射手 = []; にしましょう。 for(let i = 0; i < 10; i++) { letshooter = function() { // シューター関数 アラート( i ); // その番号を表示する必要があります }; シューター.push(シューター); } シューターを返す。 } let army = makeArmy(); 軍隊[0](); // 0 軍隊[5](); // 5
for
反復ごとに独自の変数i
を使用して新しい字句環境を生成するため、これは本質的に同じです。したがって、すべての反復で生成されたshooter
、まさにその反復から独自のi
参照します。
さて、あなたはこれを読むのに多大な労力を費やし、最終的なレシピは非常に簡単です - for
使用するだけです、あなたは疑問に思うかもしれません - それだけの価値がありましたか?
そうですね、質問に簡単に答えられるなら、解決策を読まないでしょう。したがって、このタスクが物事をもう少しよく理解するのに役立つことを願っています。
さらに、 for
よりwhile
を好む場合や、そのような問題が現実となる他のシナリオも確かにあります。
サンドボックス内のテストを含むソリューションを開きます。