跳到主要內容
黯羽輕揚每天積累一點點

let 和 const_ES6 筆記 11

免費2016-09-10#JS#es6 let#js const#js let关键字#es6 const关键字

let 是更完美的 var

寫在前面

let x = x => x + 1;似乎成了 ES6 的起手式,如同 return arr.map(fx).filter(isValid).reduce(accumulator) 作為 ES5 的亮黑色一樣,會點 ES6,還用 var 起手會被人嫌棄的

不過,ES6 中最不疼不癢的特性應該就是 letconst 了,如果已經習慣了 var 的小脾氣的話

一。為什麼需要 let 和 const?

因為 var 有一些小脾氣,他們認為是函數作用域引發的「bug」,比如這個小小的詭異問題:

var x = 4;
(function() {
    console.log(x);     // undefined
    console.log(x + 1); // NaN
    var x = 1;
    // 因為 var 的提升特性,以上代碼等價於
    // var x;
    // console.log(x);
    // console.log(x + 1);
    // x = 1;
})();

這個叫 Hosting(提升),被強行扣上了黑鍋:

誰讓你提升的,弄出來一串詭異的 undefined、NaN,都怪你

其實如果不特意節省變量名的話,很難遇到這個問題

而另一個問題所有 JS 玩家都遇到過,如下:

(function() {
    var arr = [1, 2, 3];
    for (var i = 0; i < arr.length; i++) {
        setTimeout(function() {
            // 因為 50ms 後外部循環結束了,i === arr.length = 3
            console.log(arr[i]);    // undefined x 3
        }, 50);
        // 修復
        // (function(i) {
        //     setTimeout(function() {
        //         console.log(arr[i]);    // 1 2 3
        //     });
        // })(i);
    }
})();

閉包持有的是外部作用域訪問權限,而不是變量的值,50ms 後去訪問 i,拿到的當然是 3,這就是閉包的特性,其它函數式語言的閉包也是這樣子,他們又說:

誰讓你不合常理,循環體執行時的狀態你怎麼不給我存著,害我取到一堆 undefined

JS 無力辯解,心想自己確實有些地方做的不對:

  • 全域作用域中 var 聲明的變量會成為 global 對象的屬性

  • 沒有塊級作用域,辛苦大家用了 20 年 IIFE(明明應該是由一對花括號來搞定的事情)

於是就有了 letconst

二。let 的特點

1.let 聲明的變量有塊級作用域

沒錯,20 年後,JS 也有塊級作用域了

for (let i = 0; i < 3; i++) {
    //...
}
console.log(i); // Uncaught ReferenceError: i is not defined

那麼就有了一個問題,創建「塊」最簡單的方式是什麼?如果還是 IIFE,那又有什麼區別呢?答案見後文,因為涉及 JS 語法小細節,不在此展開

2.let 也有提升特性

直接把第一個示例代碼中的關鍵 var 換成 let 試試:

var y = 4;
(function() {
    console.log(y);     // Uncaught ReferenceError: y is not defined
    console.log(y + 1);
    let y = 1;
    // let 也有提升特性,以上代碼不完全等價於
    // let y;
    // console.log(y);     // undefined
    // console.log(y + 1); // NaN
    // y = 1;
})();

這次直接報錯了,外層的 y = 4 被屏蔽了,說明 let y 確實提升了一個塊級變量 y,報錯則是因為執行到 let 行才會加載變量定義(第 6 個特點)

因為 TDZ(見後文)的存在,未被註釋部分並不完全等價於被註釋掉的代碼(上面會報錯,而下面不報錯)

3.異常會在當前行拋出

let 有助於定位錯誤,除 NaN 之外異常會在當前行拋出,比如 undefined,把 NaN 排除在外是因為:

let a;
console.log(a + 1); // NaN === undefined + 1

JS 的弱類型機制不認為 NaN 算異常

其它異常會在當前行拋出,對比可見:

// let 當前行報錯
(() => {
    x++;    // Uncaught ReferenceError: x is not defined
    [1, 2, 3][x][0];
    let x = 1;
})();
// var 當前行不報錯
(() => {
    x++;
    [1, 2, 3][x][0];    // Uncaught TypeError: Cannot read property '0' of undefined
    var x = 1;
})();

明明是 x++ 時候就跑偏了,只到引發其它錯誤時才報錯,let 成功避免了這種情況

P.S. 上面的 (() => {/* 新版 IIFE */})(); 只是為了隔離影響,便於測試,let + class + ES6 模組就是為了剔除 IIFE,合理的 ES6 代碼中不應該出現僅用於隔離一塊作用域的 IIFE

4.let 聲明的全域變量不是全域對象的屬性

let b = 2;
console.log(window.b);  // undefined

不是說不需要變量命名空間了,script 標籤並沒有隔離作用域的效果,window 上的自定義屬性少了,全域變量的問題仍在,至於配合 ES6 模組作用域,這似乎是非常遙遠的事情

P.S. 雖然 webpack 等構建工具支持 ES6 模組,但只有瀏覽器支持這種模組作用域才能解決全域變量的問題,到時候或許真的就不需要命名空間了

P.S.V8 幾個月前就號稱 100% 支持 ES6 了,但 ES6 模組一直不支持,可能也不打算支持,因為 ES6 模組機制不適合瀏覽器環境,原因以後再細說

5.let 聲明的循環變量每次迭代都會重新綁定

也就是說循環體中的閉包保留了循環變量的值的副本,如下:

(function() {
    var arr = [1, 2, 3];
    for (let i = 0; i < arr.length; i++) {
        // 閉包保留了循環變量的值的副本
        setTimeout(function() {
            console.log(arr[i]);    // 1 2 3
        }, 50);
    }
})();

大家希望保存循環體執行時狀態,那就依大家的意思,JS 妥協了,但只退了一小步,僅僅對循環變量做了點 hack,閉包的大原則不能亂(持有外部作用域的訪問權)

注意:循環變量的意思是,適用於現有的三循環方式 for-offor-in 以及傳統的用分號分隔的類 C 循環

6.執行到 let 行才會加載變量定義

在這之前使用該變量報錯 ReferenceError這段時間變量在作用域中,但尚未加載,位於 TDZ(Temporal Dead Zone)中

let 是故意的,這樣既兼容 Hosting,同時還能報錯

7.let 變量作用域是整塊有效,而不是從聲明處開始到塊結尾有效

與 C 語言不同,算是塊級 Hosting,有一個很貼切的描述:

JavaScript 中 var 聲明的作用域像是 Photoshop 中的油漆桶工具,從聲明處開始向前後兩個方向擴散,直到觸及函數邊界才停止

let 的 Hosting 方式與 var 沒太大區別(只是邊界變成了塊邊界),都是這種雙向擴散的

8.重定義 let 變量會報錯 SyntaxError

會在詞法解析階段報錯,而不是運行時報錯,而且 SyntaxError 無法被 try-catch 捕獲

var 的「容錯性」很強,如下:

var x = 2;
var x;
var x = x++;

嗯,x 還是 2,第二句被忽略了,第三句 var 忽略掉,賦值執行了,怎麼寫都不報錯

let x = 2;
let x;  // Uncaught SyntaxError: Identifier 'x' has already been declared

這樣的話,以後面試題都簡單多了:)

9.class 聲明和 let 一樣,同名類會報錯 SyntaxError

class 出廠時就和 let 簽了合作條款,遵循 let 式聲明規則:

class A {}
class A {}  // Uncaught SyntaxError: Identifier 'A' has already been declared

三。const 的特點

const 與 let 類似,但 const 變量只讀

特點:

  • 修改 const 變量應該報錯 SyntaxError,但 Chrome47 操作無效但不報錯

  • const 聲明必須同時賦值,否則報錯,但 Chrome47 不報錯,值為 undefined

注意:這兩個約束在 Chrome51 中已經有了,現在會報錯(不知道是哪次的更新,話說這些 ES6 筆記是 16 年 1 月份的事情,不小心目擊了規範的約束力)

示例如下:

// 嘗試修改 const 變量
const PI = Math.PI;
PI = 3; // Uncaught TypeError: Assignment to constant variable.
PI++;   // 同上
console.log(PI);    // 註釋掉上兩句會輸出 3.141592653589793

// 嘗試聲明時不賦值
const UNDEF;    // 詞法檢查階段報錯
                // Uncaught SyntaxError: Missing initializer in const declaration
console.log(UNDEF); // 前面報錯了,到不了這

四。創建塊最簡單的方式

ES6 有塊級作用域了,意味著 IIFE 隔離作用域將成為歷史,那麼替代品是什麼?

{
    let tip = '這是我的領地';
};  //!!! 千萬千萬注意這個不起眼的分號
console.log(tip);   // Uncaught ReferenceError: tip is not defined

{}; 比 IIFE 清爽多了,等等,末尾的分號是什麼東西,有用嗎?Java 裡的代碼塊明顯不需要分號吧

注意:這個不起眼的分號是必不可少的,去掉就會報錯,因為 {} 會被當作對象字面量解析,引發語法錯誤,Java 確實不需要這個分號,因為沒有對象字面量沒有歧義,詞法解析器不會懵

JS 中的花括號

其實 JS 中有 4 中花括號,分別是:

// 1.對象字面量
{
    a: 1,
    b: 2
}
// 2.複合語句(一組代碼,單語句可以省略花括號)
if (true) {
    console.log(1);
    console.log(2);
}
// 3.作為語法結構(花括號是語法結構,不能省略)
try {}
catch (ex) {}
// 4.label(也是代碼分組,用來支持 break、continue 的跨層跳轉)
label: {}

塊和對象字面量有歧義,都是

{
    //...
}

JS 會把這個東西當作表達式來解析,因此檢查對象字面量語法,不對就報錯。如果告訴 JS 這個東西應該當作塊來解析,歧義自然就沒了,兩種方法:

// 1.分號強制語句(和逗號強制表達式一樣)
{/* 我是一個塊語句 */};
// 2.複合語句
{{/* 我是一個複合語句 */}}

所以另一種稍麻煩的創建塊的方式就是:

// 一種可愛的方式
{{
    let tip = '這是我的領地';
}}
console.log(tip);   // Uncaught ReferenceError: tip is not defined

當然故意用 label 來創建塊也是可以的,反正 label 一般沒什麼用,如下:

block: {
    let tip = '這是我的領地';
}
console.log(tip);   // Uncaught ReferenceError: tip is not defined

數數就有 3 種方式,可能還有更多待發現的,關於 JS 語法的更多信息請查看《JavaScript 語言精髓與編程實踐》

五。總結

let 是更完美的 var

let... 作為 ES6 的起手式也沒錯,但 let 相關的東西不比 var 少,用好 let 也像用好 var 一樣不容易

此外,let 雖然是 var 的替代品,但並不意味著可以對著老代碼做一遍全文替換,let 限制更多,「容錯性」自然不如 var。但無論怎樣,該過去的都將過去,var 終會消失,所以,嘗試接受 let

參考資料

  • 《JavaScript 語言精髓與編程實踐》:非常不錯的一本書,如果有耐心看完的話

  • 《ES6 in Depth》:InfoQ 中文站提供的免費電子書

評論

暫無評論,快來發表你的看法吧

提交評論