1. 程式人生 > 其它 >第2章 let和const命令

第2章 let和const命令

2.1 let命令

2.1.1 基本用法

ES6 新增了 let 命令,用於宣告變數。其用法類似於 var,但是所宣告的變數只在 let 命令所在的程式碼塊內有效。

{
    let hzh1 = 1;
    var hzh2 = 2;
}

console.log("hzh2 = " + hzh2);
console.log("hzh1 = " + hzh1);
[Running] node "e:\HMV\Babel\hzh.js"
hzh2 = 2
e:\HMV\Babel\hzh.js:7
console.log("hzh1 = " + hzh1);
                        ^

ReferenceError: hzh1 is not defined
    at Object.<anonymous> (e:\HMV\Babel\hzh.js:7:25)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

[Done] exited with code=1 in 0.264 seconds

上面的程式碼在程式碼塊中分別用 let 和 var 聲明瞭兩個變數。然後在程式碼塊之外呼叫這兩個變數,結果let 宣告的變數報錯,var 宣告的變數返回了正確的值。這表明,let 宣告的變數只在其所在程式碼塊內有效。

for迴圈的計數器就很適合使用let命令。

for(let hzh = 1; hzh < 0; hzh++) {
    // ...
}

console.log(hzh);
[Running] node "e:\HMV\Babel\hzh.js"
e:\HMV\Babel\hzh.js:5
console.log(hzh);
            ^

ReferenceError: hzh is not defined
    at Object.<anonymous> (e:\HMV\Babel\hzh.js:5:13)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

[Done] exited with code=1 in 0.178 seconds

以上程式碼中的計數器 hzh 只在 for 迴圈體內有效,在迴圈體外引用就會報錯。

下面的程式碼如果使用 var,最後將輸出10。

// hzh.js

var hzh1 = [];
for (var hzh2 = 0; hzh2 < 10; hzh2++) {
    hzh1[hzh2] = function () {
        console.log("hzh2 = " + hzh2);
    };
    console.log("第 " + hzh2 + " 輪輸出時:" + "hzh2 = " + hzh2);
}

console.log("呼叫hzh1[0]():");
hzh1[0]();
console.log("呼叫hzh1[1]():");
hzh1[1]();
console.log("呼叫hzh1[2]():");
hzh1[2]();
console.log("呼叫hzh1[3]():");
hzh1[3]();
console.log("呼叫hzh1[4]():");
hzh1[4]();
console.log("呼叫hzh1[5]():");
hzh1[5]();
console.log("呼叫hzh1[6]():");
hzh1[6]();
console.log("呼叫hzh1[7]():");
hzh1[7]();
console.log("呼叫hzh1[8]():");
hzh1[8]();
console.log("呼叫hzh1[9]():");
hzh1[9]();
[Running] node "e:\HMV\Babel\hzh.js"
第 0 輪輸出時:hzh2 = 0
第 1 輪輸出時:hzh2 = 1
第 2 輪輸出時:hzh2 = 2
第 3 輪輸出時:hzh2 = 3
第 4 輪輸出時:hzh2 = 4
第 5 輪輸出時:hzh2 = 5
第 6 輪輸出時:hzh2 = 6
第 7 輪輸出時:hzh2 = 7
第 8 輪輸出時:hzh2 = 8
第 9 輪輸出時:hzh2 = 9
呼叫hzh1[0]():
hzh2 = 10
呼叫hzh1[1]():
hzh2 = 10
呼叫hzh1[2]():
hzh2 = 10
呼叫hzh1[3]():
hzh2 = 10
呼叫hzh1[4]():
hzh2 = 10
呼叫hzh1[5]():
hzh2 = 10
呼叫hzh1[6]():
hzh2 = 10
呼叫hzh1[7]():
hzh2 = 10
呼叫hzh1[8]():
hzh2 = 10
呼叫hzh1[9]():
hzh2 = 10

[Done] exited with code=0 in 0.191 seconds

上面的程式碼中,變數 hzh2 是 var 宣告的,在全域性範圍內都有效,所以全域性只有一個變數 hzh2 。每一次迴圈,變數 hzh2 的值都會發生改變,而迴圈內,被賦給陣列 hzh1 的函式內部的 console.log(hzh2) 中的 hzh2 指向全域性的 hzh2 。也就是說,所有陣列 hzh1 的成員中的 hzh2 指向的都是同一個 hzh2 ,導致執行時輸出的是最後一輪的 hzh2 值,也就是 10 。

如果使用 let,宣告的變數僅在塊級作用域內有效,最後將輸出 6 。

// hzh.js

var hzh1 = [];
for (let hzh2 = 0; hzh2 < 10; hzh2++) {
    hzh1[hzh2] = function () {
        console.log("hzh2 = " + hzh2);
    };
    console.log("第 " + hzh2 + " 輪輸出時:" + "hzh2 = " + hzh2);
}

console.log("呼叫hzh1[0]():");
hzh1[0]();
console.log("呼叫hzh1[1]():");
hzh1[1]();
console.log("呼叫hzh1[2]():");
hzh1[2]();
console.log("呼叫hzh1[3]():");
hzh1[3]();
console.log("呼叫hzh1[4]():");
hzh1[4]();
console.log("呼叫hzh1[5]():");
hzh1[5]();
console.log("呼叫hzh1[6]():"); // 這個數書上的輸出
hzh1[6]();
console.log("呼叫hzh1[7]():");
hzh1[7]();
console.log("呼叫hzh1[8]():");
hzh1[8]();
console.log("呼叫hzh1[9]():");
hzh1[9]();






[Running] node "e:\HMV\Babel\hzh.js"
第 0 輪輸出時:hzh2 = 0
第 1 輪輸出時:hzh2 = 1
第 2 輪輸出時:hzh2 = 2
第 3 輪輸出時:hzh2 = 3
第 4 輪輸出時:hzh2 = 4
第 5 輪輸出時:hzh2 = 5
第 6 輪輸出時:hzh2 = 6
第 7 輪輸出時:hzh2 = 7
第 8 輪輸出時:hzh2 = 8
第 9 輪輸出時:hzh2 = 9
呼叫hzh1[0]():
hzh2 = 0
呼叫hzh1[1]():
hzh2 = 1
呼叫hzh1[2]():
hzh2 = 2
呼叫hzh1[3]():
hzh2 = 3
呼叫hzh1[4]():
hzh2 = 4
呼叫hzh1[5]():
hzh2 = 5
呼叫hzh1[6]():
hzh2 = 6
呼叫hzh1[7]():
hzh2 = 7
呼叫hzh1[8]():
hzh2 = 8
呼叫hzh1[9]():
hzh2 = 9

[Done] exited with code=0 in 0.172 seconds

上面的程式碼中,變數 hzh2 是 let 宣告的,當前的 hzh2 只在本輪迴圈有效。所以每一次迴圈的 hzh2 其實都是一個新的變數,於是最後輸出的是 6 。大家可能會問,如果每一輪迴圈的變數 i 都是重新宣告的,那它怎麼知道上一輪迴圈的值從而計算出本輪迴圈的值呢?這是因為 JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數 hzh2 時,就在上一輪迴圈的基礎上進行計算。

另外,for 迴圈還有一個特別之處,就是設定迴圈變數的那部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。

// hzh.js

console.log("看看是輸出數字還是hzh?")
for (let hzh = 0; hzh < 3; hzh++) {
    let hzh = 'hzh';
    console.log(hzh);
}
[Running] node "e:\HMV\Babel\hzh.js"
看看是輸出數字還是hzh?
hzh
hzh
hzh

[Done] exited with code=0 in 0.173 seconds

正確執行以上程式碼將輸出3次 hzh。這表明函式內部的變數 hzh 與迴圈變數 hzh 不在同一個作用域,而是有各自單獨的作用域。

2.1.2 不存在變數提升

var 命令會發生“變數提升”現象,即變數可以在宣告之前使用,值為 undefined 。這種現象多少是有些奇怪的,按照一般的邏輯,變數應該在宣告語句之後才可以使用。

為了糾正這種現象,let 命令改變了語法行為,它所宣告的變數一定要在聲明後使用,否則便會報錯。

// hzh.js

// var 的情況
console.log("hzh1 = " + hzh1); // 輸出 undefined
var hzh1 = 2;
console.log("");
// let 的情況
console.log("hzh2 = " + hzh2); // 報錯 ReferenceError
let hzh2 = 2;
[Running] node "e:\HMV\Babel\hzh.js"
hzh1 = undefined

e:\HMV\Babel\hzh.js:8
console.log("hzh2 = " + hzh2); // 報錯 ReferenceError
                        ^

ReferenceError: Cannot access 'hzh2' before initialization
    at Object.<anonymous> (e:\HMV\Babel\hzh.js:8:25)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

[Done] exited with code=1 in 0.174 seconds

在以上程式碼中,變數 hzh1 用 var 命令宣告會發生變數提升,即指令碼開始執行時,變數 hzh1 便已經存在,但是沒有值,所以會輸出 undefined 。變數 hzh2 用 let 命令宣告則不會發生變數提升。這表示在宣告它之前,變數 hzh2 是不存在的,這時如果用到它,就會丟擲一個錯誤。

2.1.3 暫時性死區

只要塊級作用域記憶體在 let 命令,它所宣告的變數就“繫結”(binding)這個區域,不再受外部的影響。

// hzh.js

var hzh = 123;

if(true) {
    hzh = 'huangzihan'; // ReferenceError
    let hzh;
}
[Running] node "e:\HMV\Babel\hzh.js"
e:\HMV\Babel\hzh.js:6
    hzh = 'huangzihan'; // ReferenceError
        ^

ReferenceError: Cannot access 'hzh' before initialization
    at Object.<anonymous> (e:\HMV\Babel\hzh.js:6:9)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

[Done] exited with code=1 in 0.197 seconds

上面的程式碼中存在全域性變數 hzh,但是塊級作用域內 let 又聲明瞭一個區域性變數 hzh,導致後者繫結這個塊級作用域,所以在 let 宣告變數前,對 hzh 賦值會報錯。

ES6 明確規定,如果區塊中存在 let 和 const 命令,則這個區塊對這些命令宣告的變數從一開始就形成封閉作用域。只要在宣告之前就使用這些變數,就會報錯。

總之,在程式碼塊內,使用 let 命令宣告變數之前,該變數都是不可用的。這在語法上稱為“暫時性死區”(temporal dead zone,簡稱TDZ)。

// hzh.js

if(true) {
    // TDZ 開始
    hzh = 'huangzihan'; // ReferenceError
    console.log("hzh = " + hzh);

    let hzh; // TDZ 結束
    console.log("hzh = " + hzh); // undefined

    hzh = 123;
    console.log("hzh = " + hzh); //123
}
[Running] node "e:\HMV\Babel\hzh.js"
e:\HMV\Babel\hzh.js:5
    hzh = 'huangzihan'; // ReferenceError
        ^

ReferenceError: Cannot access 'hzh' before initialization
    at Object.<anonymous> (e:\HMV\Babel\hzh.js:5:9)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

[Done] exited with code=1 in 0.172 seconds

上面的程式碼中,在 let 命令宣告變數 hzh 之前,都屬於變數 hzh 的“死區”。

“暫時性死區”也意味著 typeof 不再是一個百分百安全的操作。


上面的程式碼中,變數x使用let命令宣告,所以在宣告之前都屬於
x的“死區”,只要用到該變數就會報錯。因此,typeof執行時就
會丟擲一個ReferenceError。
作為比較,如果一個變數根本沒有被宣告,使用typeof反而不會
報錯。

上面的程式碼中,undeclared_variable是一個不存在的變數名,結
果返回“undefined”。所以,在沒有let之前,typeof運算子是百分
之百安全的,永遠不會報錯。現在這一點不成立了。這樣的設
計是為了讓大家養成良好的程式設計習慣,變數一定要在宣告之後
使用,否則就會報錯。
有些“死區”比較隱蔽,不太容易發現。

上面的程式碼中,呼叫bar函式之所以報錯(某些實現可能不報錯
),是因為引數x的預設值等於另一個引數y,而此時y還沒有聲
明,屬於“死區”。如果y的預設值是x,就不會報錯,因為此時x
已宣告。

另外,下面的程式碼也會報錯,與var的行為不同。

以上程式碼報錯也是因為暫時性死區。使用let宣告變數時,只要
變數在還沒有宣告前使用,就會報錯。以上示例就屬於這種情
況,在變數x的宣告語句還沒有執行完成前就嘗試獲取x的值,
導致出現“x未定義”的錯誤。
ES6規定暫時性死區和let、const語句不出現變數提升,主要是
為了減少執行時錯誤,防止在變數宣告前就使用這個變數,從
而導致意料之外的行為。這樣的錯誤在ES5中是很常見的,現
在有了這種規定,避免此類錯誤就很容易了。
總之,暫時性死區的本質就是,只要進入當前作用域,所要使
用的變數就已經存在,但是不可獲取,只有等到宣告變數的那
一行程式碼出現,才可以獲取和使用該變數。

2.1.4 不允許重複宣告

let不允許在相同作用域內重複宣告同一個變數。

因此,不能在函式內部重新宣告引數。

2.2 塊級作用域

2.2.1 為什麼需要塊級作用域

ES5只有全域性作用域和函式作用域,沒有塊級作用域,這導致
很多場景不合理。
第一種場景,內層變數可能會覆蓋外層變數

以上程式碼的原意是,if程式碼塊的外部使用外層的tmp變數,內部
使用內層的tmp變數。但是,函式f執行後,輸出結果為
undefined,原因在於變數提升導致內層的tmp變數覆蓋了外層的
tmp變數。
第二種場景,用來計數的迴圈變數洩露為全域性變數

上面的程式碼中,變數i只用來控制迴圈,但是迴圈結束後,它並
沒有消失,而是洩露成了全域性變數

2.2.2 ES6的塊級作用域

let實際上為JavaScript新增了塊級作用域。

上面的函式有兩個程式碼塊,都聲明瞭變數 n,執行後輸出 5。
這表示外層程式碼塊不受內層程式碼塊的影響。如果使用var定義變
量n,最後輸出的值就是10。
ES6允許塊級作用域的任意巢狀。

上面的程式碼使用了一個5層的塊級作用域。外層作用域無法讀
取內層作用域的變數。
內層作用域可以定義外層作用域的同名變數。

塊級作用域的出現,實際上使得獲得廣泛應用的立即執行匿名
函式(IIFE)不再必要了。

2.2.3 塊級作用域與函式宣告

函式能不能在塊級作用域之中宣告?這是一個相當令人困惑的
問題。
ES5規定,函式只能在頂層作用域和函式作用域之中宣告,不
能在塊級作用域宣告。

上面兩種函式宣告在ES5中都是非法的。
但是,瀏覽器沒有遵守這個規定,為了相容以前的舊程式碼,還
是支援在塊級作用域之中宣告函式,因此上面兩種情況實際上
都能執行,並不會報錯。
ES6 引入了塊級作用域,明確允許在塊級作用域之中宣告函式
。ES6 規定,在塊級作用域之中,函式宣告語句的行為類似於
let,在塊級作用域之外不可引用。

以上程式碼在ES5中執行會得到 I am inside!,因為在if內宣告的
函式f會被提升到函式頭部,實際執行的程式碼如下。

而在ES6中執行就完全不一樣了,理論上會得到 I am
outside!。因為塊級作用域內宣告的函式類似於let,對作用域
之外沒有影響。但是,如果真的在ES6瀏覽器中執行上面的代
碼,是會報錯的,這是為什麼呢?
原來,如果改變了塊級作用域內宣告的函式的處理規則,顯然
會對舊程式碼產生很大影響。為了減輕因此產生的不相容問題
,ES6 在附錄
B(www.ecma-international.org/ecma-262/6.0/index.html#sec-blo
ck-level-function-declarations-web-legacy-compatibility-semantics)
中規定,瀏覽器的實現可以不遵守上面的規定,而有自己的行
為方式
(stackoverflow.com/questions/31419897/what-are-the-precise-sem
antics-of-block-level-functions-in-es6),具體如下。
· 允許在塊級作用域內宣告函式。
· 函式宣告類似於var,即會提升到全域性作用域或函式作用域的
頭部。
· 同時,函式宣告還會提升到所在的塊級作用域的頭部。
注意!
上面3條規則只對ES6的瀏覽器實現有效,其他環境的實現不用
遵守,仍舊將塊級作用域的函式聲明當作let處理即可。
根據這3條規則,在瀏覽器的ES6環境中,塊級作用域內宣告函
數的行為類似於var宣告變數。

上面的程式碼在符合ES6的瀏覽器中都會報錯,因為實際執行的
是以下程式碼。

考慮到環境導致的行為差異太大,應該避免在塊級作用域內聲
明函式。如果確實需要,也應該寫成函式表示式的形式,而不
是函式宣告語句。

另外,還有一個需要注意的地方。ES6 的塊級作用域允許宣告
函式的規則只在使用大括號的情況下成立,如果沒有使用大括
號,就會報錯。

2.2.4 do表示式

本質上,塊級作用域是一個語句,將多個操作封裝在一起,沒
有返回值。

上面的程式碼中,塊級作用域將兩個語句封裝在一起。但是,在
塊級作用域以外,沒有辦法得到t的值,因為塊級作用域不返回
值,除非t是全域性變數。
現在有一個提案
(wiki.ecmascript.org/doku.php?id=strawman:do
expressions),使得塊級作用域可以變為表示式,即可以返回
值,辦法就是在塊級作用域之前加上do,使它變為do表示式。

上面的程式碼中,變數x會得到整個塊級作用域的返回值。

2.3 const命令

const宣告一個只讀的常量。一旦宣告,常量的值就不能改變。
上面的程式碼表明改變常量的值會報錯。
const宣告的常量不得改變值。這意味著,const一旦宣告常量,
就必須立即初始化,不能留到以後賦值。
上面的程式碼表示,對於const而言,只宣告不賦值就會報錯。
const的作用域與let命令相同:只在宣告所在的塊級作用域內有
效。

2.3.1 基本用法

上面的程式碼在常量MAX宣告之前就被呼叫,結果報錯。
使用const宣告常量也與let一樣,不可重複宣告。

2.3.2 本質

const實際上保證的並不是變數的值不得改動,而是變數指向的
那個記憶體地址不得改動。對於簡單型別的資料(數值、字串
、布林值)而言,值就儲存在變數指向的記憶體地址中,因此等
同於常量。但對於複合型別的資料(主要是物件和陣列)而言
,變數指向的記憶體地址儲存的只是一個指標,const只能保證這
個指標是固定的,至於它指向的資料結構是不是可變的,這完
全不能控制。因此,將一個物件宣告為常量時必須非常小心

上面的程式碼中,常量foo儲存的是一個地址,這個地址指向一個
物件。不可變的只是這個地址,即不能把foo指向另一個地址,
但物件本身是可變的,所以依然可以為其新增新屬性。
來看另一個例子。

上面的程式碼中,常量a是一個數組,這個陣列本身是可寫的,
但是如果將另一個數組賦值給a,就會報錯。
如果真的想將物件凍結,應該使用Object.freeze方法。
上面的程式碼中,常量foo指向一個凍結的物件,所以新增新屬性
時不起作用,嚴格模式時還會報錯。
除了將物件本身凍結,物件的屬性也應該凍結。下面是一個將
物件徹底凍結的函式。

2.3.3 ES6宣告變數的6種方法

ES5只有兩種宣告變數的方法:使用var命令和function命令。
ES6除了添加了let和const命令,後面的章節中還會介紹另外兩
種宣告變數的方法:使用import命令和class命令。所以,ES6一
共有6種宣告變數的方法。

2.4 頂層物件的屬性

頂層物件在瀏覽器環境中指的是window物件,在Node環境中指
的是global物件。在ES5中,頂層物件的屬性與全域性變數是等價
的。

上面的程式碼中,頂層物件的屬性賦值與全域性變數的賦值是同一
件事。
頂層物件的屬性與全域性變數相關,被認為是 JavaScript 語言中
最大的設計敗筆之一。這樣的設計帶來了幾個很大的問題:首
先,無法在編譯時就提示變數未宣告的錯誤,只有執行時才能
知道(因為全域性變數可能是頂層物件的屬性創造的,而屬性的
創造是動態的);其次,程式設計師很容易不知不覺地就建立全域性
變數(比如打字出錯);最後,頂層物件的屬性是到處都可以
讀寫的,這非常不利於模組化程式設計。另一方面,window 物件
有實體含義,指的是瀏覽器的視窗物件,這樣也是不合適的。
ES6為了改變這一點,一方面規定,為了保持相容性,var命令
和function命令宣告的全域性變數依舊是頂層物件的屬性;另一方
面規定,let命令、const命令、class命令宣告的全域性變數不屬於
頂層物件的屬性。也就是說,從ES6開始,全域性變數將逐步與
頂層物件的屬性隔離

2.5 global物件

ES5的頂層物件本身也是一個問題,因為它在各種實現中是不
統一的。
· 在瀏覽器中,頂層物件是window,但Node和Web Worker沒有
window。
· 在瀏覽器和Web Worker中,self也指向頂層物件,但是Node
沒有self。
· 在Node中,頂層物件是global,但其他環境都不支援。
同一段程式碼為了能夠在各種環境中都取到頂層物件,目前一般
是使用 this 變數,但是也有侷限性。
· 在全域性環境中,this會返回頂層物件。但是,在Node模組和
ES6模組中,this返回的是當前模組。
· 對於函式中的this,如果函式不是作為物件的方法執行,而是
單純作為函式執行,this會指向頂層物件。但是,嚴格模式下
,this會返回undefined。
· 不管是嚴格模式,還是普通模式,new Function(′return
this′)()總會返回全域性物件。但是,如果瀏覽器用了
CSP(Content Security Policy,內容安全政策),那麼eval、new
Function這些方法都可能無法使用。
綜上所述,很難找到一種方法可以在所有情況下都取到頂層對
象。以下是兩種勉強可以使用的方法

現在有一個提案(github.com/tc39/proposal-global),在語言標
準的層面引入 global 作為頂層物件。也就是說,在所有環境下
,global都是存在的,都可以拿到頂層物件。
墊片庫 system.global(github.com/ljharb/System.global)模擬了
這個提案,可以在所有環境下拿到global。

上面的程式碼可以保證,在各種環境中global物件都是存在的。
上面的程式碼將頂層物件放入變數global中。