由一道關於變數作用域的面試題,來加深對var和let的理解
最近,有一道JavaScript面試題挺流行的,很多朋友去面試的時候都遇到了。這道題目大致是這個樣子的:
以下這段程式碼執行後,結果為什麼不是依次輸出0到9?如果要讓它實現這樣的輸出,你會怎麼來修改這段程式碼?
for (var i = 0 ; i < 10; i++) {
setTimeout(function () {
console.log(i)
})
}
那讓我們先來看一看,在這段程式碼中列印變數i
的最終輸出結果到底會是什麼呢?待一陣十指亂動,風一般的敲出執行程式碼的命令,只見螢幕一閃,亮出十行大字:
10
10
10
10
10
10
10
10
10
10
What?! 它輸出的居然是10個10
而不是更貼近我們第一感覺的0到9
,這是怎麼回事兒?又是一個什麼坑……還能不能好好的寫JavaScript了……
原因分析
其實,這個鍋也不能全由JavaScript來背,有可能是你沒有完全理解JavaScript導致的。產生這個執行結果的關鍵點就在於for
語句中的var i = 0;
這句變數宣告程式碼。
我們都知道,var
是用來宣告變數的,並且我們通常也知道,一個語句從哪裡開始宣告就會在哪裡開始被處理。但是var
是JavaScript語法中的一個例外!我們來看一下Mozilla官方文件中對var
的定義:
var變數宣告,無論發生在何處,都在執行任何程式碼之前進行處理。
用var宣告的變數的作用域是它當前的執行上下文,它可以是巢狀的函式,也可以是宣告在任何函式外的變數。如果你重新宣告一個 JavaScript 變數,它將不會丟失其值。
由於上述定義的原因,var
變數宣告(以及其他宣告,比如函式宣告)總是在任意程式碼執行之前處理的,所以在程式碼中的任意位置宣告變數總是等效於在程式碼開頭宣告。這意味著變數可以在宣告之前使用,這個行為叫做變數提升(Hoisting)
。變數提升就像是把所有的變數宣告移動到函式或者全域性程式碼的開頭位置:
bla = 2
var bla
// 可以理解為:
var bla
bla = 2
因此對於我們這道題,變數i
的宣告就相當於提前到了for
語句的外面,相應的,變數i
的作用域範圍也同時擴大到了for
語句的外面,與以下的寫法相互等效:
var i = 0;
for (; i < 10; i++) {
setTimeout(function () {
console.log(i)
})
}
另外一點,我們得明白setTimeout()
的執行時機:它總是在當前的同步程式碼執行完成後開始執行。可以在前面的程式碼中加入一些log進行跟蹤並驗證這一點:
var i = 0;
for (; i < 10; i++) {
console.log(`+++++`, i)
setTimeout(function () {
console.log(i)
})
}
執行這段程式碼後的結果:
+++++ 0
+++++ 1
+++++ 2
+++++ 3
+++++ 4
+++++ 5
+++++ 6
+++++ 7
+++++ 8
+++++ 9
10
10
10
10
10
10
10
10
10
10
由此可見,當開始執行setTimeout()
中的程式碼時for
迴圈外面的變數i
就已經變成了10
,使用console.log(i)
從作用域查詢到的i
值就是10
,因此十次setTimeout()
中的程式碼就都列印出了10
。
解決方式
原因找到了,罪魁禍首說到底就是由於var
變數的作用域特性以及作用域範圍導致的。那解決這個問題的關鍵點還是在怎麼控制變數的作用域。
方法一
要控制變數的作用域,最常見的手段,就是使用函式閉包
將變數值封閉在指定的作用域內。
我們可以在setTimeout()
的外面進行一層簡單的包裝來形成閉包,達到將每次迴圈時的i
值封閉在閉包內部:
for (var i = 0 ; i < 10; i++) {
(function (i) {
setTimeout(function () {
console.log(i)
})
})(i)
}
這樣的話,在setTimeout()
中查詢變數i
的時候,就會獲取到封入閉包並以引數形式傳入的引數i
了。
方法二
除了函式閉包,我們還可以使用的解決方案,就是ES6中新引入的let
變數宣告。與var
不同的是,由let
宣告的變數的作用域是隻在其宣告的塊或子塊中可用,所以它被稱為塊級作用域變數。
我們這道題的程式碼只要做很小的修改,只需要將var
替換成let
,就能如我們期望的那樣工作了:
for (let i = 0 ; i < 10; i++) {
setTimeout(function () {
console.log(i)
})
}
使用了let
後,變數i
的作用域被限定在for
語句塊以及子塊setTimeout()
中,並且:
子塊中的變數值是該子塊產生時的那個值
是不是覺得let
變數的作用域關係比較清晰?在現在的實際開發中,我們也更推薦使用let
來替代var
進行變數宣告,它會使你的程式碼更清晰更簡化,不容易出bug。
相關文章
- JavaScript全域性作用域下,變數加var和不加var的區別。JavaScript變數
- 教你理解let和var的區別
- 理解 Javascript 中變數的作用域JavaScript變數
- var與let宣告變數的區別變數
- 關於JS中變數的作用域-例項JS變數
- 強大的CSS:var變數的區域性作用域(繼承)特性CSS變數繼承
- 解除 for 迴圈時 var 和 let 定義變數的困惑變數
- [面試專題]從for迴圈看let和var的區別面試
- var、let、const宣告變數的區別變數
- Shell變數的作用域問題變數
- 1.變數:var,let,const變數
- 加深對 JavaScript This 的理解JavaScript
- 關於global和$GLOBALS[]的一道經典面試題面試題
- 由一道面試題理解類載入機制面試題
- var 和 let 的區別
- 關於PHP字串的一道面試題PHP字串面試題
- 對js中執行環境、作用域和作用域鏈的理解JS
- javascript中的作用域(全域性變數和區域性變數)JavaScript變數
- 變數、作用域和記憶體問題變數記憶體
- 關於argument變數的理解變數
- 變數和函式宣告提升,let和var const區別變數函式
- lisp 變數的作用域Lisp變數
- 一道關於Promise應用的面試題Promise面試題
- var、let、const變數宣告的區別及特點變數
- JavaScript中變數和作用域JavaScript變數
- CSS變數的作用域和預設值CSS變數
- javascript:變數、作用域和記憶體問題JavaScript變數記憶體
- javascript變數、作用域和記憶體問題......JavaScript變數記憶體
- java中變數的作用域Java變數
- JavaScript 變數的作用域鏈JavaScript變數
- 變數作用域變數
- js中變數作用域問題JS變數
- 深入理解JavaScript的作用域與變數提升(hoisting)JavaScript變數
- 11-程式碼塊和變數的作用域變數
- 一道面試題考驗了你對java的理解程度面試題Java
- 面試題:一道關於解構賦值和引數預設值的程式設計題面試題賦值程式設計
- 淺談let和var的區別
- var、let和const的區別