[JavaScript] Loop 迴圈的 Scoping 陷阱

JavaScript 經常有一些和其它程式語言不一樣的地方。例如它的 Scoping 原則, 千萬不要直覺地以自己的想像去揣摩; 應該實際驗證一下, 才能確定。

來看看以下的簡單小程式。先不要把它拿去跑; 你覺得它應該會印出什麼結果?

function test() {
  for (i=0; i<2; i++) 
    for (i=0; i<4; i++)
      console.log(i);
}
test();
console.log('i =', i);

大部份 JavaScript 教學網站對 For Loop 的寫法都是像上面程式這樣的。如果你是已經先學了其它語言, 可能會直覺得會以為程式裡的 i 應該在兩個迴圈中都是區域變數。所以它應該印出兩次 "0,1,2,3", 然後最下面的 i 應該是 undefined。

事實不然。程式跑完之後, 結果是這樣的:

0
1
2
3
i = 5

為什麼會這樣? 因為在 JavaScript 中, 如果你第一次使用了一個變數並賦予值, 那麼它會自動將它的 Scope 定義為 Global。所以這個 i 不但在迴圈之外可以存取, 它甚至在函式之外也可以存取; 這也就是為什麼最下面的 i 會等於 5, 而不是 undefined。

同理, 程式中兩個 For 迴圈裡的 i 事實上也是同一個。內層迴圈把 i 加到 4 之後, 又回到外層迴圈並且加到 5; 由於它的值已經大於 2, 所以內層迴圈不會再跑一次。假設程式裡外層迴圈數不是 2 而是更大 (例如 10), 那麼輸出結果會更有趣!

由於內外層迴圈都只是把 i 當作計數器, 程式本身並沒有使用它的值, 所以上述寫法其實是 OK 的。但是它事實上並不是很好的寫法; 一般我們會使用不同名稱的計數器變數, 例如 i, j, k 等等。本文中的寫法只是為了示範而已。

MDM 的範例寫法就比較正確了。它對於 For Loop 中使用的計數器變數都加上了 var 宣告。我們把原來的程式改寫如下:

function test() {
  for (var i=0; i<2; i++) 
    for (var i=0; i<4; i++)
      console.log(i);
}
test();
console.log('i =', i);

同樣地, 如果你不跑程式, 你覺得它的輸出應該是什麼?

這次它的結果比較正確了, 但是也許也不是你原來想像的:

0
1
2
3
* Uncaught ReferenceError: i is not defined

當我們用 var 宣告一個變數時, 如果它是在函式外宣告, 那麼它的定義域會是 Global, 如果它是在函式內宣告, 那麼它的定義域就會是在該函式以內。所以它的效果和完全不宣告就賦值的做法是不太一樣的。

既然它的定義域只限於函式內, 那麼我們這次在函式外企圖存取變數 i, 自然會得到 undefined 的錯誤。這是合乎預期的。

但是它仍然只輸出一次結果。這是因為在這個程式中, test() 裡面的 i 仍然是同一個 i (即使你宣告了它兩次)。你可以把 console.log('i =', i); 這一行搬到 test() 函式的最後一行, 它的輸出值仍然是 5。換句話說, 這個 var 真的只負責把變值侷限在 function 裡面而已。只要是在函數中, 你仍然到處可以存取到它的值。

如果你要達到最原始的目的, 就是使用 let 來宣告變數:

function test() {
  for (let i=0; i<2; i++) 
    for (let i=0; i<4; i++)
      console.log(i);
  console.log('i =', i);
}
test();
console.log('i =', i);

這個 let 用於宣告一個「只作用在當前區塊的變數」, 是最嚴格的區域變數宣告法。用這種方式來宣告 i 的話, 外層迴圈甚至不認得內層迴圈的 i, 倒過來也如此(除非你取不同的變數名)。所以它的輸出符合我們原先的目的:

0
1
2
3
0
1
2
3
* Uncaught ReferenceError: i is not defined

也就是說, 內層迴圈會跑兩次; 而且一旦出了迴圈之外, 不管是在函式內外, 都再也取不到 i 的值。

我知道 for (let i=0...) 這種寫法可能不太符合你長久以來的習慣, 但是若不想再踫到意料之外的錯誤, 今天起就改過來吧!


Dev 2Share @ 點部落