前端進階之 JS 抽象語法樹

PinesCheng發表於2019-03-02

文章同步於 Github/Blog

Babel 與 Babylon

Babel 是 JavaScript 編譯器 compiler,更確切地說是原始碼到原始碼的編譯器,通常也叫做 轉換編譯器(transpiler)。 意思是說你為 Babel 提供一些 JavaScript 程式碼,Babel 更改這些程式碼,然後返回給你新生成的程式碼。

Babel 是一個通用的多功能的 JavaScript 編譯器。此外它還擁有眾多模組可用於不同形式的靜態分析。

靜態分析是在不需要執行程式碼的前提下對程式碼進行分析的處理過程 (執行程式碼的同時進行程式碼分析即是動態分析)。 靜態分析的目的是多種多樣的, 它可用於語法檢查,編譯,程式碼高亮,程式碼轉換,優化,壓縮等等場景。

Babylon 是 Babel 的解析器 parser。最初是 從 Acorn 專案 fork 出來的。Acorn 非常快,易於使用,並且針對非標準特性(以及那些未來的標準特性) 設計了一個基於外掛的架構。

Babylon 已經移入 Babel mono-repo 更名為 babel-parser

baimage

首先,讓我們安裝它。

$ npm install --save babylon
複製程式碼

先從解析一個程式碼字串開始:

import * as babylon from "babylon";

const code = `function square(n) {
  return n * n;
}`;

babylon.parse(code);
// Node {
//   type: "File",
//   start: 0,
//   end: 38,
//   loc: SourceLocation {...},
//   program: Node {...},
//   comments: [],
//   tokens: [...]
// }
複製程式碼

我們還能像下面這樣傳遞選項給 parse()方法:

babylon.parse(code, {
  sourceType: "module", // default: "script"
  plugins: ["jsx"] // default: []
});
複製程式碼

sourceType 可以是 "module" 或者 "script",它表示 Babylon 應該用哪種模式來解析。 "module" 將會在嚴格模式下解析並且允許模組定義,"script" 則不會。

注意: sourceType 的預設值是 "script" 並且在發現 importexport 時產生錯誤。 使用 scourceType: "module" 來避免這些錯誤。

由於 Babylon 使用了基於外掛的架構,因此有一個 plugins 選項可以開關內建的外掛。 注意 Babylon 尚未對外部外掛開放此 API 介面,不排除未來會開放此API。

解析(Parse)

解析(Parse )步驟接收程式碼並輸出 抽象語法樹(AST)。 這個步驟分為兩個階段:**詞法分析(Lexical Analysis) **語法分析(Syntactic Analysis)

詞法分析

詞法分析階段把字串形式的程式碼轉換為 令牌(tokens) 流。

你可以把令牌看作是一個扁平的語法片段陣列:

n * n;
複製程式碼
[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]
複製程式碼

每一個 type 有一組屬性來描述該令牌:

{
  type: {
    label: `name`,
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}
複製程式碼

和 AST 節點一樣它們也有 startendloc 屬性。

語法分析

語法分析階段會把一個令牌流轉換成 抽象語法樹(AST) 的形式。 這個階段會使用令牌中的資訊把它們轉換成一個 AST 的表述結構,這樣更易於後續的操作。

這個處理過程中的每一步都涉及到建立或是操作抽象語法樹,亦稱 AST。

Babel 使用一個基於 ESTree 並修改過的 AST,它的核心說明文件可以在[這裡](https://github. com/babel/babel/blob/master/doc/ast/spec. md)找到。

function square(n) {
  return n * n;
}
複製程式碼

AST Explorer 可以讓你對 AST 節點有一個更好的感性認識。 這裡是上述程式碼的一個示例連結。

這個程式可以被表示成如下所示的 JavaScript Object(物件):

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}
複製程式碼

你會留意到 AST 的每一層都擁有相同的結構:

{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}
複製程式碼
{
  type: "Identifier",
  name: ...
}
複製程式碼
{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}
複製程式碼

注意:出於簡化的目的移除了某些屬性

這樣的每一層結構也被叫做 節點(Node)。 一個 AST 可以由單一的節點或是成百上千個節點構成。 它們組合在一起可以描述用於靜態分析的程式語法。

每一個節點都有如下所示的介面(Interface):

interface Node {
  type: string;
}
複製程式碼

字串形式的 type 欄位表示節點的型別(如: "FunctionDeclaration""Identifier",或 "BinaryExpression")。 每一種型別的節點定義了一些附加屬性用來進一步描述該節點型別。

Babel 還為每個節點額外生成了一些屬性,用於描述該節點在原始程式碼中的位置。

{
  type: ...,
  start: 0,
  end: 38,
  loc: {
    start: {
      line: 1,
      column: 0
    },
    end: {
      line: 3,
      column: 1
    }
  },
  ...
}
複製程式碼

每一個節點都會有 startendloc 這幾個屬性。

變數宣告

程式碼

let a  = `hello`
複製程式碼

AST

image

VariableDeclaration

變數宣告,kind 屬性表示是什麼型別的宣告,因為 ES6 引入了 const/let
declarations 表示宣告的多個描述,因為我們可以這樣:let a = 1, b = 2;

interface VariableDeclaration <: Declaration {
    type: "VariableDeclaration";
    declarations: [ VariableDeclarator ];
    kind: "var";
}
複製程式碼

VariableDeclarator

變數宣告的描述,id 表示變數名稱節點,init 表示初始值的表示式,可以為 null

interface VariableDeclarator <: Node {
    type: "VariableDeclarator";
    id: Pattern;
    init: Expression | null;
} 
複製程式碼

Identifier

識別符號,我覺得應該是這麼叫的,就是我們寫 JS 時自定義的名稱,如變數名,函式名,屬性名,都歸為識別符號。相應的介面是這樣的:

interface Identifier <: Expression, Pattern {
    type: "Identifier";
    name: string;
}
複製程式碼

一個識別符號可能是一個表示式,或者是解構的模式(ES6 中的解構語法)。我們等會會看到 ExpressionPattern 相關的內容的。

Literal

字面量,這裡不是指 [] 或者 {} 這些,而是本身語義就代表了一個值的字面量,如 1“hello”, true 這些,還有正規表示式(有一個擴充套件的 Node 來表示正規表示式),如 /d?/。我們看一下文件的定義:

interface Literal <: Expression {
    type: "Literal";
    value: string | boolean | null | number | RegExp;
}
複製程式碼

value 這裡即對應了字面量的值,我們可以看出字面量值的型別,字串,布林,數值,null 和正則。

二元運算表示式

程式碼

let a = 3+4
複製程式碼

AST

image

BinaryExpression

二元運算表示式節點,leftright 表示運算子左右的兩個表示式,operator 表示一個二元運算子。

interface BinaryExpression <: Expression {
    type: "BinaryExpression";
    operator: BinaryOperator;
    left: Expression;
    right: Expression;
}
複製程式碼

BinaryOperator

二元運算子,所有值如下:

enum BinaryOperator {
    "==" | "!=" | "===" | "!=="
         | "<" | "<=" | ">" | ">="
         | "<<" | ">>" | ">>>"
         | "+" | "-" | "*" | "/" | "%"
         | "|" | "^" | "&" | "in"
         | "instanceof"
}
複製程式碼

if 語句

程式碼

if(a === 0){
}
複製程式碼

AST

image

IfStatement

if 語句節點,很常見,會帶有三個屬性,test 屬性表示 if (...) 括號中的表示式。

consequent 屬性是表示條件為 true 時的執行語句,通常會是一個塊語句。

alternate 屬性則是用來表示 else 後跟隨的語句節點,通常也會是塊語句,但也可以又是一個 if 語句節點,即類似這樣的結構:
if (a) { //... } else if (b) { // ... }
alternate 當然也可以為 null

interface IfStatement <: Statement {
    type: "IfStatement";
    test: Expression;
    consequent: Statement;
    alternate: Statement | null;
}
複製程式碼

常見的 AST node types

常見的 AST node types 在 Babylon 中 定義如下:

Node objects

符合規範的解析出來的 AST 節點用 Node 物件來標識,Node 物件應該符合這樣的介面:

interface Node {
    type: string;
    loc: SourceLocation | null;
}
複製程式碼

type 欄位表示不同的節點型別,下邊會再講一下各個型別的情況,分別對應了 JavaScript 中的什麼語法。
loc 欄位表示原始碼的位置資訊,如果沒有相關資訊的話為 null,否則是一個物件,包含了開始和結束的位置。介面如下:

interface SourceLocation {
    source: string | null;
    start: Position;
    end: Position;
}
複製程式碼

這裡的 Position 物件包含了行和列的資訊,行從 1 開始,列從 0 開始:

interface Position {
    line: number; // >= 1
    column: number; // >= 0
}
複製程式碼

Identifier

識別符號,就是我們寫 JS 時自定義的名稱,如變數名,函式名,屬性名,都歸為識別符號。相應的介面是這樣的:

interface Identifier <: Expression, Pattern {
    type: "Identifier";
    name: string;
}
複製程式碼

一個識別符號可能是一個表示式,或者是解構的模式(ES6 中的解構語法)。我們等會會看到 ExpressionPattern 相關的內容的。

PrivateName

interface PrivateName <: Expression, Pattern {
  type: "PrivateName";
  id: Identifier;
}
複製程式碼

A Private Name Identifier.

Literal

字面量,這裡不是指 [] 或者 {} 這些,而是本身語義就代表了一個值的字面量,如 1“hello”, true 這些,還有正規表示式(有一個擴充套件的 Node 來表示正規表示式),如 /d?/。我們看一下文件的定義:

interface Literal <: Expression {
    type: "Literal";
    value: string | boolean | null | number | RegExp;
}
複製程式碼

RegExpLiteral

value 這裡即對應了字面量的值,我們可以看出字面量值的型別,字串,布林,數值,null 和正則。

這個針對正則字面量的,為了更好地來解析正規表示式的內容,新增多一個 regex 欄位,裡邊會包括正則本身,以及正則的 flags

interface RegExpLiteral <: Literal {
  regex: {
    pattern: string;
    flags: string;
  };
}
複製程式碼

Programs

一般這個是作為根節點的,即代表了一棵完整的程式程式碼樹。

interface Program <: Node {
    type: "Program";
    body: [ Statement ];
}
複製程式碼

body 屬性是一個陣列,包含了多個 Statement(即語句)節點。

Functions

函式宣告或者函式表示式節點。

interface Function <: Node {
    id: Identifier | null;
    params: [ Pattern ];
    body: BlockStatement;
}
複製程式碼

id 是函式名,params 屬性是一個陣列,表示函式的引數。body 是一個塊語句。

有一個值得留意的點是,你在測試過程中,是不會找到 type: "Function" 的節點的,但是你可以找到 type: "FunctionDeclaration"type: "FunctionExpression",因為函式要麼以宣告語句出現,要麼以函式表示式出現,都是節點型別的組合型別,後邊會再提及 FunctionDeclarationFunctionExpression 的相關內容。

Statement

語句節點沒什麼特別的,它只是一個節點,一種區分,但是語句有很多種,下邊會詳述。

interface Statement <: Node { }
複製程式碼

ExpressionStatement

表示式語句節點,a = a + 1 或者 a++ 裡邊會有一個 expression 屬性指向一個表示式節點物件(後邊會提及表示式)。

interface ExpressionStatement <: Statement {
    type: "ExpressionStatement";
    expression: Expression;
}
複製程式碼

BlockStatement

塊語句節點,舉個例子:if (...) { // 這裡是塊語句的內容 },塊裡邊可以包含多個其他的語句,所以有一個 body 屬性,是一個陣列,表示了塊裡邊的多個語句。

interface BlockStatement <: Statement {
    type: "BlockStatement";
    body: [ Statement ];
} 
複製程式碼

ReturnStatement

返回語句節點,argument 屬性是一個表示式,代表返回的內容。

interface ReturnStatement <: Statement {
    type: "ReturnStatement";
    argument: Expression | null;
}
複製程式碼

IfStatement

if 語句節點,很常見,會帶有三個屬性,test 屬性表示 if (...) 括號中的表示式。

consequent 屬性是表示條件為 true 時的執行語句,通常會是一個塊語句。

alternate 屬性則是用來表示 else 後跟隨的語句節點,通常也會是塊語句,但也可以又是一個 if 語句節點,即類似這樣的結構:
if (a) { //... } else if (b) { // ... }
alternate 當然也可以為 null

interface IfStatement <: Statement {
    type: "IfStatement";
    test: Expression;
    consequent: Statement;
    alternate: Statement | null;
}
複製程式碼

SwitchStatement

switch 語句節點,有兩個屬性,discriminant 屬性表示 switch 語句後緊隨的表示式,通常會是一個變數,cases 屬性是一個 case 節點的陣列,用來表示各個 case 語句。

interface SwitchStatement <: Statement {
    type: "SwitchStatement";
    discriminant: Expression;
    cases: [ SwitchCase ];
}
複製程式碼

ForStatement

for 迴圈語句節點,屬性 init/test/update 分別表示了 for 語句括號中的三個表示式,初始化值,迴圈判斷條件,每次迴圈執行的變數更新語句(init 可以是變數宣告或者表示式)。這三個屬性都可以為 null,即 for(;;){}
body 屬性用以表示要迴圈執行的語句。

interface ForStatement <: Statement {
    type: "ForStatement";
    init: VariableDeclaration | Expression | null;
    test: Expression | null;
    update: Expression | null;
    body: Statement;
}
複製程式碼

Declarations

宣告語句節點,同樣也是語句,只是一個型別的細化。下邊會介紹各種宣告語句型別。

interface Declaration <: Statement { }
複製程式碼

FunctionDeclaration

函式宣告,和之前提到的 Function 不同的是,id 不能為 null

interface FunctionDeclaration <: Function, Declaration {
    type: "FunctionDeclaration";
    id: Identifier;
}
複製程式碼

VariableDeclaration

變數宣告,kind 屬性表示是什麼型別的宣告,因為 ES6 引入了 const/let
declarations 表示宣告的多個描述,因為我們可以這樣:let a = 1, b = 2;

interface VariableDeclaration <: Declaration {
    type: "VariableDeclaration";
    declarations: [ VariableDeclarator ];
    kind: "var";
}
複製程式碼
VariableDeclarator

變數宣告的描述,id 表示變數名稱節點,init 表示初始值的表示式,可以為 null

interface VariableDeclarator <: Node {
    type: "VariableDeclarator";
    id: Pattern;
    init: Expression | null;
} 
複製程式碼

Expressions

表示式節點。

interface Expression <: Node { }
複製程式碼

Import

interface Import <: Node {
    type: "Import";
}
複製程式碼

ArrayExpression

陣列表示式節點,elements 屬性是一個陣列,表示陣列的多個元素,每一個元素都是一個表示式節點。

interface ArrayExpression <: Expression {
    type: "ArrayExpression";
    elements: [ Expression | null ];
}
複製程式碼

ObjectExpression

物件表示式節點,property 屬性是一個陣列,表示物件的每一個鍵值對,每一個元素都是一個屬性節點。

interface ObjectExpression <: Expression {
    type: "ObjectExpression";
    properties: [ Property ];
}
複製程式碼
Property

物件表示式中的屬性節點。key 表示鍵,value 表示值,由於 ES5 語法中有 get/set 的存在,所以有一個 kind 屬性,用來表示是普通的初始化,或者是 get/set

interface Property <: Node {
    type: "Property";
    key: Literal | Identifier;
    value: Expression;
    kind: "init" | "get" | "set";
}
複製程式碼

FunctionExpression

函式表示式節點。

interface FunctionExpression <: Function, Expression {
    type: "FunctionExpression";
}
複製程式碼

BinaryExpression

二元運算表示式節點,leftright 表示運算子左右的兩個表示式,operator 表示一個二元運算子。

interface BinaryExpression <: Expression {
    type: "BinaryExpression";
    operator: BinaryOperator;
    left: Expression;
    right: Expression;
}
複製程式碼
BinaryOperator

二元運算子,所有值如下:

enum BinaryOperator {
    "==" | "!=" | "===" | "!=="
         | "<" | "<=" | ">" | ">="
         | "<<" | ">>" | ">>>"
         | "+" | "-" | "*" | "/" | "%"
         | "|" | "^" | "&" | "in"
         | "instanceof"
}
複製程式碼

AssignmentExpression

賦值表示式節點,operator 屬性表示一個賦值運算子,leftright 是賦值運算子左右的表示式。

interface AssignmentExpression <: Expression {
    type: "AssignmentExpression";
    operator: AssignmentOperator;
    left: Pattern | Expression;
    right: Expression;
}
複製程式碼
AssignmentOperator

賦值運算子,所有值如下:(常用的並不多)

enum AssignmentOperator {
    "=" | "+=" | "-=" | "*=" | "/=" | "%="
        | "<<=" | ">>=" | ">>>="
        | "|=" | "^=" | "&="
}
複製程式碼

ConditionalExpression

條件表示式,通常我們稱之為三元運算表示式,即 boolean ? true : false。屬性參考條件語句。

interface ConditionalExpression <: Expression {
    type: "ConditionalExpression";
    test: Expression;
    alternate: Expression;
    consequent: Expression;
}
複製程式碼

Misc

Decorator

interface Decorator <: Node {
  type: "Decorator";
  expression: Expression;
}
複製程式碼

Patterns

模式,主要在 ES6 的解構賦值中有意義,在 ES5 中,可以理解為和 Identifier 差不多的東西。

interface Pattern <: Node { }
複製程式碼

Classes

interface Class <: Node {
  id: Identifier | null;
  superClass: Expression | null;
  body: ClassBody;
  decorators: [ Decorator ];
}
複製程式碼

ClassBody

interface ClassBody <: Node {
  type: "ClassBody";
  body: [ ClassMethod | ClassPrivateMethod | ClassProperty | ClassPrivateProperty ];
}
複製程式碼

ClassMethod

interface ClassMethod <: Function {
  type: "ClassMethod";
  key: Expression;
  kind: "constructor" | "method" | "get" | "set";
  computed: boolean;
  static: boolean;
  decorators: [ Decorator ];
}
複製程式碼

Modules

ImportDeclaration

interface ImportDeclaration <: ModuleDeclaration {
  type: "ImportDeclaration";
  specifiers: [ ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier ];
  source: Literal;
}
複製程式碼

import 宣告,如: import foo from "mod";

Babylon AST node types

想知道完整的核心 Babylon AST node types,可檢視 babylon spec.md

總結

剛開始本來是準備講解 Babel 及常用模組的,後來發現內容太龐大,一篇文章根本容納不了,於是改為只關注 Babel 的程式碼解析 Babylon 部分,結果隨便一整理,又是這麼長,只能感嘆 Babel 博大精深啊。

參考

相關文章