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 造成的問題, 所有閉包都指到同一個變數, 所以讓各次迴圈共用的變數不再共用,而是指向當下的值。 但因此表現的行為會和對語法的想像有出入, 同時又因為增加的複雜度,製造了更多的與想像不合的地方。