JavaScript語(yǔ)法樹(shù)與代碼轉(zhuǎn)化實(shí)踐
JavaScript 語(yǔ)法樹(shù)與代碼轉(zhuǎn)化實(shí)踐 歸納于筆者的現(xiàn)代 JavaScript 開(kāi)發(fā):語(yǔ)法基礎(chǔ)與實(shí)踐技巧系列文章中。本文引用的參考資料聲明于 JavaScript 學(xué)習(xí)與實(shí)踐資料索引中,特別需要聲明是部分代碼片引用自 Babel Handbook 開(kāi)源手冊(cè);也歡迎關(guān)注前端每周清單系列獲得一手資訊。
JavaScript 語(yǔ)法樹(shù)與代碼轉(zhuǎn)化
瀏覽器的兼容性問(wèn)題一直是前端項(xiàng)目開(kāi)發(fā)中的難點(diǎn)之一,往往客戶(hù)端瀏覽器的升級(jí)無(wú)法與語(yǔ)法特性的迭代保持一致;因此我們需要使用大量的墊片(Polyfill),以保證現(xiàn)代語(yǔ)法編寫(xiě)而成的 JavaScript 順利運(yùn)行在生產(chǎn)環(huán)境下的瀏覽器中,從而在可用性與代碼的可維護(hù)性之間達(dá)成較好的平衡。而以 Babel 為代表的語(yǔ)法轉(zhuǎn)化工具能夠幫我們自動(dòng)將 ES6 等現(xiàn)代 JavaScript 代碼轉(zhuǎn)化為可以運(yùn)行在舊版本瀏覽器中的 ES5 或其他同等的實(shí)現(xiàn);實(shí)際上,Babel 不僅僅是語(yǔ)法解析器,其更是擁有豐富插件的平臺(tái),稍加擴(kuò)展即可被應(yīng)用在前端監(jiān)控埋點(diǎn)、錯(cuò)誤日志收集等場(chǎng)景中。筆者也利用 Babel 以及 Babylon 為 swagger-decorator 實(shí)現(xiàn)了 flowToDecorator 函數(shù),其能夠從 Flow 文件中自動(dòng)提取出類(lèi)型信息并為類(lèi)屬性添加合適的注解。
Babel
自 Babel 6 之后,核心的 babel-core 僅暴露了部分核心接口,并使用 Babylon 進(jìn)行語(yǔ)法樹(shù)構(gòu)建,即上圖中的 Parse 與 Generate 步驟;實(shí)際的轉(zhuǎn)化步驟則是由配置的插件(Plugin)完成。而所謂的 Preset 則是一系列插件的合集,譬如 babel-preset-es2015 的源代碼中就定義了一系列的插件:
- return {
- plugins: [
- [transformES2015TemplateLiterals, { loose, spec }],
- transformES2015Literals,
- transformES2015FunctionName,
- [transformES201***rrowFunctions, { spec }],
- transformES2015BlockScopedFunctions,
- [transformES2015Classes, optsLoose],
- transformES2015ObjectSuper,
- ...
- modules === "commonjs" && [transformES2015ModulesCommonJS, optsLoose],
- modules === "systemjs" && [transformES2015ModulesSystemJS, optsLoose],
- modules === "amd" && [transformES2015ModulesAMD, optsLoose],
- modules === "umd" && [transformES2015ModulesUMD, optsLoose],
- [transformRegenerator, { async: false, asyncGenerators: false }]
- ].filter(Boolean) // filter out falsy values
- };
Babel 能夠?qū)⑤斎氲?JavaScript 代碼根據(jù)不同的配置將代碼進(jìn)行適當(dāng)?shù)剞D(zhuǎn)化,其主要步驟分為解析(Parse)、轉(zhuǎn)化(Transform)與生成(Generate):
在解析步驟中,Babel 分別使用詞法分析(Lexical Analysis)與語(yǔ)法分析(Syntactic Analysis)來(lái)將輸入的代碼轉(zhuǎn)化為抽象語(yǔ)法樹(shù);其中詞法分析步驟會(huì)將代碼轉(zhuǎn)化為令牌流,而語(yǔ)法分析步驟則是將令牌流轉(zhuǎn)化為語(yǔ)言?xún)?nèi)置的 AST 表示。
在轉(zhuǎn)化步驟中,Babel 會(huì)遍歷上一步生成的令牌流,根據(jù)配置對(duì)節(jié)點(diǎn)進(jìn)行添加、更新與移除等操作;Babel 本身并沒(méi)有進(jìn)行轉(zhuǎn)化操作,而是依賴(lài)于外置的插件進(jìn)行實(shí)際的轉(zhuǎn)化。
***的代碼生成則是將上一步中經(jīng)過(guò)轉(zhuǎn)化的抽象語(yǔ)法樹(shù)重新生成為代碼,并且同時(shí)創(chuàng)建 SourceMap;代碼生成相較于前兩步會(huì)簡(jiǎn)單很多,其核心思想在于深度優(yōu)先遍歷抽象語(yǔ)法樹(shù),然后生成對(duì)應(yīng)的代碼字符串。
抽象語(yǔ)法樹(shù)
抽象語(yǔ)法樹(shù)(Abstract Syntax Tree, AST)的作用在于牢牢抓住程序的脈絡(luò),從而方便編譯過(guò)程的后續(xù)環(huán)節(jié)(如代碼生成)對(duì)程序進(jìn)行解讀。AST 就是開(kāi)發(fā)者為語(yǔ)言量身定制的一套模型,基本上語(yǔ)言中的每種結(jié)構(gòu)都與一種 AST 對(duì)象相對(duì)應(yīng)。上文提及的解析步驟中的詞法分析步驟會(huì)將代碼轉(zhuǎn)化為所謂的令牌流,譬如對(duì)于代碼 n * n,其會(huì)被轉(zhuǎn)化為如下數(shù)組:
- [
- { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
- { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
- { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
- ...
- ]
其中每個(gè) type 是一系列描述該令牌屬性的集合:
- {
- type: {
- label: 'name',
- keyword: undefined,
- beforeExpr: false,
- startsExpr: true,
- rightAssociative: false,
- isLoop: false,
- isAssign: false,
- prefix: false,
- postfix: false,
- binop: null,
- updateContext: null
- },
- ...
- }
這里的每一個(gè) type 類(lèi)似于 AST 中的節(jié)點(diǎn)都擁有 start、end、loc 等屬性;在實(shí)際應(yīng)用中,譬如對(duì)于 ES6 中的箭頭函數(shù),我們可以通過(guò) babylon 解釋器生成如下的 AST 表示:
- {
- type: {
- label: 'name',
- keyword: undefined,
- beforeExpr: false,
- startsExpr: true,
- rightAssociative: false,
- isLoop: false,
- isAssign: false,
- prefix: false,
- postfix: false,
- binop: null,
- updateContext: null
- },
- ...
- }
我們可以使用 AST Explorer 這個(gè)工具進(jìn)行在線預(yù)覽與編輯;在上述的 AST 表示中,顧名思義,ArrowFunctionExpression 就表示該表達(dá)式為箭頭函數(shù)表達(dá)式。該函數(shù)擁有 foo 與 bar 這兩個(gè)參數(shù),參數(shù)所屬的 Identifiers 類(lèi)型是沒(méi)有任何子節(jié)點(diǎn)的變量名類(lèi)型;接下來(lái)我們發(fā)現(xiàn)加號(hào)運(yùn)算符被表示為了 BinaryExpression 類(lèi)型,并且其 operator 屬性設(shè)置為 +,而左右兩個(gè)參數(shù)分別掛載于 left 與 right 屬性下。在接下來(lái)的轉(zhuǎn)化步驟中,我們即是需要對(duì)這樣的抽象語(yǔ)法樹(shù)進(jìn)行轉(zhuǎn)換,該步驟主要由 Babel Preset 與 Plugin 控制;Babel 內(nèi)部提供了 babel-traverse 這個(gè)庫(kù)來(lái)輔助進(jìn)行 AST 遍歷,該庫(kù)還提供了一系列內(nèi)置的替換與操作接口。而經(jīng)過(guò)轉(zhuǎn)化之后的 AST 表示如下,在實(shí)際開(kāi)發(fā)中我們也常常首先對(duì)比轉(zhuǎn)化前后代碼的 AST 表示的不同,以了解應(yīng)該進(jìn)行怎樣的轉(zhuǎn)化操作:
- // AST shortened for clarity
- {
- "program": {
- "type": "Program",
- "body": [
- {
- "type": "ExpressionStatement",
- "expression": {
- "type": "Literal",
- "value": "use strict"
- }
- },
- {
- "type": "ExpressionStatement",
- "expression": {
- "type": "FunctionExpression",
- "async": false,
- "params": [
- {
- "type": "Identifier",
- "name": "foo"
- },
- {
- "type": "Identifier",
- "name": "bar"
- }
- ],
- "body": {
- "type": "BlockStatement",
- "body": [
- {
- "type": "ReturnStatement",
- "argument": {
- "type": "BinaryExpression",
- "left": {
- "type": "Identifier",
- "name": "foo"
- },
- "operator": "+",
- "right": {
- "type": "Identifier",
- "name": "bar"
- }
- }
- }
- ]
- },
- "parenthesizedExpression": true
- }
- }
- ]
- }
- }
自定義插件
Babel 支持以觀察者(Visitor)模式定義插件,我們可以在 visitor 中預(yù)設(shè)想要觀察的 Babel 結(jié)點(diǎn)類(lèi)型,然后進(jìn)行操作;譬如我們需要將下述箭頭函數(shù)源代碼轉(zhuǎn)化為 ES5 中的函數(shù)定義:
- // Source Code
- const func = (foo, bar) => foo + bar;
- // Transformed Code
- "use strict";
- const _func = function(_foo, _bar) {
- return _foo + _bar;
- };
在上一節(jié)中我們對(duì)比過(guò)轉(zhuǎn)化前后兩個(gè)函數(shù)語(yǔ)法樹(shù)的差異,這里我們就開(kāi)始定義轉(zhuǎn)化插件。首先每個(gè)插件都是以 babel 對(duì)象為輸入?yún)?shù),返回某個(gè)包含 visitor 的對(duì)象的函數(shù)。***我們需要調(diào)用 babel-core 提供的 transform 函數(shù)來(lái)注冊(cè)插件,并且指定需要轉(zhuǎn)化的源代碼或者源代碼文件:
- // plugin.js 文件,定義插件
- import type NodePath from "babel-traverse";
- export default function(babel) {
- const { types: t } = babel;
- return {
- name: "ast-transform", // not required
- visitor: {
- Identifier(path) {
- path.node.name = `_${path.node.name}`;
- },
- ArrowFunctionExpression(path: NodePath<BabelNodeArrowFunctionExpression>, state: Object) {
- // In some conversion cases, it may have already been converted to a function while this callback
- // was queued up.
- if (!path.isArrowFunctionExpression()) return;
- path.arrowFunctionToExpression({
- // While other utils may be fine inserting other arrows to make more transforms possible,
- // the arrow transform itself absolutely cannot insert new arrow functions.
- allowInsertArrow: false,
- specCompliant: !!state.opts.spec
- });
- }
- }
- };
- }
- // babel.js 使用插件
- var babel = require('babel-core');
- var plugin= require('./plugin');
- var out = babel.transform(src, {
- plugins: [plugin]
- });
常用轉(zhuǎn)化操作
遍歷
- 獲取子節(jié)點(diǎn)路徑
我們可以通過(guò) path.node.{property} 的方式來(lái)訪問(wèn) AST 中節(jié)點(diǎn)屬性:
- // the BinaryExpression AST node has properties: `left`, `right`, `operator`
- BinaryExpression(path) {
- path.node.left;
- path.node.right;
- path.node.operator;
- }
我們也可以使用某個(gè)路徑對(duì)象的 get 方法,通過(guò)傳入子路徑的字符串表示來(lái)訪問(wèn)某個(gè)屬性:
- BinaryExpression(path) {
- path.get('left');
- }
- Program(path) {
- path.get('body.0');
- }
- 判斷某個(gè)節(jié)點(diǎn)是否為指定類(lèi)型
內(nèi)置的 type 對(duì)象提供了許多可以直接用來(lái)判斷節(jié)點(diǎn)類(lèi)型的工具函數(shù):
- BinaryExpression(path) {
- if (t.isIdentifier(path.node.left)) {
- // ...
- }
- }
或者同時(shí)以淺比較來(lái)查看節(jié)點(diǎn)屬性:
- BinaryExpression(path) {
- if (t.isIdentifier(path.node.left, { name: "n" })) {
- // ...
- }
- }
- // 等價(jià)于
- BinaryExpression(path) {
- if (
- path.node.left != null &&
- path.node.left.type === "Identifier" &&
- path.node.left.name === "n"
- ) {
- // ...
- }
- }
- 判斷某個(gè)路徑對(duì)應(yīng)的節(jié)點(diǎn)是否為指定類(lèi)型
- BinaryExpression(path) {
- if (path.get('left').isIdentifier({ name: "n" })) {
- // ...
- }
- }
- 獲取指定路徑的父節(jié)點(diǎn)
有時(shí)候我們需要從某個(gè)指定節(jié)點(diǎn)開(kāi)始向上遍歷獲取某個(gè)父節(jié)點(diǎn),此時(shí)我們可以通過(guò)傳入檢測(cè)的回調(diào)來(lái)判斷:
- path.findParent((path) => path.isObjectExpression());
- // 獲取最近的函數(shù)聲明節(jié)點(diǎn)
- path.getFunctionParent();
- 獲取兄弟路徑
如果某個(gè)路徑存在于 Function 或者 Program 中的類(lèi)似列表的結(jié)構(gòu)中,那么其可能會(huì)包含兄弟路徑:
- // 源代碼
- var a = 1; // pathA, path.key = 0
- var b = 2; // pathB, path.key = 1
- var c = 3; // pathC, path.key = 2
- // 插件定義
- export default function({ types: t }) {
- return {
- visitor: {
- VariableDeclaration(path) {
- // if the current path is pathA
- path.inList // true
- path.listKey // "body"
- path.key // 0
- path.getSibling(0) // pathA
- path.getSibling(path.key + 1) // pathB
- path.container // [pathA, pathB, pathC]
- }
- }
- };
- }
- 停止遍歷
部分情況下插件需要停止遍歷,我們此時(shí)只需要在插件中添加 return 表達(dá)式:
- BinaryExpression(path) {
- if (path.node.operator !== '**') return;
- }
我們也可以指定忽略遍歷某個(gè)子路徑:
- outerPath.traverse({
- Function(innerPath) {
- innerPath.skip(); // if checking the children is irrelevant
- },
- ReferencedIdentifier(innerPath, state) {
- state.iife = true;
- innerPath.stop(); // if you want to save some state and then stop traversal, or deopt
- }
- });
操作
- 替換節(jié)點(diǎn)
- // 插件定義
- BinaryExpression(path) {
- path.replaceWith(
- t.binaryExpression("**", path.node.left, t.numberLiteral(2))
- );
- }
- // 代碼結(jié)果
- function square(n) {
- - return n * n;
- + return n ** 2;
- }
- 將某個(gè)節(jié)點(diǎn)替換為多個(gè)節(jié)點(diǎn)
- // 插件定義
- ReturnStatement(path) {
- path.replaceWithMultiple([
- t.expressionStatement(t.stringLiteral("Is this the real life?")),
- t.expressionStatement(t.stringLiteral("Is this just fantasy?")),
- t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head)")),
- ]);
- }
- // 代碼結(jié)果
- function square(n) {
- - return n * n;
- + "Is this the real life?";
- + "Is this just fantasy?";
- + "(Enjoy singing the rest of the song in your head)";
- }
- 將某個(gè)節(jié)點(diǎn)替換為源代碼字符串
- // 插件定義
- FunctionDeclaration(path) {
- path.replaceWithSourceString(`function add(a, b) {
- return a + b;
- }`);
- }
- // 代碼結(jié)果
- - function square(n) {
- - return n * n;
- + function add(a, b) {
- + return a + b;
- }
- 插入兄弟節(jié)點(diǎn)
- // 插件定義
- FunctionDeclaration(path) {
- path.insertBefore(t.expressionStatement(t.stringLiteral("Because I'm easy come, easy go.")));
- path.insertAfter(t.expressionStatement(t.stringLiteral("A little high, little low.")));
- }
- // 代碼結(jié)果
- + "Because I'm easy come, easy go.";
- function square(n) {
- return n * n;
- }
- + "A little high, little low.";
- 移除某個(gè)節(jié)點(diǎn)
- // 插件定義
- FunctionDeclaration(path) {
- path.remove();
- }
- // 代碼結(jié)果
- - function square(n) {
- - return n * n;
- - }
- 替換節(jié)點(diǎn)
- // 插件定義
- BinaryExpression(path) {
- path.parentPath.replaceWith(
- t.expressionStatement(t.stringLiteral("Anyway the wind blows, doesn't really matter to me, to me."))
- );
- }
- // 代碼結(jié)果
- function square(n) {
- - return n * n;
- + "Anyway the wind blows, doesn't really matter to me, to me.";
- }
- 移除某個(gè)父節(jié)點(diǎn)
- // 插件定義
- BinaryExpression(path) {
- path.parentPath.remove();
- }
- // 代碼結(jié)果
- function square(n) {
- - return n * n;
- }
作用域
- 判斷某個(gè)局部變量是否被綁定:
- FunctionDeclaration(path) {
- if (path.scope.hasBinding("n")) {
- // ...
- }
- }
- FunctionDeclaration(path) {
- if (path.scope.hasOwnBinding("n")) {
- // ...
- }
- }
- 創(chuàng)建 UID
- FunctionDeclaration(path) {
- path.scope.generateUidIdentifier("uid");
- // Node { type: "Identifier", name: "_uid" }
- path.scope.generateUidIdentifier("uid");
- // Node { type: "Identifier", name: "_uid2" }
- }
- 將某個(gè)變量聲明提取到副作用中
- // 插件定義
- FunctionDeclaration(path) {
- const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);
- path.remove();
- path.scope.parent.push({ id, init: path.node });
- }
- // 代碼結(jié)果
- - function square(n) {
- + var _square = function square(n) {
- return n * n;
- - }
- + };