前端进阶之 JS 抽象语法树

文章同步于 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/letdeclarations 表示声明的多个描述,因为我们可以这样: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/letdeclarations 表示声明的多个描述,因为我们可以这样: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 博大精深啊。

参考

猜你喜欢

转载自juejin.im/post/5b6304f46fb9a04fb136fee8
今日推荐