レッスンに戻る

関数の軍隊

重要度: 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 を使用するだけです)、疑問に思うかもしれません。それは価値がありましたか?

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

その上、実際に whilefor より優先する場合や、そのような問題が現実的なシナリオがあります。

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