var,let,const三種宣告方式的區別
變數提升
很多人認為,var是存在變數提升了,因此,如果你有如下程式碼:
console.log(foo); // undefined var foo = 123;
程式不會報錯,而是打印出undefined。這是因為,編譯器預編譯的時候,第一步只會記錄變數和函式的定義,第二步才會執行程式(hoisting是對一種現象的描述,而不是一種編譯器具體的行為),所以程式看起來等價於下面的程式碼:
var foo; console.log(foo); // undefined foo = 123;
再來看let是否存在變數提升:
console.log(foo); //Uncaught ReferenceError: Cannot access 'foo' before initialization let foo = 123;
發生了引用錯誤,但是這是因為let定義的變數不存在提升導致的麼?
作為對照,我們直接執行下面的程式碼,看看會出現什麼錯誤:
console.log(foo); // Uncaught ReferenceError: foo is not defined
錯誤原因:foo沒有定義。
如果說let foo不存在變數提升,那麼在執行
console.log(foo); let foo = 123;
也應該報出foo沒有定義這個錯誤的,但是錯誤確是因為foo沒有被初始化。
下面是揭示真相的時刻了:let和const定義的變數都會被提升,但是不會被初始化,不能被引用,不會像var定義的變數那樣,初始值為undefined。因此,在使用console.log(foo)的時候會發生引用錯誤。
為什麼let和const定義的變數不會被初始化呢?主要是因為const。const,顧名思義:常量,const的引用不應被改變。如果編譯器把const初始化為undefined,之後,又讓它等於我們定義的那個值,就改變了const的引用。因此,委員會決定let和const雖然也會發生變數提升,但是沒有任何初始值。
塊級作用域
var、let和const另外一個重要區別就是let和const只在塊級作用域中有效,比如:
{ var a = 'Smiley'; } console.log(a) // 能正確打印出Smiley
但是:
{ let a = 'Smiley'; } console.log(a) // 報錯: Uncaught ReferenceError: a is not defined
迴圈中i的定義
我們來看紅寶書當中的一個例子:
var result = new Array(); for (var i=0; i<10; i++) { result[i] = function() { return i; } } console.log(result[0]()) // 列印10
之所以結果返回10,這是因為所有的i都使用同一個引用,而i最終會被更新為10,因此,result中所有函式都會返回10。
那麼怎麼修復呢?在ES6之前,最常見的修復方法是使用IIFE。但是let和const的發明讓我們有了一種更簡單的修復方法:
var result = new Array(); for (let i=0; i<10; i++) { result[i] = function() { return i; } } console.log(result[0]()); // 列印0
在每次迴圈當中,都會建立一個新的let i(因為let只在塊級作用域有效,因此是不會報錯的),因此result[i]使用的每個i都是不同的,值並不會像使用var的例子那樣不斷增加。
其他
關於var和let還有一些微小的區別,比如:
- 暫時性死區(如果理解了JS的兩步預編譯,這個很容易理解)
- let和const不允許重複宣告
- let和const不會繫結全域性作用域
這些都比較容易理解,不再贅述了。
let和const之間的區別
另外一點值得注意的是,let和const之間的區別。
大家一般認為const是常量,不能被更改。但是請看下面這段程式碼:
const obj = { a: 12 } obj.a = 1;
我們更改了obj這個const的a這個屬性,但是沒有任何報錯。不是說const定義的變數不能被更改麼?其實是const定義的變數的引用不能被更改,在上面的程式中,如果你說obj = ‘abc’,程式是會報錯的。但是上面例子中,引用其實是一個指標,即使指標指向的東西改變了,但只要指標沒有改變,就不會報錯的。所以,建議只使用const定義簡單型別的資料,定義物件還是使用let會不容易產生歧義。