ES6系列之 let 和 const

常銘發表於2019-04-14

為什麼需要塊級作用域

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 的特點:

  1. 不存在變數提升
if(condition) {
    let value = 1
}
console.log(value) // Uncaught ReferenceError: value is not defined
複製程式碼

不管 conditon 為 true 或者 false ,都無法輸出value,結果為 Uncaught ReferenceError: value is not defined

  1. 重複宣告報錯
let value = 1
let value = 2
複製程式碼

重複宣告同一個變數,會直接報錯 Uncaught SyntaxError: Identifier 'value' has already been declared

  1. 不繫結在全域性作用域上
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…

相關文章