ES6 學習筆記 - 塊級做用域綁定

本文知識點主要整理自《深刻理解 ES6(Understanding ECMAScript 6)》中文版實體書的內容,部分地方會加上本身的理解,同時書中敘述比較模糊的部分參考了阮一峯老師的《ECMAScript 6 入門》與網絡上其餘大佬們的博客、問答,篇幅有限沒法一一列出,在此表示感謝。javascript

var 聲明及變量提高機制

變量提高(Hoisting)

在函數做用域或者全局做用域中使用 var 聲明的變量,不管是在哪裏進行聲明,都會被當成在當前做用域的頂部進行的變量聲明,這就是變量提高(Hoisting)。java

如:git

在函數 temp 中聲明局部變量,與在函數 temp 後聲明全局變量es6

function temp (condition) {
    if (condition) {
        var a = 1; // 函數的局部變量
    } else {
        console.log(a); // undefined
    }
}

var b = 2; // 所有變量
複製代碼

當預編譯的時候,其實是將上面的代碼轉化爲:github

var b; // 全局變量 b 被提高到全局做用於頂部進行了聲明

function temp (conditionP { var a; // 局部變量 a 被提高到函數做用於頂部進行了聲明 if (condition) {
        a = 1;
    } else {
        console.log(a); // undefined
    }
}

b = 2;
複製代碼

同時因爲變量提高的緣由,即使在 tempconditionfalse,在其 else 分支內仍然能夠訪問到 a數組

塊級聲明

因爲變量提高的存在,對於接觸 JavaScript 的人來講不免會不太習慣,甚至會致使一些 bug。所以 ES6 加入了塊級做用域來對變量的聲明週期進行控制。網絡

ES6 的塊級聲明做用域指定的區塊之間,主要有:app

  • 函數內部
  • 塊中(字符 { 與 } 之間)

let 聲明

let 的用法與 var 相同,使用 let 進行聲明就能將變量的做用域限制在區塊中,不會進行變量提高。函數

若是上文的代碼中,a 使用 let 進行聲明的話,那麼在 if 語句中 conditionfalse 時,else 分支將沒法訪問到 a 的值。工具

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

禁止重複聲明

同一個做用域內,不能使用 let 再次聲明一個已經存在的變量,不論該變量原先是用什麼關鍵字聲明的。

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

一樣,若是是先使用 let 聲明,再使用其餘關鍵字聲明,一樣會報錯。

若是都是使用 var 關鍵字則不會報錯

若是是在當前做用域下內嵌另外一個做用域,則能夠在內嵌做用域中聲明與父級做用域同名的變量,不過在內嵌做用域中的變量在內嵌做用域內會覆蓋掉父級做用域的變量值。

const 聲明

const 聲明的是常量,常量一旦被賦值以後便不可改變。所以,每個常量在被聲明的同時 必須進行初始化

constlet 相似,都不會產生變量提高,而且聲明的變量都只做用於域塊級做用域。同時,若是採用 const 聲明已被聲明的變量,一樣會報錯。

let 不一樣的是,不論嚴格模式或者非嚴格模式下,const 聲明過的變量都不能對其 進行改變。

使用 const 聲明對象

與其餘的語言中的常量很不一樣的一點,const 聲明的常量雖然不可以改變值,但當使用 const 聲明對象時,能夠改變對象的屬性值。

const person = {
    name: 'Jack',
};

person.name = 'Harry'; // 不會報錯

person = { name: 'Peter' }; // 報錯
複製代碼

在 JavaScript 中,對象是引用數據類型,即上面代碼中 person 存儲的實際上是對象的引用(引用能夠理解爲內存地址)。而對 person.name 的值的改變是改變 person 引用的對象,對 person 這個 引用 自己並未做出修改,所以使用 const 聲明的 person 並未報錯。

而當使用 { name: 'Peter' }person 進行修改時,其實是將 person 的引用 更改 到新對象的內存地址,person 的值被改變了,所以報錯。

臨時死區

當使用 let 或者 const 聲明參數時,在預編譯階段,JavaScript 引擎會將這些參數綁定到其對應的做用域內,而且在執行到聲明語句以前若是對這些變量進行操做 均會報錯,即使是 typeof

咱們將變量在聲明以前所處的封閉區域成爲 臨時死區暫時性死區 (Temporal Dead Zone,簡稱 TDZ)。

JavaScript 引擎在預編譯時,會將變量提高到做用域頂部(var),或將變量放入 TDZ(letconst)。訪問 TDZ 中的變量會報錯。當執行變量聲明語句後,變量纔會從 TDZ 中移除。

注意,TDZ 是綁定做用域的。若是在 TDZ 外訪問變量則不會報錯。如:

console.log(typeof param); // 輸出 'undefined',此處的 param 爲全局變量,預編譯的時候會有變量提高,所以是 undefined

// JavaScript 引擎在預編譯到 if 區塊時,會建立一個對應的 TDZ 而且把 param 加入其中
if (condition) {
    param = 9; // 報錯,訪問了 if 區塊內的 TDZ 內的變量 param

    let param = 1; // 此時 param 從 TDZ 中移出,能夠訪問
}
複製代碼

循環中的塊做用域綁定

在 ES5 中,在循環內部聲明的變量,在循環外部仍舊能夠訪問。

for (var i = 0; i < 10; i += 1) {
    console.log(i);
}

console.log(i); // 10;
複製代碼

這也是因爲變量提高, i 的聲明在預編譯時被提到全局做用域頂部,所以在循環結束後外部仍舊能夠訪問 i

若是採用 let 聲明的話,那麼 i 在循環結束後就會被銷燬,外部沒法進行訪問。

for (let i = 0; i < 10; i += 1) {
    console.log(i);
}

console.log(i); // 報錯,沒法訪問 i
複製代碼

循環中的函數

var funcs = [];

for (var i = 0; i < 10; i += 1) {
    funcs.push(function () {
        console.log(i);
    });
}

funcs.forEach(function (func) {
    func();
});
複製代碼

以上代碼執行後會輸出 10 個 10。這是因爲變量提高的緣由,i 提高到做用域的頂部,而且在循環外也可以訪問,在 for 循環執行完畢後,i 的值爲 10,所以在 forEach 遍歷數組內的函數而且執行時,均輸出 10。

在 ES5 中問了解決該問題,經常使用的方式是使用 當即調用函數表達式(IIFE)。具體寫法以下:

var funcs = [];

for (var i = 0; i < 10; i += 1) {
    funcs.push((function (value) {
        return function () {
            console.log(value);
        }
    } (i)));
}

funcs.forEach(function (func) {
    func(); // 0, 1, 2, ..., 10
});

複製代碼

在 IIFE 中,將 i 進行值傳遞(函數的傳參是值傳遞),建立副本而且存儲爲變量 value ,所以纔可以實現正確輸出。

循環中的 let 聲明

在 ES6 中,能夠在 for 循環中直接使用 let 關鍵字聲明,來達到和 IIFE 同樣的效果。

for (let i = 0; i < 10; i+= 1) {
    funcs.push(function () {
        console.log(i);
    })
}
複製代碼

for 循環中,聲明賦值語句 let i = 0 僅在循環以前執行一次(而且這一句執行的時候是在 父級 做用域,與循環內部的做用域是分開的),在執行循環時, JavaScript 內部會記錄當前循環的值,在進入下一輪時,會 建立一個新的值,而且用記住的值進行計算而且進入下一輪。所以使用 let 關鍵詞聲明的循環在每一次循環時都會獲得一個屬於該次循環的 副本

for-in 語句使用 let 關鍵字是也是一樣的效果。

let 聲明在循環內部的表現是專門定義的,不必定與不產生變量提高的特性相關。

循環中的 const 聲明

// 下面會報錯
for (const i = 0; i < 10; i += 1) {
    ...
}

// 下面則不會報錯
for (const i in obj) {
    ...
}
複製代碼

對於 for 循環來講,每次循環執行完畢,都會去修改 i 的值,而後再建立循環體內塊級做用域的副本,所以在 for 循環用 const 聲明時會報錯。

for-infor-of 在每次迭代時,是每次都會在 新的做用域內 執行 const 聲明,不會修改值,所以不會報錯。

全局塊做用域綁定

使用 var 進行全局變量聲明時,會爲全局對象建立一個新的屬性。以 Web 爲例:

var apple = 1;

console.log(window.apple); // 1
複製代碼

若是全局對象已經存在屬性,則會進行 覆蓋(這是一個隱患)。

使用 letconst 在全局做用域聲明時,會建立一個新的綁定,而且不會覆蓋全局對象已有的屬性。

let RepExp = 'Hello';
console.log(RepExp); // Hello
console.log(window.RepExp === RepExp); // false
複製代碼

最佳實踐

在 ESLint 等代碼規範工具中,推薦的寫法是 默認使用 const,在確實須要改變值或者引用的時候才使用 let。雖然比較繁瑣,可是可以有效下降一些隱性 bug 出現的機率。

詳見 ESLint 的 no-varprefer-const 規則

參考資料

相關文章
相關標籤/搜索