JavaScript 常見(jiàn) AST 梳理
這是掘金小冊(cè)《babel 插件通關(guān)秘籍》的第三節(jié)(試讀章節(jié))。
babel 編譯的第一步是把源碼 parse 成抽象語(yǔ)法樹 AST (Abstract Syntax Tree),后續(xù)對(duì)這個(gè) AST 進(jìn)行轉(zhuǎn)換。(之所以叫抽象語(yǔ)法樹是因?yàn)槭÷缘袅嗽创a中的分隔符、注釋等內(nèi)容)
AST 也是有標(biāo)準(zhǔn)的,JS parser 的 AST 大多是 estree 標(biāo)準(zhǔn),從 SpiderMonkey 的 AST 標(biāo)準(zhǔn)擴(kuò)展而來(lái)。babel 的整個(gè)編譯流程都是圍繞 AST 來(lái)的,這一節(jié)我們來(lái)學(xué)一下 AST。
熟悉了 AST,也就是知道轉(zhuǎn)譯器和 JS 引擎是怎么理解代碼的,對(duì)深入掌握 Javascript 也有很大的好處。
常見(jiàn)的 AST 節(jié)點(diǎn)
AST 是對(duì)源碼的抽象,字面量、標(biāo)識(shí)符、表達(dá)式、語(yǔ)句、模塊語(yǔ)法、class 語(yǔ)法 都有各自的 AST。我們分別來(lái)了解一下:
Literal
Literal 是字面量的意思,比如 let name = 'guang'中,'guang'就是一個(gè)字符串字面量 StringLiteral,相應(yīng)的還有 數(shù)字字面量 NumericLiteral,布爾字面量 BooleanLiteral,字符串字面量 StringLiteral,正則表達(dá)式字面量 RegExpLiteral 等。

代碼中的字面量很多,babel 就是通過(guò) xxLiteral 來(lái)抽象這部分內(nèi)容的。
Identifier
Identifer 是標(biāo)識(shí)符的意思,變量名、屬性名、參數(shù)名等各種聲明和引用的名字,都是Identifer。我們知道,JS 中的標(biāo)識(shí)符只能包含字母或數(shù)字或下劃線(“_”)或美元符號(hào)(“$”),且不能以數(shù)字開(kāi)頭。這是 Identifier 的詞法特點(diǎn)。
來(lái)嘗試分析一下,下面這一段代碼里面有多少 Identifier 呢?
const name = 'guang';
function say(name) {
console.log(name);
}
const obj = {
name: 'guang'
}
答案是這些

Identifier 是變量和變量的引用,代碼中也是隨處可見(jiàn)。
Statement
statement 是語(yǔ)句,它是可以獨(dú)立執(zhí)行的單位,比如 break,continue,debugger,return 或者 if 語(yǔ)句、while 語(yǔ)句、for 語(yǔ)句,還有聲明語(yǔ)句,表達(dá)式語(yǔ)句等。我們寫的每一條可以獨(dú)立執(zhí)行的代碼,都是語(yǔ)句。
語(yǔ)句末尾一般會(huì)加一個(gè)分號(hào)分隔,或者用換行分隔。
下面這些我們經(jīng)常寫的代碼,每一行都是一個(gè) Statement:
break;
continue;
return;
debugger;
throw Error();
{}
try {} catch(e) {} finally{}
for (let key in obj) {}
for (let i = 0;i < 10;i ++) {}
while (true) {}
do {} while (true)
switch (v){case 1: break;default:;}
label: console.log();
with (a){}
他們對(duì)應(yīng)的 AST 節(jié)點(diǎn)如圖所示

語(yǔ)句是代碼執(zhí)行的最小單位,可以說(shuō),代碼是由語(yǔ)句(Statement)構(gòu)成的。
Declaration
聲明語(yǔ)句是一種特殊的語(yǔ)句,它執(zhí)行的邏輯是在作用域內(nèi)聲明一個(gè)變量、函數(shù)、class、import、export 等。
比如下面這些聲明語(yǔ)句:
const a = 1;
function b(){}
class C {}
import d from 'e';
export default e = 1;
export {e};
export * from 'e';
他們對(duì)應(yīng)的 AST 節(jié)點(diǎn)如圖:

聲明語(yǔ)句用于定義變量,變量聲明也是代碼中一個(gè)基礎(chǔ)的部分。
Expression
expression 是表達(dá)式,特點(diǎn)是執(zhí)行完以后有返回值,這是和語(yǔ)句 (statement) 的區(qū)別。
下面是一些常見(jiàn)的表達(dá)式
[1,2,3]
a = 1
1 + 2;
-1;
function(){};
() => {};
class{};
a;
this;
super;
a::b;
它們對(duì)應(yīng)的AST如圖:

細(xì)心的同學(xué)可能會(huì)問(wèn) identifier 和 super 怎么也是表達(dá)式呢?
其實(shí)有的節(jié)點(diǎn)可能會(huì)是多種類型,identifier、super 有返回值,符合表達(dá)式的特點(diǎn),所以也是 expression。
我們判斷 AST 節(jié)點(diǎn)是不是某種類型要看它是不是符合該種類型的特點(diǎn),比如語(yǔ)句的特點(diǎn)是能夠單獨(dú)執(zhí)行,表達(dá)式的特點(diǎn)是有返回值。
有的表達(dá)式可以單獨(dú)執(zhí)行,符合語(yǔ)句的特點(diǎn),所以也是語(yǔ)句,比如賦值表達(dá)式、數(shù)組表達(dá)式等,但有的表達(dá)式不能單獨(dú)執(zhí)行,需要和其他類型的節(jié)點(diǎn)組合在一起構(gòu)成語(yǔ)句。比如匿名函數(shù)表達(dá)式和匿名 class 表達(dá)式單獨(dú)執(zhí)行會(huì)報(bào)錯(cuò)
function(){};
class{}
需要和其他部分一起構(gòu)成一條語(yǔ)句,比如組成賦值語(yǔ)句
a = function() {}
b = class{}
表達(dá)式語(yǔ)句解析成 AST 的時(shí)候會(huì)包裹一層 ExpressionStatement 節(jié)點(diǎn),代表這個(gè)表達(dá)式是被當(dāng)成語(yǔ)句執(zhí)行的。

小結(jié):表達(dá)式的特點(diǎn)是有返回值,有的表達(dá)式可以獨(dú)立作為語(yǔ)句執(zhí)行,會(huì)包裹一層 ExpressionStatement。
Class
class 的語(yǔ)法比較特殊,有專門的 AST 節(jié)點(diǎn)來(lái)表示。
整個(gè) class 的內(nèi)容是 ClassBody,屬性是 ClassProperty,方法是ClassMethod(通過(guò) kind 屬性來(lái)區(qū)分是 constructor 還是 method)。
比如下面的代碼
class Guang extends Person{
name = 'guang';
constructor() {}
eat() {}
}
對(duì)應(yīng)的AST是這樣的

class 是 es next 的語(yǔ)法,babel 中有專門的 AST 來(lái)表示它的內(nèi)容。
Modules
es module 是語(yǔ)法級(jí)別的模塊規(guī)范,所以也有專門的 AST 節(jié)點(diǎn)。
import
import 有 3 種語(yǔ)法:
named import:
import {c, d} from 'c';
default import:
import a from 'a';
namespaced import:
import * as b from 'b';
這 3 種語(yǔ)法都對(duì)應(yīng) ImportDeclaration 節(jié)點(diǎn),但是 specifiers 屬性不同,分別對(duì)應(yīng) ImportSpicifier、ImportDefaultSpecifier、ImportNamespaceSpcifier。

圖中黃框標(biāo)出的是 specifier 部分??梢灾庇^的看出整體結(jié)構(gòu)相同,只是specifier 部分不同,所以 import 語(yǔ)法的 AST 的結(jié)構(gòu)是ImportDeclaration 包含著各種 import specifier。
export
export 也有3種語(yǔ)法:
named export:
export { b, d};
default export:
export default a;
all export:
export * from 'c';
分別對(duì)應(yīng) ExportNamedDeclaration、ExportDefaultDeclaration、ExportAllDeclaration 的節(jié)點(diǎn)
其中 ExportNamedDeclaration 才有 specifiers 屬性,其余兩種都沒(méi)有這部分(也比較好理解,export 不像 import 那樣結(jié)構(gòu)類似,這三種 export 語(yǔ)法結(jié)構(gòu)是不一樣的,所以不是都包含 specifier)。
比如這三種 export
export { b, d};
export default a;
export * from 'c';
對(duì)應(yīng)的 AST 節(jié)點(diǎn)為

import 和 export 是語(yǔ)法級(jí)別的模塊化實(shí)現(xiàn),也是經(jīng)常會(huì)操作的 AST。
Program & Directive
program 是代表整個(gè)程序的節(jié)點(diǎn),它有 body 屬性代表程序體,存放 statement 數(shù)組,就是具體執(zhí)行的語(yǔ)句的集合。還有 directives 屬性,存放Directive 節(jié)點(diǎn),比如"use strict" 這種指令會(huì)使用 Directive 節(jié)點(diǎn)表示。

Program 是包裹具體執(zhí)行語(yǔ)句的節(jié)點(diǎn),而 Directive 則是代碼中的指令部分。
File & Comment
babel 的 AST 最外層節(jié)點(diǎn)是 File,它有 program、comments、tokens 等屬性,分別存放 Program 程序體、注釋、token 等,是最外層節(jié)點(diǎn)。
注釋分為塊注釋和行內(nèi)注釋,對(duì)應(yīng) CommentBlock 和 CommentLine 節(jié)點(diǎn)。

上面 6 種就是常見(jiàn)的一些 AST 節(jié)點(diǎn)類型,babel 就是通過(guò)這些節(jié)點(diǎn)來(lái)抽象源碼中不同的部分。
AST 可視化查看工具
當(dāng)然,我們并不需要記什么內(nèi)容對(duì)應(yīng)什么 AST 節(jié)點(diǎn),可以通過(guò) axtexplorer.net 這個(gè)網(wǎng)站來(lái)直觀的查看。

這個(gè)網(wǎng)站可以查看代碼 parse 以后的結(jié)果,但是如果想查看全部的 AST 可以在babel parser 倉(cāng)庫(kù)里的 AST 文檔里查,或者直接去看 @babel/types 的 typescript 類型定義。
AST 的公共屬性
每種 AST 都有自己的屬性,但是它們也有一些公共屬性:
type:AST 節(jié)點(diǎn)的類型start、end、loc:start 和 end 代表該節(jié)點(diǎn)對(duì)應(yīng)的源碼字符串的開(kāi)始和結(jié)束下標(biāo),不區(qū)分行列。而 loc 屬性是一個(gè)對(duì)象,有 line 和 column 屬性分別記錄開(kāi)始和結(jié)束行列號(hào)。leadingComments、innerComments、trailingComments:表示開(kāi)始的注釋、中間的注釋、結(jié)尾的注釋,因?yàn)槊總€(gè) AST 節(jié)點(diǎn)中都可能存在注釋,而且可能在開(kāi)始、中間、結(jié)束這三種位置,通過(guò)這三個(gè)屬性來(lái)記錄和 Comment 的關(guān)聯(lián)。extra:記錄一些額外的信息,用于處理一些特殊情況。
總結(jié)
這一節(jié)我們學(xué)習(xí)了代碼中常見(jiàn)的語(yǔ)法在 babel 的 AST 中對(duì)應(yīng)的節(jié)點(diǎn)。
我們學(xué)習(xí)了:標(biāo)識(shí)符 Identifer、各種字面量 xxLiteral、各種語(yǔ)句 xxStatement,各種聲明語(yǔ)句 xxDeclaration,各種表達(dá)式 xxExpression,以及 Class、Modules、File、Program、Directive、Comment 這些 AST 節(jié)點(diǎn)。
了解了這些節(jié)點(diǎn),就能知道平時(shí)寫的代碼是怎么用 AST 表示的,當(dāng)然也不需要記,可以去文檔或一些工具網(wǎng)站 (astexpoler.net) 去查。
AST 節(jié)點(diǎn)可能同時(shí)有多種類型,確定一種 AST 節(jié)點(diǎn)是什么類型主要看它的特點(diǎn),比如 Statement 的特點(diǎn)是可以單獨(dú)執(zhí)行,Expression 的特點(diǎn)是有返回值,所以一些可以單獨(dú)執(zhí)行的 Expression 會(huì)包一層 ExpressionStatement 執(zhí)行。
不同 AST 節(jié)點(diǎn)有不同的屬性來(lái)存放各自對(duì)應(yīng)的源碼內(nèi)容,但是都有一些公共屬性如 type、xxComments、loc 等。
學(xué)會(huì)了 AST,就可以把對(duì)代碼的操作轉(zhuǎn)為對(duì) AST 的操作了。
