関数の軍隊
以下のコードは、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
の内部で何が起こっているのかを正確に調べてみましょう。そうすれば、解決策は明らかになります。
-
空の配列
shooters
を作成しますlet shooters = [];
-
ループ内で
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); } ];
-
配列は関数から返されます。
その後、後で、任意のメンバー(例:
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
です(while
はi=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
を使用するだけです)、疑問に思うかもしれません。それは価値がありましたか?
まあ、あなたが簡単に質問に答えることができれば、あなたは解決策を読まないでしょう。ですから、うまくいけば、このタスクはあなたが物事をもう少し理解するのに役立ったはずです。
その上、実際に while
を for
より優先する場合や、そのような問題が現実的なシナリオがあります。