寫在前面
let x = x => x + 1;似乎成了 ES6 的起手式,如同 return arr.map(fx).filter(isValid).reduce(accumulator) 作為 ES5 的亮黑色一樣,會點 ES6,還用 var 起手會被人嫌棄的
不過,ES6 中最不疼不癢的特性應該就是 let 和 const 了,如果已經習慣了 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(明明應該是由一對花括號來搞定的事情)
於是就有了 let 和 const
二。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-of、for-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 中文站提供的免費電子書
暫無評論,快來發表你的看法吧