javascript 中 for 迴圈變數的重覆宣告

javascript 的 for (let i=0; i<9; i++) 形式迴圈, 從前只有 var 時代時有共用變數的問題, 但改用 let 後問題解決了, 但仔細看會發現就語意來說問題不應該解決才對, 因為 let 和 i++ 代表每次迴圈中的 i 只是上一個 i 的值改變了,但還是同一個變數。 所以 var 時代的共用變數問題應該仍要存在,但實際使用上卻消失了, 應該是在 es2016 的規格書中寫了不同的執行情況。

var 時代的問題

在從前只有 var 時,因為 var 的作用域是超出 for 迴圈 block 的, 所以在迴圈結束後會殘留一個結束狀態的 i 在作用域。 而且因為 i 在迴圈每次執行時是共用的, 所以如果在迴圈中用了閉包把 i 包入函數, 則所有函數中的 i 還是同一個 i,值也相同, 在迴圈結束後所有 i 也都會停在那時的值。

var a = []
for (var i=0; i<9; i++) {
  a.push(function () { return i })
}
// i = 9

a.map(function (f) { return f() })
// [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
// 每個函數不同,但每個函數中的 i 都指向那個 var 宣告的 i,
// 停在迴圈結束時的 9。

let 各次迴圈的獨立

其實我很少用 let,多半用 const, 因為我不太需要去改變某個已經宣告的變數, 多是宣告一個新的變數。 只有字串相加的時候會用 s += anotherString , 所以得用 let;另外就是 for 迴圈計數的時候。

雖然你在迴圈每次執行時,可能不會更改 i 的值, 但你在 for 迴圈的第三個表達式 i++ , 很明顯就是改了 i 的值,所以不能用 const, 如果用 const 在執行到 i++ 會丟 TypeError: Assignment to constant variable. 。 這就表示,你在迴圈每次執行的 i, 都是上一次的 i++ 的結果,都是同一個 i。

可是如果是同一個 i,那和 var 相比 雖然在迴圈結束後 i 就消失了; 但用閉包包入的 i 還是會都指向同一個 i, 也應該指到同一個值,但並沒有。

let a = []
for (let i=0; i<9; i++) {
    a.push(() => i)
}
a.map(f => f()) // [0, 1, 2, 3, 4, 5, 6, 7, 8]

for let 只在當次迴圈有效

所以 for (let i=0;;) 形式的迴圈中, 第一個表達式如果是 let 或 const 宣告, 那該變數在第二和第三表達式裡是同一個, 同時如果在迴圈執行中改變了 i 的值, 是會將改變帶到下一輪的。 可是如果是在迴圈進到下一圈後, 上一圈用閉包捕捉的 i 就和新的 i 無關了。

也許可以想像成,在進入迴圈內前, 會將 i 複製一份到迴圈內的作用域, 然後迴圈執行完一次後再複製出來, 因此這次的 i 和下次的 i 是獨立的, 但在迴圈內用閉包抓的 i, 在離開單次迴圈前仍有效。

let f = () => null
for (let i=0; i<9; i++) {
    console.log('init', i)
    f() // 這時 f 是上一次的 f 和上一次的 i,不會對當下的 i 造成影響。
    console.log('after previous increment', i)
    f = () => i++ // 現在的 i
    f() // 這次就有影響了。
    console.log('after current increment', i)
}

更加混亂的共用變數

但我異想天開,又想如果把 f 和 i 一起宣告在迴圈頭呢? 結果發現該 f 所捕捉到的 i 竟然完全和迴圈內的 i 無關, 頂多只有一開始值相同,之後任一方改變都不會影響到另一方。

for (let i=0, set=v=>i=v, get=()=>i; i<9; i++) {
    console.log('before', i,get())
    set(get()-1)
    console.log('after', i,get())
}

結論

總之,想完全弄懂可以去看 ecma script spec , (網頁有點大,注意流量。) 但太冗長了,我沒有看完也沒有全看懂, 反正看起來和我想像的沒有衝突。

這整件事,就是為了解決一開始 var 時代不只因 var 造成的問題, 所有閉包都指到同一個變數, 所以讓各次迴圈共用的變數不再共用,而是指向當下的值。 但因此表現的行為會和對語法的想像有出入, 同時又因為增加的複雜度,製造了更多的與想像不合的地方。