返回課程

函數軍隊

重要性: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 的最後值為 10while 停止於 i=10)。

    因此,所有 shooter 函式從外部詞法環境取得相同的值,也就是最後一個值 i=10

    如上所示,在 while {...} 區塊的每個反覆運算中,都會建立一個新的詞法環境。因此,為了修正這個問題,我們可以將 i 的值複製到 while {...} 區塊中的變數,如下所示

    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,以及其他情境,其中此類問題是真實存在的。

在沙盒中開啟有測試的解答。