下面是一道很入門的js面試題:
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i)
}, 10 * i)
}
複製程式碼
幾乎每個前端在初學js的都會遇到這個問題, 有一段時間也是面試必問的題, 當然現在看到這段程式碼幾乎不用想, 輸出肯定是10*10
.
原因也是很簡單: 變數提升. js沒有塊級作用域, 所以在for迴圈中定義的i提升為全域性的了, 另外for迴圈是同步執行的, 所有當setTimeout
內部的匿名函式執行的時候i已經是10了.
那怎麼解決呢? 也沒啥疑問, 閉包或者用let:
// 閉包
for (var i = 0; i < 10; i++) {
void function (j) {
setTimeout(function () {
console.log(j)
}, 10 * j)
}(i)
}
// let
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i)
}, 10 * i)
}
複製程式碼
為什麼上面的方法能解決呢? 閉包那個不用多說, 因為js有函式作用域. i作為引數傳入, 直接繫結到匿名函式上, 作用域鏈到此截至, i再提升也跟他沒關係了. 至於let, 其實是js內部實現的問題了, 簡單講就是let會生成不同的i例項, 10個匿名函式其實分別得到的是10個不同的i例項, 最終獲取的當然是理想值了.
當然這篇文章不會在這裡簡單的結束. 我們再深入一點, 來看看第一段程式碼的執行過程把:
我們可以看到, for 每執行一次, 就會呼叫setTimeout延遲若干秒向事件佇列推入一個匿名函式, 但是因為for是同步的, 所以推入的匿名函式不是立馬執行的, 而是要等for迴圈結束, 當然for執行時間很快, 但是影響卻不小, 由於作用域問題, i被提升了, 當for結束了全域性i就是10, 這時候call stack也空了, 匿名函式開始依次推入到call stack執行, 由於引用的都是變數i, 而i已經是10了, 輸出10*10
沒毛病. 如果用let呢? 根據mdn, 每次for都會建立一個新的i binding, 也就是說匿名函式引用的是不同的i例項. 結果不言而喻.
所以, 一個簡簡單單的面試題還是有很多可挖掘的點, 比如上面我們就涉及了作用域, 非同步同步, 事件迴圈等等, 每個點都可以深入說很多: let const的暫存死區, 非同步的執行順序, 事件迴圈的基本實現等等. 希望有機會可以深入討論.