為什麼需要塊級作用域
ES5 只有全域性作用域和函式作用域,沒有塊級作用域,這帶來很多不合理的場景。
通過var宣告變數存在變數提升:
if (condition) {
var value = 1
}
console.log(value)
複製程式碼
初學者可能會認為當變數condition為true時,才會建立value。當condition為false時,不會建立value,結果應該是報錯。然而因為JavaScript存在變數提升的概念,程式碼等同於:
var value
if (condition) {
value = 1
}
console.log(value) // undefined
複製程式碼
所有當condition為false,輸入結果為undefined。
ES5 只有全域性作用域和函式作用域,其中變數提升也分成兩種情況:一種全域性宣告的變數,提升會在全域性最上面,上面就屬於全域性變數宣告;一種是函式中宣告的變數,提升在函式的最上方:
function fn() {
var value
if (condition) {
value = 1
}
console.log(value) // undefined
}
console.log(value) // Uncaught ReferenceError: value is not defined
複製程式碼
所有當condition為false,函式內輸入結果為undefined,函式輸入就會報錯Uncaught ReferenceError: value is not defined。函式的變數提升是根據最近的外層函式提升,沒有函式就為全域性下提升。
為規範變數使用控制,ECMAScript 6 引入了塊級作用域。 塊級作用域就是 {} 之間的區域
let 和 const
我們來整理一下 let 和 const 的特點:
- 不存在變數提升
if(condition) {
let value = 1
}
console.log(value) // Uncaught ReferenceError: value is not defined
複製程式碼
不管 conditon 為 true 或者 false ,都無法輸出value,結果為 Uncaught ReferenceError: value is not defined
- 重複宣告報錯
let value = 1
let value = 2
複製程式碼
重複宣告同一個變數,會直接報錯 Uncaught SyntaxError: Identifier 'value' has already been declared
- 不繫結在全域性作用域上
var value = 1
console.log(window.value) // 1
複製程式碼
在來看一下let宣告:
let value = 1
console.log(window.value) // undefined
複製程式碼
let 和 const 的區別:
const宣告一個只讀的常量。一旦宣告,常量的值就不能改變。
const value = 1
value = 2 // Uncaught TypeError: Assignment to constant variable.
複製程式碼
上面程式碼表明改變常量的值會報錯。
const宣告的變數不得改變值,這意味著,const一旦宣告變數,就必須立即初始化,不能留到以後賦值。
const foo;
// SyntaxError: Missing initializer in const declaration
複製程式碼
上面程式碼表示,對於const來說,只宣告不賦值,就會報錯。
對於物件的變數,變數指向是資料指向的記憶體地址。const只能保證資料指向記憶體地址不能改變,並不能保證該地址下資料不變。
const data = {
value: 1
}
// 更改資料
data.value = 2
console.log(data.value) // 2
// 更改地址
data = {} // Uncaught TypeError: Assignment to constant variable.
複製程式碼
上述程式碼中,常量 data 儲存的是一個地址,這裡地址指向一個物件。不可變的只是這個地址,即不能將 data 指向另一個地址,但是物件本身是可以變的,所以依然為其更改或新增新屬性。
暫時性死區
在程式碼塊內,使用let命令宣告變數之前,該變數都是不可用的。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱 TDZ)。
let 和 const 宣告的變數不會被提升到作用域頂部,如果在宣告之前訪問這些變數,會導致報錯:
console.log(typeof value); // Uncaught ReferenceError: value is not defined
let value = 1;
複製程式碼
這是因為 JavaScript 引擎在掃描程式碼發現變數宣告時,要麼將它們提升到作用域頂部(遇到 var 宣告),要麼將宣告放在 TDZ 中(遇到 let 和 const 宣告)。訪問 TDZ 中的變數會觸發執行時錯誤。只有執行過變數宣告語句後,變數才會從 TDZ 中移出,然後方可訪問。
看似很好理解,不保證你不犯錯:
var value = 'global';
// 例子1
(function() {
console.log(value);
let value = 'local';
}());
// 例子2
{
console.log(value);
const value = 'local';
};
複製程式碼
兩個例子中,結果並不會列印 "global",而是報錯 Uncaught ReferenceError: value is not defined,就是因為 TDZ 的緣故。
常見面試題
for(var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
})
}
// 3
// 3
// 3
複製程式碼
上述程式碼中,我們期望輸出0,1,2三個值,但是輸出結果是 3,3,3 ,不符合我們的預期。
解決方案如下: 使用閉包解法
for(var i = 0; i < 3; i++) {
(function(i) {
setTimeout(() => {
console.log(i)
})
})(i)
}
// 0
// 1
// 2
複製程式碼
ES6 的 let 解法
for(let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
})
}
// 0
// 1
// 2
複製程式碼
上述程式碼中,變數 i 是 let 宣告的,當前的i只在本輪迴圈有效,所以每一次迴圈的i其實都是一個新的變數,所以最後輸出的是0,1,2。你可能會問,如果每一輪迴圈的變數i都是重新宣告的,那它怎麼知道上一輪迴圈的值,從而計算出本輪迴圈的值。這是因為 JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算.
另外,for迴圈還有一個特別之處,就是設定迴圈變數的那部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
複製程式碼
上面程式碼正確執行,輸出了 3 次abc。這表明函式內部的變數i與迴圈變數i不在同一個作用域,有各自單獨的作用域。
如果嘗試將 let 改成 const 定義:
for (const i = 0; i < 3; i++) {
console.log(i);
}
// 0
// Uncaught TypeError: Assignment to constant variable.
複製程式碼
上述程式碼中,會先輸出一次 0,然後程式碼就會報錯。這是由於for迴圈的執行順序造成的,i 定義為 0,然後執行 i < 3比較,符合條件執行迴圈主體,輸出一次 0, 然後執行 i++,由於 i 使用const定義的只讀變數,程式碼執行報錯。
說完了普通的for迴圈,我們還有for…in迴圈呢~
那下面的結果是什麼呢?
const object = {a: 1, b: 1, c: 1};
for (const key in object) {
console.log(key)
}
// a
// b
// c
複製程式碼
上述程式碼中,雖然使用 const 定義 key 值,但是程式碼中並沒有嘗試修改 key 值,程式碼正常執行,這也是普通for迴圈和for…in迴圈的區別。
Babel編譯
在 Babel 中是如何編譯 let 和 const 的呢?我們來看看編譯後的程式碼:
let value = 1;
複製程式碼
編譯為:
var value = 1;
複製程式碼
我們可以看到 Babel 直接將 let 編譯成了 var,如果是這樣的話,那麼我們來寫個例子:
if (false) {
let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined
複製程式碼
如果還是直接編譯成 var,列印的結果肯定是 undefined,然而 Babel 很聰明,它編譯成了:
if (false) {
var _value = 1;
}
console.log(value);
複製程式碼
我們再寫個直觀的例子:
let value = 1;
{
let value = 2;
}
value = 3;
複製程式碼
var value = 1;
{
var _value = 2;
}
value = 3;
複製程式碼
本質是一樣的,就是改變數名,使內外層的變數名稱不一樣。
那像 const 的修改值時報錯,以及重複宣告報錯怎麼實現的呢?
其實就是在編譯的時候直接給你報錯……
那迴圈中的 let 宣告呢?
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
複製程式碼
Babel 巧妙的編譯成了:
var funcs = [];
var _loop = function _loop(i) {
funcs[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 10; i++) {
_loop(i);
}
funcs[0](); // 0
複製程式碼
專案實踐
在我們實際專案開發過程中,應該預設使用 let 定義可變的變數,使用 const 定義不可變的變數,而不是都使用 var 來定義變數。同時,變數定義位置也有一定區別,使用 var 定義變數都會在全域性頂部或者函式頂部定義,防止變數提升造成的問題,對於使用 let 和 const 定義遵循就近原則,即變數定義在使用的最近的塊級作用域中。
ES6系列文章
ES6系列文章地址:github.com/changming-h…