ES6 系列之 let 和 const

塊級做用域的出現

經過 var 聲明的變量存在變量提高的特性:git

if (condition) {
    var value = 1;
}
console.log(value);
複製代碼

初學者可能會以爲只有 condition 爲 true 的時候,纔會建立 value,若是 condition 爲 false,結果應該是報錯,然而由於變量提高的緣由,代碼至關於:github

var value;
if (condition) {
    value = 1;
}
console.log(value);
複製代碼

若是 condition 爲 false,結果會是 undefined。面試

除此以外,在 for 循環中:閉包

for (var i = 0; i < 10; i++) {
    ...
}
console.log(i); // 10
複製代碼

即使循環已經結束了,咱們依然能夠訪問 i 的值。異步

爲了增強對變量生命週期的控制,ECMAScript 6 引入了塊級做用域。函數

塊級做用域存在於:oop

  • 函數內部
  • 塊中(字符 { 和 } 之間的區域)

let 和 const

塊級聲明用於聲明在指定塊的做用域以外沒法訪問的變量。ui

let 和 const 都是塊級聲明的一種。lua

咱們來回顧下 let 和 const 的特色:spa

1.不會被提高

if (false) {
    let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined
複製代碼

2.重複聲明報錯

var value = 1;
let value = 2; // Uncaught SyntaxError: Identifier 'value' has already been declared
複製代碼

3.不綁定全局做用域

當在全局做用域中使用 var 聲明的時候,會建立一個新的全局變量做爲全局對象的屬性。

var value = 1;
console.log(window.value); // 1
複製代碼

然而 let 和 const 不會:

let value = 1;
console.log(window.value); // undefined
複製代碼

再來講下 let 和 const 的區別:

const 用於聲明常量,其值一旦被設定不能再被修改,不然會報錯。

值得一提的是:const 聲明不容許修改綁定,但容許修改值。這意味着當用 const 聲明對象時:

const data = {
    value: 1
}

// 沒有問題
data.value = 2;
data.num = 3;

// 報錯
data = {}; // Uncaught TypeError: Assignment to constant variable.
複製代碼

臨時死區

臨時死區(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 的緣故。

循環中的塊級做用域

var funcs = [];
for (var i = 0; i < 3; i++) {
    funcs[i] = function () {
        console.log(i);
    };
}
funcs[0](); // 3
複製代碼

一個老生常談的面試題,解決方案以下:

var funcs = [];
for (var i = 0; i < 3; i++) {
    funcs[i] = (function(i){
        return function() {
            console.log(i);
        }
    }(i))
}
funcs[0](); // 0
複製代碼

ES6 的 let 爲這個問題提供了新的解決方法:

var funcs = [];
for (let i = 0; i < 3; i++) {
    funcs[i] = function () {
        console.log(i);
    };
}
funcs[0](); // 0
複製代碼

問題在於,上面講了 let 不提高,不能重複聲明,不能綁定全局做用域等等特性,但是爲何在這裏就能正確打印出 i 值呢?

若是是不重複聲明,在循環第二次的時候,又用 let 聲明瞭 i,應該報錯呀,就算由於某種緣由,重複聲明不報錯,一遍一遍迭代,i 的值最終仍是應該是 3 呀,還有人說 for 循環的 設置循環變量的那部分是一個單獨的做用域,就好比:

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc
複製代碼

這個例子是對的,若是咱們把 let 改爲 var 呢?

for (var i = 0; i < 3; i++) {
  var i = 'abc';
  console.log(i);
}
// abc
複製代碼

爲何結果就不同了呢,若是有單獨的做用域,結果應該是相同的呀……

若是要追究這個問題,就要拋棄掉以前所講的這些特性!這是由於 let 聲明在循環內部的行爲是標準中專門定義的,不必定就與 let 的不提高特性有關,其實,在早期的 let 實現中就不包含這一行爲。

咱們查看 ECMAScript 規範第 13.7.4.7 節:

let 規範

咱們會發現,在 for 循環中使用 let 和 var,底層會使用不一樣的處理方式。

那麼當使用 let 的時候底層究竟是怎麼作的呢?

簡單的來講,就是在 for (let i = 0; i < 3; i++) 中,即圓括號以內創建一個隱藏的做用域,這就能夠解釋爲何:

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc
複製代碼

而後每次迭代循環時都建立一個新變量,並以以前迭代中同名變量的值將其初始化。這樣對於下面這樣一段代碼

var funcs = [];
for (let i = 0; i < 3; i++) {
    funcs[i] = function () {
        console.log(i);
    };
}
funcs[0](); // 0
複製代碼

就至關於:

// 僞代碼
(let i = 0) {
    funcs[0] = function() {
        console.log(i)
    };
}

(let i = 1) {
    funcs[1] = function() {
        console.log(i)
    };
}

(let i = 2) {
    funcs[2] = function() {
        console.log(i)
    };
};
複製代碼

當執行函數的時候,根據詞法做用域就能夠找到正確的值,其實你也能夠理解爲 let 聲明模仿了閉包的作法來簡化循環過程。

循環中的 let 和 const

不過到這裏尚未結束,若是咱們把 let 改爲 const 呢?

var funcs = [];
for (const i = 0; i < 10; i++) {
    funcs[i] = function () {
        console.log(i);
    };
}
funcs[0](); // Uncaught TypeError: Assignment to constant variable.
複製代碼

結果會是報錯,由於雖然咱們每次都建立了一個新的變量,然而咱們卻在迭代中嘗試修改 const 的值,因此最終會報錯。

說完了普通的 for 循環,咱們還有 for in 循環呢~

那下面的結果是什麼呢?

var funcs = [], object = {a: 1, b: 1, c: 1};
for (var key in object) {
    funcs.push(function(){
        console.log(key)
    });
}

funcs[0]()
複製代碼

結果是 'c';

那若是把 var 改爲 let 或者 const 呢?

使用 let,結果天然會是 'a',const 呢? 報錯仍是 'a'?

結果是正確打印 'a',這是由於在 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 而不是 var ,這種狀況下,對於須要寫保護的變量要使用 const。然而另外一種作法日益普及:默認使用 const,只有當確實須要改變變量的值的時候才使用 let。這是由於大部分的變量的值在初始化後不該再改變,而預料以外的變量之的改變是不少 bug 的源頭。

ES6 系列

ES6 系列目錄地址:github.com/mqyqingfeng…

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級做用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。

若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎star,對做者也是一種鼓勵。

相關文章
相關標籤/搜索