深入理解 Babel - 微內核架構與 ECMAScript 標準化
隨著瀏覽器版本的持續更新,瀏覽器對JavaScript的支持越來越強大,Babel的重要性顯得較低了。但Babel的設計思路、背后依賴的ECMAScript標準化思想仍然值得借鑒。
本文涉及的Babel版本主要是V7.16及以下,截至發文時,Babel最新發布的版本是V7.25.6,未出現大版本更新,近2年也進入了穩定迭代期,本文的分析思路基本適用目前的Babel設計。
一、Babel簡介
Babel是什么
Babel是JavaScript轉譯器,通過Babel,開發者可以自由使用下一代ECMAScript 語法。高版本ECMAScript語法將被轉譯為低版本語法,以便順利運行在各類環境,如低版本瀏覽器、低版本 Node.js 等。
Babel 是轉譯器,不是編譯器。下面是轉譯和編譯的區別:
編譯,一般指將一種語言轉換為另一種語法和抽象程度等都不同的語言,常見的比如 gcc 編譯器。
轉譯,一般指將一種語言轉換為不同版本或者抽象程度相同的語言,比如 Babel 可以把 ECMAScript 6 語法轉譯為 ECMAScript 5語法。
利用 Babel,開發者可以使用 ECMAScript 的各種新特性進行開發,同時花極少的精力關注瀏覽器或其他JS運行環境對新特性的支持。甚至,開發者可以根據自身需要,創造屬于自己的 JavaScript 語法。
Babel在轉譯的時候,會對源碼進行以下處理: 語法轉譯(Syntax)和添加API Polyfill。
圖片
- 語法(Syntax)部分Babel 支持識別高版本語法,并通過插件將源碼從高版本語法轉譯為低版本語法,如:
箭頭函數 () => {} 轉為普通函數 function() {}。
const / let 轉譯為var
- API Polyfill有些運行時相關的 API,語法轉譯無法解決它們對低版本瀏覽器等環境的兼容性問題,因此 Babel 通過與 core-js 等工具的配合,實現 API 部分對目標環境(通常是低版本瀏覽器等)的兼容。例如[1, 2, 3].include、Promise等 API,Babel 在處理時,如果目標環節可能不支持原生的 include / Promise 的話,Babel 會在轉譯結果中嵌入 include / Promise 的自定義實現。有多種方式可以使用 Babel,如: 命令行(babel-cli、babel-node)、瀏覽器(babel-standalone)、API 調用(babel-core)、webpack loader(babel-loader)等。
轉譯過程
和多數轉譯器相同,Babel 運行的生命周期主要是 3 個階段: 解析、轉換、代碼生成。
這個過程涉及抽象語法樹:
抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。
AST 是樹形對象,以結構化的形式表示編程語言的語法結構,樹上的每個節點都表示源代碼中的一種結構。
圖片
源碼字符串需要經轉譯器生成 AST,轉譯器有很多種,不同轉譯器,生成的AST對象格式細節可能有差異,但共同點為: 都是樹形對象、該樹形對象描述了節點特征、各節點之間的關系(兄弟、父子等)。
以下是 Babel 生命周期的三個過程:
- 解析(Parsing): Code1 ==> 抽象語法樹1解析過程包括 2 個環節: 詞法解析、語法解析,最終生成抽象語法樹。詞法解析階段,代碼字符串被解析為 token 令牌數組,數組項是一個對象,包括: 代碼字符碎片的值、位置、類型等信息。token 數組是平鋪式的數組,沒有嵌套的結構信息,它是為語法解析階段做準備的。語法解析階段,token 令牌數組被解析為結構化的抽放語法樹對象(AST)。babel-parser 完成該階段的主要功能。
圖片
- 轉換(Transformation): AST1 ==> AST2Babel 生成 AST 后,會對 AST 進行遍歷,遍歷過程中,各類插件對原 AST 對象進行增刪改查等操作,AST 結構被修改。
圖片
- 代碼生成(Generation): AST2 ==> Code2Babel 將修改后的 AST 對象轉目標代碼字符串。babel-generator 完成該階段的主要功能。
圖片
二、Babel微內核架構
微內核架構
Babel 采用微內核架構,其內核保留核心功能,其余功能利用外部工具和插件機制實現,也體現了"開放-封閉"的設計原則。
圖片
除了微內核設計架構,Babel 的模塊設計也可以做如下分類:
圖片
轉譯模塊
轉譯模塊位于 Babel 微內核架構的"微內核"部分,該部分主要負責代碼轉譯,也就是上面提到的"解析-轉換-代碼生成"過程。
該模塊主要包括: babel-parser、babel-traverse、babel-generator。
- babel-parser負責將代碼字符串轉為 AST 對象。有 2 點值得一提:
babel-parser 本身并不會對 AST 做轉換操作,只是負責解析出 AST。AST 轉換部分交由各類 plugins 和 presets 處理。
babel-parser 內置了對 ESNext/TypeScript/JSX/Flow 最新版本語法的支持,但很多默認是不開啟的,目前沒有開放插件機制擴展新語法。
- babel-traverse在轉譯過程中,babel-traverse 負責遍歷 AST 節點,并根據配置的 plugins/presets,在遍歷過程中,對各個 AST 節點進行增刪改查的操作。AST 是一個樹形對象,遍歷 AST 對象的過程也是一個深度優先遍歷的過程。
- babel-generator負責將 AST 節點,轉為代碼字符串,同時也可以生成 source-map。
插件模塊
插件模塊包括 plugins、presets。
- plugins豐富的插件,幫助 Babel 成為一個非常成功的轉譯工具。對 AST 的遍歷、轉換是 Babel 轉譯的核心功能,但 Babel 本身并不參與該過程,將這些功能作為插件引入到運行時。具體來說,babel-core 作為核心工具,不提供對 AST 的修改邏輯,通過調用各類插件,實現對 AST 的修改。Babel的插件分為語法插件和轉換插件。
語法插件
值得注意的是,babel-parser 負責將 JavaScript 代碼解析出抽象語法樹(AST),它支持全面識別 ESNext/TypeScript/JSX/Flow 等語法,目前由 Babel 團隊開發維護,不支持插件化。
Babel 插件生態中的語法插件,其功能就是作為"開關",配置是否開啟 babel-parser 的某些語法轉譯功能。
語法插件在 Babel 源碼中,以 babel-plugin-syntax 開頭。
舉個例子:
babel-plugin-syntax-decorators負責開啟 babel-parser 對裝飾器的語法支持。
babel-plugin-syntax-dynamic-import負責開啟 babel-parser 對 import 語句的語法支持。
babel-plugin-syntax-jsx負責開啟 babel-parser 對 jsx 語法的支持。
- 轉換插件轉換插件就是社區里常說的 Babel 插件,負責轉換 AST 節點。在介紹babel-traverse時提到,它負責遍歷AST對象,每個AST節點會被訪問到,等待轉換,轉換的部分,由"轉換插件"負責。轉換插件會提供一個叫做"Visitor"的對象,該對象的 Key 為節點名稱,Value 部分提供進入該節點時、離開該節點時的回調函數,在回調函數里,可以對該節點進行一系列操作。"Visitor" 又稱為 "訪問者"。
// plugin 提供 visitor,在 visitor 中對 AST 節點操作
const visitor = {
Program: {
enter() {},
exit() {},
},
CallExpression: {
enter() {},
exit() {},
},
NumberLiteral: {
enter() {},
exit() {},
}
};
traverse(ast, visitor);
轉換插件在Babel源碼中,以 babel-plugin-transform 開頭。
舉個例子:
babel-plugin-transform-strict-mode
該插件攔截 Program 節點,也就是整個程序的根節點,添加 "use strict"指令。
visitor 節點值為函數時,是 enter 回調的快捷方式。
{
name: "transform-strict-mode",
visitor: {
Program(path) {
const { node } = path;
for (const directive of node.directives) {
if (directive.value.value === "use strict") return;
}
path.unshiftContainer(
"directives",
t.directive(t.directiveLiteral("use strict")),
);
},
},
};
}
- babel-plugin-transform-object-assign
該插件負責攔截函數調用表達式節點 CallExpression,將 Object.assign 轉為 extends 寫法。
{
name: "transform-object-assign",
visitor: {
CallExpression(path, file) {
if (path.get("callee").matchesPattern("Object.assign")) {
path.node.callee = file.addHelper("extends");
}
},
},
}
- PresetsBabel 插件的功能是細粒度的,大部分插件承擔了單一功能。而在實際開發過程中,往往需要支持對各類語法的支持。此時,有兩種做法:
需要支持哪些特性,就分別引入支持該特性的插件
直接引入一個插件集合,涵蓋所需的各類插件功能
很顯然,第一種做法是相對麻煩的。針對第二種做法,Babel提供了插件集 preset。
preset 在 Babel 源碼中,以 babel-preset 開頭。
例如,Babel 已經提供了幾種常用的 preset 供開發者使用:
babel-preset-env
babel-preset-react
babel-preset-flow
babel-preset-typescript
- 插件運行順序Babel 配置項中,plugins 和 presets 均以數組的形式配置,執行時有先后順序。
plugins 在 presets之前運行
plugins 按照數組正序執行
presets 按照數組倒序執行
圖片
工具模塊
工具模塊提供 Babel 相關模塊所需的各類工具,以下一一簡要介紹:
- babel-corebabel-core 對外提供了 Babel 多項功能的 API,如轉譯文件、轉譯代碼、創建/獲取配置等,在 Babel 官方文檔介紹的比較詳細,不再贅述。值得注意的是,轉譯類的 API 均提供了同步和異步版本,如 transformSync/transfomAsync、parseSync/parseAsync。
- babel-cliBabel 的命令行工具,可以直接轉譯文件夾/文件,它也提供了很多配置項做其他工作,官方文檔介紹的比較詳細,感興趣的同學可以去 Babel 官網查看詳細配置。
- babel-standaloneBabel 對外服務的很多包是基于 node 環境下使用的,babel-standalone 提供了瀏覽器下轉譯的方案。babel-standalone 內置了所有 Babel 插件,所以體積還是比較大的,而且在瀏覽器端轉譯需要時間,比較適合開發、學習使用,不適合在生產環境使用。舉個例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test babel-standalone</title>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const arr = [1, 2, 3];
console.log(...arr);
</script>
</head>
<body></body>
</html>
在瀏覽器運行該 html,可以看到,頁面結構變成了:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test babel-standalone</title>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const arr = [1, 2, 3];
console.log(...arr);
</script>
<script>
"use strict";
var _console;
var arr = [1, 2, 3];
(_console = console).log.apply(_console, arr); //# sourceMappingURL=data:application/json;charset=utf-8;base64...
</script>
</head>
<body></body>
</html>
- babel-node
提供在命令行執行高級語法的環境。
例如:
// index.js 里可以使用高級語法
babel-node -e index.js
index.js 文件以及被其引入的其他文件均可以使用高級語法了。和 babel-cli 不同的是,babel-cli 只負責轉換,不在 node 運行時執行;babel-node 會在 node 運行時執行轉換,不適合生產環境使用。
- babel-register
在源文件中,引入babel-register,如 index.js:
index.js
require('babel-register');
require('./run');
run.js
import fs from 'fs';
console.log(fs);
執行 node index 時,run.js 就不需要被轉碼了。
babel-register 通過攔截 node require 方法,為 node 運行時引入了 Babel 的轉譯能力。
- babel-loader
babel-loader 是利用 babel-core 的 API 封裝的 webpack loader,用于 webpack 構建過程。
- babel-types
babel-types 是一個非常強大的工具集合,它集成了節點校驗、增刪改查等功能,是 Babel 核心模塊開發、插件開發等場景下不可或缺的工具。
例如:
const t = require('@babel/types');
const binaryExpression = t.binaryExpression('+', t.numericLiteral(1), t.numericLiteral(2));
- babel-template
模板引擎,負責將代碼字符串轉為 AST 節點對象。
import { smart as template } from '@babel/template';
import generate from '@babel/generator';
import * as t from '@babel/types';
const buildRequire = template( var %%importName%% = require(%%source%%); );
const ast = buildRequire({
importName: t.identifier('myModule'),
source: t.stringLiteral("my-module"),
});
const code = generate(ast).code
console.log(code)
運行結果:
var myModule = require("my-module");
- babel-code-frame
負責打印出錯的代碼位置,例如:
const { codeFrameColumns } = require('@babel/code-frame');
const testCode = `
class Run {
constructor() {}
}
`;
const location = {
start: {
line: 2,
column: 2,
}
};
const result = codeFrameColumns(testCode, location);
console.log(result);
1 | class Run {
> 2 | constructor() {}
| ^
3 | }
4 |
- babel-highlight
向控制臺輸出有顏色的代碼片段。該工具可以識別 JavaScript 中的操作符號、標識符、保留字等類型的詞法單元,并在終端環境下顯示不同的顏色。
運行時相關模塊
Babel 配合其插件可以對靜態代碼進行轉譯,但有一些遺漏點:
- 對于運行時涉及的一些高版本 API,并沒有提供兼容目標環境的 Polyfill。
- 轉譯產物代碼可能有些臃腫。
為此,運行時模塊(runtime)關注的是轉譯產物的運行時環境,對運行時提供 API polyfill、代碼優化等,該模塊涉及幾個子包:
- babel-preset-env
- babel-plugin-transform-runtime
- babel-runtime
- babel-runtime-corejs2/3
- core-js
接下來以案例解釋 runtime 模塊的作用。
源碼文件 index.js 的內容:
const a = 1; // const 為語法部分
class Base {} // class 為語法部分
new Promise() // Promise 為 API 部分
這段源碼包含了語法和 API 部分:
- const、class 為語法部分
- Promise 為 API 部分
如果希望這段源碼轉為 ES5 版本,使構建產物可以運行在不支持 ES6 和 Promise 的環境里,該怎么做呢?
用 babel 命令行執行轉譯,其中源文件為 index.js,轉譯產物文件為 index-compiled.js。
npx babel index.js --out-file index-compiled.js
需要配置.babelrc 幫助 Babel 完成語法和 API 部分的轉譯:
.babelrc:
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
簡要解釋下該配置的原理:
- babel-preset-env 可以完成語法部分轉譯,即 const 轉譯為var但構建產物中,有些輔助代碼如 _classCallCheck 是以硬編碼的形式直接寫入轉譯產物的:
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var a = 1;
var Base = function Base() {
_classCallCheck(this, Base);
};
new Promise();
這樣的后果就是構建產物比較臃腫。
- babel-plugin-transform-runtime 可以將上述 _classCallCheck 置于通用包中,以引用的形式寫入轉譯產物:
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var a = 1;
var Base = function Base() {
(0, _classCallCheck2["default"])(this, Base);
};
new Promise();
- babel-plugin-transform-runtime 的配置參數 corejs 用于轉譯 API 部分,如 Promsie
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var a = 1;
var Base = function Base() {
(0, _classCallCheck2["default"])(this, Base);
};
new _promise"default";
Babel 轉譯過程的運行時優化是一個繁瑣的過程,為此將單獨用一章講解運行時優化,感興趣的同學可以直接閱讀 "Babel Runtime" 章節詳細了解。
三、標準化
Babel 生態涉及的一些標準化組織。無論是 JavaScript、HTML、DOM、URL 等領域,均需要統一的標準,才能在不同的運行環境下有統一的表現。Babel 轉譯也需要遵循這些標準,包括 ECMAScript、web標準等。
ECMAScript
JavaScript誕生
1995 年,JavaScript 的第一個版本發布。用時間線的方式描述 JavaScript 的誕生過程會更清晰:
圖片
ECMAScript發布
1996 年,微軟模仿 JavaScript 實現了 JScript 并內置在 IE3.0,隨后,Netscape 公司著手推動 JavaScript 標準化。
這里涉及幾個組織:
- Ecma International
Ecma International 是一家國際性會員制度的信息和電信標準組織。1994年之前,名為歐洲計算機制造商協會(European Computer Manufacturers Association)。因為計算機的國際化,組織的標準牽涉到很多其他國家,因此組織決定改名表明其國際性。
Ecma International 的任務包括與有關組織合作開發通信技術和消費電子標準、鼓勵準確的標準落實、和標準文件與相關技術報告的出版。
Ecma International 負責多個國際標準的制定:
- CD-ROM 格式(之后被國際標準化組織批準為ISO 9660)
- C# 語言規范
- C++/CLI 語言規范
- 通用語言架構(CLI)
- Eiffel 語言
- 電子產品環境化設計要素
- Universal 3D 標準
- OOXML
- Dart 語言規范
- ECMAScript 語言規范(以 JavaScript 為基礎)ECMA-262其中就包括 JavaScript 標準語言規范 ECMAScript。cma International 擁有 ECMAScript 的商標。
- ECMA TC39
「TC39」全稱「Technical Committee 39」譯為「第 39 號技術委員會」,是 Ecma International 組織架構中的一部分。
TC39 負責迭代和發展 ECMAScript,它的成員由各個主流瀏覽器廠商的代表組成,通常每年召開約 6 次會議來討論未決提案的進展情況,會議的每一項決議必須得到大部分人的贊同,并且沒有人強烈反對才可以通過。
TC39 負責:
維護和更新 ECMAScript 語言標準
識別、開發、維護 ECMAScript 的擴展功能庫
開發測試套件
為 ISO/IEC JTC 1 提供標準
評估和考慮新添加的標準
- ISO
國際標準化組織(英語: International Organization for Standardization,簡稱: ISO)成立于 1947 年 2 月 23 日,制定全世界工商業國際標準的國際標準建立機構。
ISO 的國際標準以數字表示,例如: "ISO 11180:1993" 的 "11180" 是標準號碼,而 "1993" 是出版年份。
ISO/IEC JTC 1 是國際標準化組織和國際電工委員會聯合技術委員會。其目的是開發、維護和促進信息技術以及信息和通信技術領域的標準。JTC 1 負責了許多關鍵的 IT 標準,從 MPEG 視頻格式到 C++ 編程語言。
圖片
- ECMAScript 發展過程中的關鍵節點
圖片
ECMAScript 各版本
ECMAScript 經歷了多個版本,每個版本有自己的特點,簡單列舉如下:
圖片
圖片
ECMAScript 迭代過程
一個 ECMAScript 標準的制作過程,包含了 Stage 0 到 Stage 4 共 5 個階段,每個階段提交至下一階段都需要 TC39 審批通過。
圖片
圖片
特性進入 Stage-4 后,才有可能被加入標準中,還需要 ECMA General Assembly 表決通過才能進入下一次的 ECMAScript 標準中。
如何閱讀 ECMAScript
ECMAScript 文檔結構
ECMAScript 的規格,可以在 ECMA 國際標準組織的官方網站免費下載和在線閱讀。
查看ECMAScript 不同版本的地址:https://ecma-international.org/publications-and-standards/standards/ecma-262/。
截至 2023年底,已發布的版本如下:
- ECMA-262 5.1 edition, June 2011
(https://262.ecma-international.org/5.1/index.html)
- ECMA-262, 6th edition, June 2015
(https://262.ecma-international.org/6.0/index.html)
- ECMA-262, 7th edition, June 2016
(https://262.ecma-international.org/7.0/index.html)
- ECMA-262, 8th edition, June 2017
(https://262.ecma-international.org/8.0/index.html)
- ECMA-262, 9th edition, June 2018
(https://262.ecma-international.org/9.0/index.html)
- ECMA-262, 10th edition, June 2019
(https://262.ecma-international.org/10.0/index.html)
- ECMA-262, 11th edition, June 2020
(https://262.ecma-international.org/11.0/index.html)
- ECMA-262, 12th edition, June 2021
(https://262.ecma-international.org/12.0/index.html)
- ECMA-262, 13th edition, June 2022
(https://262.ecma-international.org/13.0/index.html)
- ECMA-262, 14th edition, June 2023
(https://262.ecma-international.org/14.0/index.html)
每個版本有獨立的網址,格式為: https://262.ecma-international.org/{version}/,比如 ECMAScript 14.0 版本的網址為 https://262.ecma-international.org/14.0/。
從章節數量上,ECMAScript 6.0、ECMAScript 7.0 有 26 章,之后的版本有 27-29 章,雖然章節數量不同,規格章節的分布是保持一定規律的,以 ECMAScript 11.0 版本為例:
- Introduction: 介紹部分
該章節簡要描述了: JavaScript 和 ECMAScript 的發展歷史、不同 ECMAScript 規格的主要更新內容。
- 第 1 章到第 3 章: 描述了規格文件本身,而非語言
第 1 章用一句話描述了該規格的描述范圍。
第 2 章描述了基于規格的"實現"的一致性要求:
"實現"必須支持規格中描述的所有類型、值、對象、屬性、函數以及程序的語法和語義
"實現"必須按照 Unicode 標準和 ISO/IEC 10646 的最新版本處理文本輸入
"實現"如果提供了應用程序編程接口(API),那么該 API 需要適應不同的人文語言和國家差異,且必須實現最新版本的 ECMA-402 所定義的與本規范相兼容的接口
"實現"可以支持該規格中沒有提及的類型、值、對象、屬性、函數、正則表達式語法以及其他編程寫法
"實現"不能實現該規格中禁止的寫法
- 第 3 章描述了該規格的一些參考資料:
- ISO/IEC 10646
- ECMA-402
- EMCA-404 JSON 數據交換格式規范
- 第 4 章: 對這門語言總體設計的描述。
- 第 5 章到第 8 章: 語言宏觀層面的描述。
- 第 6 章介紹數據類型。
- 第 7 章介紹語言內部用到的抽象操作。
- 第 8 章介紹代碼如何運行。
- 第 9 章到第 27 章: 介紹具體的語法。
一般而言,除非寫編譯器,開發者無需閱讀 ECMAScript 的規格,規格的內容非常多,如無必要也無需通讀。只是在遇到一些奇怪的問題時,閱讀官方規格,是最穩妥的辦法。
通過閱讀規格解決一些問題
(以ECMAScript 11.0為例)
- 識別關鍵詞和保留字,并高亮
Babel 工具集中的 babel-highlight,可以實現在終端對代碼塊中的目標字符單元顯示不同的顏色。這里需要識別不同字符單元的類型,如關鍵字、保留字、標識符、數字、字符串等。
標識符、數字、字符串都很好理解和識別,但哪些字符應該被識別為關鍵字、保留字,而不是標識符呢?
此時可以閱讀 ECMAScript 規格了,ECMAScript 11.0 規格的 11.6.2 節介紹了關鍵詞和保留字列表。
關鍵詞(keywords)關鍵詞首先是標識符,同時有語義,包括 if、while、async、await...,個別關鍵詞是可以用作變量名的。
保留字(reserved word)保留字首先是標識符,但不能用作變量名。部分關鍵詞是保留字,但部分不是: if、while是保留字;await只有在 async方法和模塊中才是保留字;async不是保留字,它可以作為普通的變量名使用。
保留字列表
await
break
case
catch
class
const
continue
debugger
default
delete
do
else
enum
export
extends
false
finally
for
function
if
import
in
instanceof
new
null
returns
uper
switch
this
throw
true
try
typeof
var
void
while
with
yield
讀完上述規格,也就知道哪些字符單元是需要識別為保留字與關鍵詞,并高亮的了。
- 識別全局對象,并高亮
繼續使用 babel-highlight 實現代碼塊中的全局對象高亮,那么,我們需要知道哪些是規格中描述的全局變量。
規格的 18 章介紹了全局對象,通過該章的描述,可以知道:
全局屬性全局屬性有: globalThis、Infinity、NaN、undefined。
全局方法
全局方法有: eval(x)、isFinite、isNaN、parseFloat、parseInt、decodeURIComponent、encodeURIComponent 等。
- 全局構造函數
全局的構造函數有:
Array
ArrayBuffer
BigInt
BigInt64Array
BigUnit64Array
Boolean
DataView
Date
Error
EvalError
Float32Array
Float64Array
Function
Int8Array
Int16Array
Int32Array
Map
Number
Object
Promise
Proxy
RangeError
ReferenceError
RegExp
Set
SharedArrayBuffer
String
Symbol
SyntaxError
TypeError
Uint8Array
Uint8ClampedArray
Uint16Array
Uint32Array
URIError
WeakMap
WeakSet
其他的全局屬性Atomics、JSON、Math、Reflect。很顯然,當字符單元的名稱是上述名稱中的一員時,我們可以對其進行高亮處理了(若上下文中無重新定義的同名變量)。
- 自定義 Error
babel-loader 自身維護了私有的 LoaderError 對象,該對象繼承自原生 Error 類,并且訂制了部分實例屬性。代碼如下:
class LoaderError extends Error {
constructor(err) {
super();
const { name, message, codeFrame, hideStack } = format(err);
this.name = "BabelLoaderError";
this.message = ${name ? ${name}: ` : ""}${message}\n\n${codeFrame}\n`;
this.hideStack = hideStack;
Error.captureStackTrace(this, this.constructor);
}
}
可以看到,babel-loader 自定義了錯誤實例的 name、message、hideStack 屬性,那么,問題是,原生的 Error 類有哪些屬性和方法,哪些是開發者可以自定義的呢?
規格的 19.5 章節,詳細介紹了 Error 的各類規范:
Error 作為函數被調用時(Error(...)),表現和 new Error(...) 一致,均會創建并返回 Error 的新實例
Error 可以被繼承,比如通過 extends 的方式,子類必須提供 constructor 方法,且該方法內必須提供 super() 調用
Error 構造函數必須有 prototype 屬性
Error.prototype 屬性需有以下屬性:
Error.prototype.constructor: 指向構造函數
Error.prototype.message: 描述錯誤信息,默認是空字符串
Error.prototype.name: 描述錯誤名稱,默認值是 Error
從 LoaderError 的源碼可以看到,LoaderError 做了以下幾件事情:
- LoaderError 繼承自 Error
- 實例自定義了 name、message 屬性,明確 babel-loader 的信息
- 實例自定義的 hideStack 屬性是非標準屬性,用于 babel-loader 內部
web標準
是在解決 API Polyfil 的時候,Babel 配合使用的 core-js 除了提供 ECMAScript 標準下的 JavaScript API 實現,也提供了 DOM/URL 等實現。而 DOM/URL 所屬的 web 標準,由 W3C/WHATWG 制定。
圖片
經過多年發展,WHATWG 和 W3C 目前是合作關系,其中,WHATWG 維護 HTML 和 DOM 標準,W3C 使用 WHATWG 存儲庫中的 HTML 和 DOM 標準描述,W3C 在 HTML 部分的工作集中在 XHTML/XML 上。
圖片
四、總結
本文介紹了 Babel 的概述/微內核架構/ECMAScript標準化方面的設計思想和部分實現原理。
上述內容其實在很早之前就已經成型了,筆者也查看了Babel最近的迭代內容,發現并沒有太大的變化。至于代碼轉譯領域,目前是Babel還是其他工具哪個更有優勢,不在本文的討論范圍內。除了比較社區哪些工具更好而言,“Babel的設計思路、其與標準規范是怎么配合的”等也是很值得學習的地方,也是這篇文章的產生背景。