ES5 只有全局做用域和函數做用域,沒有塊級做用域,這帶來不少不合理的場景。git
經過var聲明變量存在變量提高:github
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。函數的變量提高是根據最近的外層函數提高,沒有函數就爲全局下提高。oop
爲規範變量使用控制,ECMAScript 6 引入了塊級做用域。 塊級做用域就是 {} 之間的區域ui
咱們來整理一下 let 和 const 的特色:spa
if(condition) {
let value = 1
}
console.log(value) // Uncaught ReferenceError: value is not defined
複製代碼
無論 conditon 爲 true 或者 false ,都沒法輸出value,結果爲 Uncaught ReferenceError: value is not definedcode
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
複製代碼
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 中是如何編譯 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系列文章地址:github.com/changming-h…