新系列導讀
學習程式語言是一件持之以恆的事情,從學會簡單的語法就能寫出程式,到理解型別和設計模式,再到考慮程式碼的組織架構。誰不是從這樣一點點深入和積累的呢?入門總是輕鬆又令人愉悅的,但隨著知識點越來越多學習的曲線卻驟然陡峭。但隨著對語言的深入理解,再回頭來重新審閱基本的知識,又會有柳暗花明又一村的豁然感,「啊,原來是這樣的」那種感覺。
這個 「3分鐘系列」 將利用 babel 編譯工具,來學習分析 es6+ 的部分特性。通過編譯後的 es5 程式碼,我們可以從中瞭解到 es6+ 特性的實現細節,更好的掌握新特性的適用性。
本文大量使用了阮一峰「 ECMAScript 6 入門」和「你不知道的 JavaScript」書中的程式碼。
cutting line
環境和配置
// @babel/core: 7.2.2
// @babel/preset-env: 7.2.3
// .babelrc
{
"presets": [
["@babel/preset-env", {
"ignoreBrowserslistConfig": true
}]
]
}
複製程式碼
cutting line
塊級作用域
ES5 只有全域性作用域和函式作用域,沒有塊級作用域,帶來了很多不合理的場景:
- 內層變數可能會覆蓋外層變數。
for
迴圈中的計數變數會洩露為全域性變數。
在 ES5 中為了建立一個塊級作用域,除了普通的函式宣告外,就是立即執行函式表示式了(IIFE)。
// es5
var a = 2;
(function IIFE () {
var a = 3;
console.log(a); // 3
})();
console.log(a); // 2
複製程式碼
cutting line
ES6 中引入了塊級作用域,當在花括號中存在 let
或者 const
時,花括號內為塊級作用域:
- 外層作用域無法讀取內層
let
或const
所宣告的變數。 - 內層
let
或const
所宣告的變數名可以和外層相同。 - 立即執行函式表示式不再必要了。
// ------ 原始碼區 ------
var x = 1;
let y = 1;
if (true) {
var x = 2;
let y = 2;
}
console.log(x); // 2
console.log(y); // 1
// ------ 編譯區 ------
"use strict";
var x = 1;
var y = 1;
if (true) {
var x = 2;
var _y = 2;
}
console.log(x); // 2
console.log(y); // 1
複製程式碼
特性:由於 let
使花括號提升為塊級作用域,使得即使宣告瞭相同的變數名 y
也互不干擾。
Babel:為了實現此效果,Babel 重新命名了塊級作用域內 let
宣告的變數名。
cutting line
for 迴圈
// ------ 原始碼區 ------
let a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6]();
// ------ 編譯區 ------
"use strict";
var a = [];
var _loop = function _loop(i) {
a[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 10; i++) {
_loop(i);
}
a[6]();
複製程式碼
特性:當用 var
宣告變數 i
時,a[6]()
輸出的是 10
,因為迴圈體沒有快取變數 i
。
Babel:當用 let
宣告時,Babel 建立了一個閉包 _loop
來快取變數。
cutting line
cutting line
// ------ 原始碼區 ------
let i = 1;
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// ------ 編譯區 ------
"use strict";
var i = 1;
for (var _i = 0; _i < 3; _i++) {
var _i2 = 'abc';
console.log(_i2);
}
複製程式碼
特性:for
迴圈中,初始化變數的部分和迴圈體內分別是兩個作用域。
Babel:Babel 重新命名了迴圈體內的變數 i
。
cutting line
let
ES6 新增了 let
命令,用來宣告變數。
let
擁有如下特性:
- 僅在其作用域內有效。
- 不存在變數提升。
- 產生暫時性死區。
- 不允許重複宣告。
僅在其作用域內有效
// ------ 原始碼區 ------
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
// ------ 編譯區 ------
"use strict";
{
var _a = 10;
var b = 1;
}
a; // ReferenceError: a is not defined.
b; // 1
複製程式碼
特性:let
所宣告的變數只會在其作用域內有效,作用域外呼叫該變數會報錯。
Babel:為了用 ES5 實現相同的特性,Babel 重新命名了 let
宣告的變數名,使得作用域內外的變數名不同。
cutting line
不存在變數提升
// ------ 原始碼區 ------
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;
// let 的情況
console.log(bar); // 報錯ReferenceError
let bar = 2;
// ------ 編譯區 ------
"use strict";
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2; // let 的情況
console.log(bar); // 報錯ReferenceError
var bar = 2;
複製程式碼
特性:let
必須先宣告再使用,這種語法行為糾正了 var
變數提升的現象。
Babel:Babel 在此處並沒有做特殊的處理。
重要提示:
let
在編譯後沒有新增異常提示,Babel 在變數提升細節上處理不佳,你的程式碼執行結果可能會和你預想中的有差異。養成良好的程式碼習慣,有助於避免此坑。
cutting line
暫時性死區
縮寫為「TDZ」(temporal dead zone)。
當區塊中存在 let
或 const
命令,這個區塊對這些命令宣告的變數就形成了封閉區域,凡是在宣告前就使用這些變數,就會報錯。
// ------ 原始碼區 ------
var tmp = 123;
{
tmp = 'abc'; // ReferenceError
let tmp;
}
// ------ 編譯區 ------
"use strict";
var tmp = 123;
{
_tmp = 'abc'; // ReferenceError
var _tmp;
}
複製程式碼
特性:上面的原始碼區中,期望給外部的 tmp
賦值 abc
。但由於在區塊中宣告瞭同名變數,所以此時 tmp
變數被內部佔用。
Babel:Babel 很好的處理了這個特性,將區塊內的 tmp
變數更名為 _tmp
以區分。但是,仍然會存在變數提升的問題。
cutting line
不允許重複宣告
// ------ 原始碼區 ------
function foo() {
let a = 10;
let a = 1;
}
// ------ 編譯區 ------
// 編譯報錯 Duplicate declaration "a"
複製程式碼
特性:let
不允許在相同作用域內,重複宣告同一個變數。
Babel:當重複宣告同一個變數時,編譯無法通過。
cutting line
const
const
宣告一個只讀的常量。
const
有如下特性:
- 變數一旦宣告,其值(記憶體地址)就不可改變。
- 宣告時必須賦值。
// ------ 原始碼區 ------
const a = 1;
// ------ 編譯區 ------
var a = 1;
複製程式碼
特性:最普通的使用方式。
Babel:如果上下文沒有違背規範,則會直接用 var
來宣告。
cutting line
變數一旦宣告,其值(記憶體地址)就不可改變。
// ------ 原始碼區 ------
const a = 0;
a = 1;
// ------ 編譯區 ------
"use strict";
function _readOnlyError(name) { throw new Error("\"" + name + "\" is read-only"); }
var a = 0;
a = (_readOnlyError("a"), 1);
複製程式碼
特性:一旦 const
宣告瞭一個變數後嘗試再次賦值,會報異常。
Babel:Babel 檢測到變數被在此賦值,主動插入了一個報錯,並終止程式執行。
cutting line
宣告時必須賦值
// ------ 原始碼區 ------
const a;
// ------ 編譯區 ------
// 編譯報錯 Unexpected token
複製程式碼
cutting line
總結
Babel 在處理 let
和 const
的大部分特性時都不錯,但是在 變數先宣告後使用 的細節上處理不佳。需要我們保持良好的變數宣告習慣。
cutting line
相容性表
目前 86% 左右的瀏覽器都原生支援 let
和 const
。
cutting line
後續
當我文章寫到此處,我仍然疑惑為什麼 Babel 為什麼會沒有正確編譯暫時性死區的特性,留下這樣的問題。
直到我找到了 @babel/plugin-transform-block-scoping
外掛。
原來想要 Babel 編譯時正確實現該特性,需要引入這個外掛並開啟配置。
// .babelrc
{
"presets": [
["@babel/preset-env", {
"ignoreBrowserslistConfig": true
}]
],
"plugins": [
["@babel/plugin-transform-block-scoping", {
"tdz": true
}]
]
}
複製程式碼
cutting line
此時 Babel 會為程式碼插入一個異常。
// ------ 原始碼區 ------
i;
let i = 1;
// ------ 編譯區 ------
"use strict";
(function () {
throw new ReferenceError("i is not defined - temporal dead zone");
})();
var i = 1;
複製程式碼
但是文件中也說了,這個外掛沒有覆蓋所有邊界情況,也應該小心使用。
Temporal Dead Zone · Issue #826 · babel/website · GitHub
@babel/plugin-transform-block-scoping · Babel
cutting line