詳解 let 和 var

常吃腦殘片發表於2018-11-27

來源:exploringjs.com/es6/ch_vari…

ES6 提供了兩種宣告變數的新方法: letconst ,它們主要取代 ES5 宣告變數的方式:var

9.1.1 let

letvar 類似,但它宣告的變數是具有塊級作用域的(block-scoped),它只存在於當前塊中。 var 是函式作用域(function-scoped)。

在下面的程式碼中,您可以看到 let 宣告的 變數 tmp 只存在於 從(A)行開始的塊中:

function order(x, y) {
    if (x > y) {
        // (A)
        let tmp = x
        x = y
        y = tmp
    }
    console.log(tmp === x) // ReferenceError: tmp is not defined
    return [x, y]
}
複製程式碼

9.1.2 const

const 作用類似於 let,但是您宣告的變數必須立即初始化,並且該值之後不能更改。

const foo;
    // SyntaxError: missing = in const declaration

const bar = 123;
bar = 456;
    // TypeError: `bar` is read-only
複製程式碼

因為 for-of 在每次迴圈迭代中建立一個繫結(變數的儲存空間),所以可以使用 const 宣告迴圈變數

for (const x of ['a', 'b']) {
    console.log(x)
}
// Output:
// a
// b
複製程式碼

9.1.3 宣告變數的方式

下表概述了在 ES6 中宣告變數的六種方式(受 kangax 表的啟發 ):

提升形式 作用域形式 建立全域性屬性
var Declaration Function Yes
let Temporal dead zone Block No
const Temporal dead zone Block No
function Complete Block Yes
classs No Block No
import Complete Module-global No

9.2 通過 letconst 阻止作用域

letconst 建立了塊作用域的變數 - 它們只存在於包圍它們的最裡面的塊中。 以下程式碼演示了 const 宣告的變數 tmp 僅存在於 if 語句的塊中:

function func() {
    if (true) {
        const tmp = 123;
    }
    console.log(tmp); // ReferenceError: tmp is not defined
}
複製程式碼

相比之下,var 宣告的變數是函式級別的:

function func() {
    if (true) {
        var tmp = 123;
    }
    console.log(tmp); // 123
}
複製程式碼

塊作用域意味著您可以在函式中隱藏變數:

function func() {
  const foo = 5;
  if (···) {
     const foo = 10; // shadows outer `foo`
     console.log(foo); // 10
  }
  console.log(foo); // 5
}
複製程式碼

9.3 const 建立不可變變數

let 建立的變數是可變的:

let foo = 'abc';
foo = 'def';
console.log(foo); // def
複製程式碼

常量(const建立的變數)是不可變的,不能再給它們賦不同的值:

const foo = 'abc';
foo = 'def'; // TypeError
複製程式碼

規範細節:更改 const 變數總是丟擲 TypeError

通常,根據 SetMutableBinding() ,更改不可變繫結僅在嚴格模式下導致異常。 但 const 宣告的變數總是產生嚴格的繫結 - 參見 FunctionDeclarationInstantiation(func, argumentsList)

9.3.1 陷阱:const 不會使值不可變

const 只意味著一個變數總是具有相同的值,但它並不意味著該值本身是不可變的或成為不可變的。 例如, obj 是一個常量,但它指向的值是可變的 - 我們可以為它新增一個屬性:

const obj = {};
obj.prop = 123;
console.log(obj.prop); // 123
複製程式碼

但是,我們不能為 obj 分配不同的值:

obj = {}; // TypeError
複製程式碼

如果您希望 obj 的值是不可變的,那麼您必須自己處理它。例如, 凍結它

const obj = Object.freeze({});
obj.prop = 123; // TypeError
複製程式碼

9.3.1.1 陷阱:Object.freeze()是淺層的

請記住,Object.freeze() 是淺層的,它只會凍結其引數的屬性,而不會凍結其屬性中儲存的物件。例如,物件 obj 被凍結:

> const obj = Object.freeze({ foo: {} });
> obj.bar = 123
TypeError: Can't add property bar, object is not extensible
> obj.foo = {}
TypeError: Cannot assign to read only property 'foo' of #<Object>
複製程式碼

但是物件 obj.foo 不是。

> obj.foo.qux = 'abc';
> obj.foo.qux
'abc'
複製程式碼

9.3.2 迴圈體中的 const

一旦建立了 const 變數,就無法更改它。但這並不意味著您無法重新進入其作用域並賦予新的值重新開始,每次迴圈都像是一次輪迴。例如,通過迴圈:

function logArgs(...args) {
    for (const [index, elem] of args.entries()) { // (A)
        const message = index + '. ' + elem; // (B)
        console.log(message);
    }
}
logArgs('Hello', 'everyone');

// Output:
// 0. Hello
// 1. everyone
複製程式碼

此程式碼中有兩個 const 宣告,在行(A) 和行(B) 中。在每次迴圈迭代期間,它們的常量有不同的值。

9.4 臨時死區(temporal dead zone)

letconst宣告的變數具有所謂的 臨時死區(TDZ):當進入其作用域時,在執行到達宣告之前不能訪問(獲取或設定)它。讓我們比較 var 變數(沒有TDZ)和let變數(有TDZ)的生命週期。

9.4.1 var 變數的生命週期

var變數沒有臨時死區。 他們的生命週期包括以下步驟:

  • 當進入 var 變數的作用域(其所在的函式)時,將為它建立儲存空間(繫結)。 通過將變數設定為undefined,立即初始化變數。
  • 當作用域內的執行到達宣告時,該變數被設定為初始化 程式指定的值(賦值)- 如果有的話。如果沒有,則變數的值仍未 undefined

9.4.2 let 變數的生命週期

通過 let 宣告的變數有臨時死區,它們的生命週期如下所示:

  • 當進入 let 變數的作用域(其所在的塊)時,將為其建立儲存空間(繫結)。變數仍然未初始化。
  • 獲取或設定未初始化的變數會導致 ReferenceError
  • 當範圍內的執行到達宣告時,該變數被設定為初始化程式指定的值(賦值)- 如果有的話。如果沒有,則將變數的值設定為 undefined

const變數與 let 變數的工作方式類似,但它們必須具有初始化程式(即立即設定值)並且不能更改。

9.4.3 示例

在 TDZ 中,如果獲取或設定變數,則丟擲異常:

let tmp = true;
if (true) { // 這裡進入塊級空間,TDZ 開始
    // 未被初始化的`tmp`儲存空間 被建立
    console.log(tmp); // 異常:ReferenceError

    let tmp; // TDZ 結束, `tmp` 被賦值為‘undefined’
    console.log(tmp); //列印:undefined

    tmp = 123;
    console.log(tmp); // 列印:123
}
console.log(tmp); // 列印:true
複製程式碼

如果有初始化,則TDZ 會在初始化後並將結果賦值給變數後結束:

let foo = console.log(foo); // 列印:ReferenceError
複製程式碼

以下程式碼演示死區實際上是臨時的 (基於時間)而不是空間(基於位置):

if (true) { // 進入塊級空間,TDZ 開始
    const func = function () {
        console.log(myVar); // 這裡沒有問題
    };

    // 當前處於 TDZ 內部
    // 如果 訪問 `myVar` 會出現異常 `ReferenceError`

    let myVar = 3; // TDZ 結束
    func(); // 此時 TDZ 不存在,呼叫func
}
複製程式碼

9.4.4 typeof 會為TDZ 中的變數丟擲 ReferenceError

如果通過 typeof 訪問時間死區中的變數,則會出現異常:

if (true) {
    console.log(typeof foo); // ReferenceError (TDZ)
    console.log(typeof aVariableThatDoesntExist); // 'undefined'
    let foo;
}
複製程式碼

為什麼? 理由如下:foo 不是未宣告的,它是未初始化的。你應該意識到它的存在,但你沒有。因此,被警告似乎是可取的。

此外,這種檢查僅對有條件地建立全域性變數有用。在正常程式中不需要做的。

9.4.4.1 有條件地建立變數

在有條件地建立變數時,您有兩種選擇。

選項1 - typeofvar

if (typeof someGlobal === 'undefined') {
    var someGlobal = { ··· };
}
複製程式碼

此選項僅適用於全域性範圍(因此不在ES6模組內)。

選項2 - window

if (!('someGlobal' in window)) {
    window.someGlobal = { ··· };
}
複製程式碼

9.4.5 為什麼會出現臨時死區(TDZ)?

constlet 產生TDZ,有幾個原因:

  • 捕獲程式設計錯誤:能夠在宣告之前訪問變數很奇怪。如果你這樣做了,可能是意外,你就應該得到警告。
  • 對於 const:使 const 正常工作很困難。引用Allen Wirfs-Brock:“TDZs ......為const提供了理性的語義。 對該主題進行了重要的技術討論,並且TDZ 成為最佳解決方案。“ let 也有一個臨時死區,這樣 letconst 之間的切換不會以意料之外的方式改變行為。
  • 面向未來的防護:JavaScript最終可能會有一些防護,一種在執行時強制執行變數具有正確值的機制(想想執行時型別檢查)。如果變數的值在宣告之前 undefined,那麼該值可能與 其保護所給出的保證 相沖突。

9.4.6 進一步閱讀

本節的來源:

9.5 迴圈頭部中的 letconst

以下迴圈允許您在其頭部宣告變數:

  • for
  • for-in
  • for-of

要進行宣告,可以使用varletconst 。 他們每個都有不同的影響,我將在下面解釋。

9.5.1 for 迴圈

在for迴圈頭部中的var 變數為該變數建立單個繫結 (儲存空間):

const arr = [];
for (var i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [3,3,3]
複製程式碼

三個箭頭函式體中的每個i 指向相同的繫結,這就是它們都返回相同值的原因。

let 變數,則為每個迴圈迭代建立一個新繫結:

const arr = [];
for (let i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [0,1,2]
複製程式碼

這一次,每個 i 指的是一個特定迭代的繫結,並保留當時的值。因此,每個箭頭函式返回不同的值。

constvar 一樣的工作,但是你不能改變const 變數的初始值:

// TypeError: Assignment to constant variable
// (due to i++)
for (const i=0; i<3; i++) {
    console.log(i);
}
複製程式碼

為每次迭代獲取新的繫結起初可能看起來很奇怪,但是每當使用迴圈 建立引用迴圈變數的函式時,它非常有用,後面的小節將對此進行解釋。

for 迴圈:規範中的每個迭代繫結 for 迴圈的求值var 作為第二種情況處理,並將 let/const 作為第三種情況。 只有 let 變數被新增到列表 perIterationLets(步驟9)中,它作為倒數第二個引數 perIterationBindings 傳遞給ForBodyEvaluation()

9.5.2 for-of 迴圈和 for-in 迴圈

for-of 迴圈中,var建立單個繫結:

const arr = [];
for (var i of [0, 1, 2]) {
    arr.push(() => i);
}
arr.map(x => x()); // [2,2,2]
複製程式碼

const 每次迭代建立一個不可變的繫結:

const arr = [];
for (const i of [0, 1, 2]) {
    arr.push(() => i);
}
arr.map(x => x()); // [0,1,2]
複製程式碼

let 也為每次迭代建立一個繫結,但它建立的繫結是可變的。

for-in 迴圈與 for-of 迴圈的工作方式類似。

for-of迴圈:規範中的每次迭代繫結 for-of 中的每次迭代繫結由 ForIn/OfBodyEvaluation處理。在步驟5.b中,建立一個新環境,並通過BindingInstantiation為其新增繫結(對於let可變的,對於const不可變的)。當前迭代值儲存在變數nextValue,用於以兩種方式之一初始化繫結:

單變數宣告(步驟5.hi):通過 InitializeReferencedBinding 處理 解構(步驟5.i.iii):通過一個 BindingInitialization 情況 ( ForDeclaration )來處理,它呼叫另一個BindingInitialization ( BindingPattern )的情況。

9.5.3 為什麼每次迭代繫結是有用的?

以下是顯示三個連結的HTML頁面:

  1. 如果單擊“yes”,則將其翻譯為“ja”。
  2. 如果單擊“no”,則將其翻譯為“nein”。
  3. 如果單擊“perhaps”,則會將其翻譯為“vielleicht”。
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    <div id="content"></div>
    <script>
        const entries = [
            ['yes', 'ja'],
            ['no', 'nein'],
            ['perhaps', 'vielleicht'],
        ];
        const content = document.getElementById('content');
        for (const [source, target] of entries) { // (A)
            content.insertAdjacentHTML('beforeend',
                `<div><a id="${source}" href="">${source}</a></div>`);
            document.getElementById(source).addEventListener(
                'click', (event) => {
                    event.preventDefault();
                    alert(target); // (B)
                });
        }
    </script>
</body>
</html>
複製程式碼

顯示的內容取決於可變target((B)行)。 如果我們在行(A)中使用了var 而不是 const,那麼整個迴圈只會有一個繫結,之後 target 將具有值 vielleicht 。 因此,無論您點選什麼連結,您都會得到翻譯vielleicht

值得慶幸的是,使用 const,我們每個迴圈迭代都有一個繫結,並正確地顯示轉換。

9.6 引數作為變數

9.6.1 引數與區域性變數

如果let 與引數具有相同名稱的變數,則會出現靜態(載入時)錯誤:

function func(arg) {
    let arg; // static error: duplicate declaration of `arg`
}
複製程式碼

在塊內執行相同操作會影響引數:

function func(arg) {
    {
        let arg; // shadows parameter `arg`
    }
}
複製程式碼

相比之下, var 與引數同名的變數什麼都不做,就像在同一作用域中重新宣告 var變數一樣。

function func(arg) {
    var arg; // does nothing
}
function func(arg) {
    {
        // We are still in same `var` scope as `arg`
        var arg; // does nothing
    }
}
複製程式碼

9.6.2 引數預設值和 臨時死區

如果引數具有預設值,則它們將被視為一系列 let 語句,並受臨時死區的影響:

// OK: `y` accesses `x` after it has been declared
function foo(x=1, y=x) {
    return [x, y];
}
foo(); // [1,1]

// 異常: `x` 試圖 在TDZ 中訪問 `y`
function bar(x=y, y=2) {
    return [x, y];
}
bar(); // ReferenceError
複製程式碼

9.6.3 引數預設值看不到主體的作用域

引數預設值的範圍與主體作用域分開(前者圍繞後者)。這意味著在引數預設值中定義的方法或函式不會看到主體的區域性變數

const foo = 'outer';
function bar(func = x => foo) {
    const foo = 'inner'; // 不會被看到哦~
    console.log(func()); // outer
}
bar();
複製程式碼

9.7 全域性物件

JavaScript的全域性物件 (Web瀏覽器中的 window ,Node.js中的 global)更像是一個bug而不是一個功能,特別是在效能方面。這就是ES6 引入區別的原因:

  • 全域性物件的所有屬性都是全域性變數。在全域性範圍中,以下宣告建立了這樣的屬性:
    • var宣告
    • 函式宣告
  • 但是現在還有全域性變數不是全域性物件的屬性。在全域性範圍中,以下宣告建立了此類變數: let 宣告 const 宣告 類宣告

注意,模組的主體不是在全域性範圍內執行的,只有指令碼才是。因此,各種變數的環境形成以下鏈。

variables----environment_chain

9.8 函式宣告和類宣告

函式宣告...

  • 是塊作用域,比如 let
  • 在全域性物件中建立屬性(在全域性作用域中),如var
  • 被提升:與其在其作用域中宣告的位置無關,函式宣告總是在作用域中的開頭建立。

以下程式碼演示了函式宣告的提升:

{ // Enter a new scope

    console.log(foo()); // OK, due to hoisting
    function foo() {
        return 'hello';
    }
}
複製程式碼

類宣告...

  • 是塊作用域。
  • 不要在全域性物件上建立屬性。
  • 沒有提升。

類沒有被提升可能會令人驚訝,因為在底層引擎下,它們建立了函式。 這種行為的基本原理是它們的 extends 子句的值是通過表示式定義的,並且這些表示式必須在適當的時間執行。

{   // 進入一個新的作用域
    const identity = x => x;

    // 目前處於 `MyClass` 的TDZ 中
    const inst = new MyClass(); // ReferenceError

    //注意 `extends` 子句的表示式
    class MyClass extends identity(Object) {
    } 
}
複製程式碼

9.9 編碼樣式: const與let對比var

我建議總是使用 letconst

  1. 使用 const:只要變數永遠不會改變其值,就可以使用它。 換句話說:變數不應該是賦值的左邊,也不應該是++或-的運算元。允許更改 const 變數引用的物件:

        const foo = {};
        foo.prop = 123; // OK
    複製程式碼

    你甚至可以在 for-of 迴圈中使用const,因為每次迴圈迭代都會建立一個(不可變的)繫結:

        for (const x of ['a', 'b']) {
            console.log(x);
        }
        // Output:
        // as
        // b
    複製程式碼

    for-of 迴圈體內,x不能改變。

  2. 稍後會更改變數的初始值時,則應該使用 let

        let counter = 0; // initial value
        counter++; // change
    
        let obj = {}; // initial value
        obj = { foo: 123 }; // change
    複製程式碼
  3. 避免var 。

如果遵循這些規則,var 將僅出現在遺留程式碼中,作為需要仔細重構的訊號。

var 會做一件letconst 不會做的事情:通過它宣告的變數成為了全域性物件的屬性。然而,這通常不是一件好事。您可以通過分配到 window (在瀏覽器中)或 global(在Node.js中)來實現相同的效果。

9.9.1 替代的方法

對於上面提到的樣式規則,另一種選擇是隻對完全不可變的東西(原始值和凍結的物件)使用 const 。然後有兩種方法:

  1. 優先 constconst 標記不可變的繫結。
  2. 優先 letconst 標記不可變的值。

我略微傾向於 1,但 2 也可以。

相關文章