抽象语法树(AST)

javascript 的 AST标准 Estree

为了让机器能够理解代码的含义我们把代码转换为了抽象语法树(AST), AST 的结构是有对应的标准的。在 javascript 中, AST 通常遵循 estree 标准.

ESTree(ECMAScript Tree)是一种用于表示 ECMAScript 源代码语法结构的抽象语法树(AST)的标准。ECMAScript 是 JavaScript 编程语言的标准,而 ESTree 标准则定义了如何表示 JavaScript 代码的语法结构,以方便编译器和其他工具对 JavaScript 代码进行处理。

ESTree 标准定义了一系列用于表示源代码结构的节点类型,并且每个节点都有一些属性来表示它所表示的结构的具体信息。例如,对于一个函数声明,ESTree 标准中定义了“FunctionDeclaration”节点来表示这个结构。“FunctionDeclaration”节点中会包含“id”属性表示函数名,“params”属性表示函数的参数,“body”属性表示函数主体等。

ESTree 标准的目的是让 JavaScript 编译器和工具之间能够交换源代码的语法信息,从而提高 JavaScript 代码的可移植性和可复用性。

==AST 中的节点类型涵盖了 javaScript 中的所有语法结构==
下面是常见的节点类型:

  • Program:表示整个源代码的结构。
  • BlockStatement:表示代码块,例如 if 语句中的代码块、函数主体等。
  • VariableDeclaration:表示变量声明的语句。
  • VariableDeclarator:表示变量的声明,包括变量名和初始值。
  • Identifier:表示标识符。
  • Literal:表示字面量,例如数字、字符串等。
  • ExpressionStatement:表示表达式语句,例如赋值语句、函数调用语句等。
  • BinaryExpression:表示两个操作数的二元表达式,例如加法、乘法等。
  • UnaryExpression:表示一个操作数的一元表达式,例如取反、自增等。
  • FunctionDeclaration:表示函数声明,包括函数名、参数列表和函数主体。
  • ReturnStatement:表示函数返回语句。
  • IfStatement:表示 if 语句,包括判断条件和执行的代码块。
  • SwitchStatement:表示 switch 语句,包括判断条件和多个 case 分支。
  • ForStatement:表示 for 循环语句,包括初始化语句、循环条件和更新语句。
  • WhileStatement:表示 while 循环语句,包括循环条件和执行的代码块。
  • DoWhileStatement:表示 do-while 循环语句,包括循环条件和执行的代码块。
  • BreakStatement:表示 break 语句。
  • ContinueStatement:表示 continue 语句。

在 AST 中,每个节点类型都是独立的,它们之间没有任何继承关系。例如,“FunctionDeclaration”节点类型并不是“BlockStatement”节点类型的子类型,它们之间没有任何关系。

不过,在 AST 中,一些节点类型可能会拥有其他节点类型作为它的子节点。例如,一个“FunctionDeclaration”节点可能会有一个“BlockStatement”节点作为它的子节点,表示函数主体。但这并不意味着“FunctionDeclaration”是“BlockStatement”的子类型,它们之间只是有一个父子关系,并不存在继承关系。

下面将介绍具体的语法结构。

语句(Statement + Declaration)

在 JavaScript 中,语句是指一个独立的执行单元。语句可以是赋值语句、函数调用语句、if 语句、for 语句、while 语句等。
在 JavaScript 中,每个语句都会在 AST 中对应一个节点。下面列出了每种语句对应的 AST 节点类型:

  1. 声明语句:对应 AST 的 VariableDeclaration 节点,表示一个变量声明。

  2. 表达式语句:对应 AST 的 ExpressionStatement 节点,表示一个表达式语句。

  3. 控制语句:对应 AST 的 IfStatement、ForStatement、WhileStatement 等节点,分别表示 if、for、while 等控制语句。

  4. 语句块:对应 AST 的 BlockStatement 节点,表示一个语句块。

  5. 空语句:对应 AST 的 EmptyStatement 节点,表示一个空语句。

注意:AST 节点的类型是由 JavaScript 语言本身定义的,并且可能会在不同的 JavaScript 引擎中有所差异。

声明语句(Declaration)

变量声明 (VariableDeclaration)

1
const x = 123;

对应的 AST

1
2
3
4
VariableDeclaration
└── VariableDeclarator
├── Identifier (x)
└── NumericLiteral (123)

函数声明(FunctionDeclaration)

1
2
3
function add(x, y) {
return x + y;
}

对应的 AST

1
2
3
4
5
6
7
8
9
10
11
FunctionDeclaration
├── Identifier (add)
├── FormalParameters
│ ├── Identifier (x)
│ └── Identifier (y)
└── BlockStatement
├── ReturnStatement
│ └── Binary Expression
│ ├── Identifier (x)
│ └── Identifier (y)
└── End

类声明(ClassDeclaration)

1
2
3
4
5
6
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}

对应的 AST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ClassDeclaration
├── Identifier (Point)
└── ClassBody
├── MethodDefinition (constructor)
│ ├── Identifier (constructor)
│ ├── FormalParameters
│ │ ├── Identifier (x)
│ │ └── Identifier (y)
│ └── BlockStatement
│ ├── ExpressionStatement
│ │ └── Assignment Expression
│ │ ├── Member Expression
│ │ │ ├── ThisExpression
│ │ │ └── Identifier (x)
│ │ └── Identifier (x)
│ ├── Expression Statement
│ │ └── Assignment Expression
│ │ ├── Member Expression
│ │ │ ├── ThisExpression
│ │ │ └── Identifier (y)
│ │ └── Identifier (y)
│ └── End
└── End

导入声明(ImportDeclaration)

named import:用于导入另一个模块中命名的导出
1
import { sum } from './math';

对应的 AST

1
2
3
4
5
ImportDeclaration
├── ImportSpecifier
│ ├── Identifier (sum)
│ └── Identifier (sum)
└── StringLiteral ('./math')
default import:用于导入另一个模块中默认的导出
1
import foo from './module.js';

AST:

1
2
3
4
ImportDeclaration
├── ImportDefaultSpecifier
│ └── Identifier (foo)
└── Literal ("./module.js")
namespace import:用于导入另一个模块中的所有导出,并将它们作为一个对象。
1
import * as module from './module.js';

AST:

1
2
3
4
ImportDeclaration
├── ImportNamespaceSpecifier
│ └── Identifier (module)
└── Literal ("./module.js")

导出声明(ExportDeclaration)

1. named export:用于将变量、函数、类等命名并导出(ExportNamedDeclaration)
1
2
3
export function add(x, y) {
return x + y;
}

对应的AST

1
2
3
4
5
6
7
8
9
10
11
12
13
ExportNamedDeclaration
├── FunctionDeclaration
│ ├── Identifier (add)
│ ├── FormalParameters
│ │ ├── Identifier (x)
│ │ └── Identifier (y)
│ └── Block Statement
│ ├── Return Statement
│ │ └── Binary Expression
│ │ ├── Identifier (x)
│ │ └── Identifier (y)
│ └── End
└── End
default export:用于将变量、函数、类等作为模块的默认导出(ExportDefaultDeclaration)
1
2
3
export default function add(x, y) {
return x + y;
}

对应的 AST

1
2
3
4
5
6
7
8
9
10
11
12
ExportDefaultDeclaration
└── FunctionDeclaration
├── Identifier (add)
├── FormalParameters
│ ├── Identifier (x)
│ └── Identifier (y)
└── Block Statement
├── Return Statement
│ └── Binary Expression
│ ├── Identifier (x)
│ └── Identifier (y)
└── End
all exports:用于导出模块中的所有导出
1
export * from './module.js';

AST

1
2
ExportAllDeclaration
└── Literal ("./module.js")

表达式语句(ExpressionStatement)

在 JavaScript 中,表达式语句是指一种语句,其中包含表达式,并且在执行表达式时不会返回任何值。表达式语句通常用于对变量进行赋值、调用函数或执行其他操作。
注意:表达式和表达式语句是有区别的。
表达式是指一段代码,它可以被计算出一个值。表达式可以用于赋值、函数调用或其他操作。

1
2
3
4
10 + 20
'Hello, World!'
{ x: 10, y: 20 }
function(x, y) { return x + y }

表达式语句是指一种语句,其中包含表达式,并且在执行表达式时不会返回任何值。表达式语句通常用于对变量进行赋值、调用函数或执行其他操作。
例如,下面是一些表达式语句的例子

1
2
3
4
5
x = 10;
y = x + 20;
console.log('Hello, World!');
add(10, 20);
return x + y;

常见纠结问题:赋值语句会返回值,还是表达式语句吗?
在 JavaScript 中,赋值表达式会返回赋值后的值。但是,当赋值表达式作为一个独立的语句时,它就是一个表达式语句。
例如,下面的代码中的赋值表达式会返回赋值后的值:

1
2
3
4
let x = 10;
let y = (x = 20);

console.log(y); // 20

但是,如果将赋值表达式放在独立的语句中,它就是一个表达式语句,在执行时不会返回任何值:

1
2
let x = 10;
x = 20;

赋值语句

1
x = 456;

对应的 AST 结构如下:

1
2
3
4
ExpressionStatement
└── AssignmentExpression
├── Identifier (x)
└── NumericLiteral (456)

函数调用语句

1
console.log(x);

对应的 AST 结构如下:

1
2
3
4
5
6
ExpressionStatement
└── CallExpression
├── MemberExpression
│ ├── Identifier (console)
│ └── Identifier (log)
└── Identifier (x)

return 语句

1
return x + y;
1
2
3
4
ReturnStatement
└── Binary Expression
├── Identifier (x)
└── Identifier (y)

throw 语句

1
throw new Error('Something went wrong');
1
2
3
4
ThrowStatement
└── NewExpression
├── Identifier (Error)
└── StringLiteral ('Something went wrong')

控制语句(control statement)

在 JavaScript 中,控制语句是指用于控制程序流程的语句。控制语句可以用于执行不同的操作、跳转到不同的位置或做出决策。

if 语句(IfStatement)

1
2
3
if (x > 100) {
console.log('x is larger than 100');
}

对应的 AST 结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
IfStatement
├── BinaryExpression
│ ├── Identifier (x)
│ ├── >
│ └── NumericLiteral (100)
├── BlockStatement
│ └── Expression Statement
│ └── CallExpression
│ ├── MemberExpression
│ │ ├── Identifier (console)
│ │ └── Identifier (log)
│ └── StringLiteral ('x is larger than 100')
└── Null

switch 语句

1
2
3
4
5
6
7
8
9
10
switch (x) {
case 10:
console.log('x is 10');
break;
case 20:
console.log('x is 20');
break;
default:
console.log('x is neither 10 nor 20');
}

对应的 AST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
SwitchStatement
├── Identifier (x)
├── SwitchCase
│ ├── NumericLiteral (10)
│ └── ExpressionStatement
│ └── CallExpression
│ ├── MemberExpression
│ │ ├── Identifier (console)
│ │ └── Identifier (log)
│ └── StringLiteral ('x is 10')
├── SwitchCase
│ ├── NumericLiteral (20)
│ └── ExpressionStatement
│ └── CallExpression
│ ├── MemberExpression
│ │ ├── Identifier (console)
│ │ └── Identifier (log)
│ └── StringLiteral ('x is 20')
└── SwitchCase
├── null
└── ExpressionStatement
└── CallExpression
├── MemberExpression
│ ├── Identifier (console)
│ └── Identifier (log)
└── StringLiteral ('x is neither 10 nor 20')

while 循环

1
2
3
4
5
let i = 0;
while (i < 10) {
console.log(i);
i++;
}

对应的AST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
While Statement
├── Binary Expression
│ ├── Identifier (i)
│ ├── <
│ └── NumericLiteral (10)
└── Block Statement
├── Expression Statement
│ └── Call Expression
│ ├── Member Expression
│ │ ├── Identifier (console)
│ │ └── Identifier (log)
│ └── Identifier (i)
└── Expression Statement
└── Update Expression
├── Identifier (i)
└── ++

for 循环

1
2
3
for (let i = 0; i < 10; i++) {
console.log(i);
}

对应的 AST 结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ForStatement
├── VariableDeclaration
│ ├── VariableDeclarator
│ │ ├── Identifier (i)
│ │ └── NumericLiteral (0)
│ └── ;
├── BinaryExpression
│ ├── Identifier (i)
│ ├── <
│ └── NumericLiteral (10)
├── UpdateExpression
│ ├── Identifier (i)
│ ├── ++
│ └── true
└── BlockStatement
└── ExpressionStatement
└── CallExpression
├── MemberExpression
│ ├── Identifier (console)
│ └── Identifier (log)
└── Identifier (i)

while循环

1
2
3
4
while (x > 0) {
console.log(x);
x--;
}

对应的 AST 结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
WhileStatement
├── BinaryExpression
│ ├── Identifier (x)
│ ├── >
│ └── NumericLiteral (0)
└── Block Statement
├── ExpressionStatement
│ └── CallExpression
│ ├── MemberExpression
│ │ ├── Identifier (console)
│ │ └── Identifier (log)
│ └── Identifier (x)
└── Expression Statement
└── UpdateExpression
├── Identifier (x)
├── --
└── true

do-while 循环

1
2
3
4
5
let i = 0;
do {
console.log(i);
i++;
} while (i < 10);

AST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DoWhile Statement
├── Block Statement
│ ├── Expression Statement
│ │ └── Call Expression
│ │ ├── Member Expression
│ │ │ ├── Identifier (console)
│ │ │ └── Identifier (log)
│ │ └── Identifier (i)
│ └── Expression Statement
│ └── Update Expression
│ ├── Identifier (i)
│ └── ++
└── Binary Expression
├── Identifier (i)
├── <
└── NumericLiteral (10)

for-in 循环

1
2
3
4
const obj = {x: 10, y: 20};
for (const key in obj) {
console.log(key, obj[key]);
}

AST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ForIn Statement
├── Variable Declaration
│ ├── Identifier (key)
│ └── Object Expression
│ ├── Property
│ │ ├── Identifier (x)
│ │ └── NumericLiteral (10)
│ └── Property
│ ├── Identifier (y)
│ └── NumericLiteral (20)
└── Block Statement
├── Expression Statement
│ └── Call Expression
│ ├── Member Expression
│ │ ├── Identifier (console)
│ │ └── Identifier (log)
│ ├── Identifier (key)
│ └── Member Expression
│ ├── Identifier (obj)
│ └── Identifier (key)

for-of 循环

1
2
3
4
const arr = [1, 2, 3];
for (const value of arr) {
console.log(value);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ForOf Statement
├── Variable Declaration
│ ├── Identifier (value)
│ └── Array Expression
│ ├── NumericLiteral (1)
│ ├── NumericLiteral (2)
│ └── NumericLiteral (3)
└── Block Statement
└── Expression Statement
└── Call Expression
├── Member Expression
│ ├── Identifier (console)
│ └── Identifier (log)
└── Identifier (value)

AST:

字面量(Literal)

字面量是指直接在程序中写出来的常量值,JavaScript 支持五种基本的字面量:数字、字符串、布尔值、对象和数组。

对应在 JavaScript 抽象语法树(AST)中,这些字面量会用到以下几种节点:

  • 数字字面量对应 NumericLiteral 节点,表示一个数字字面量。
  • 字符串字面量对应 StringLiteral 节点,表示一个字符串字面量。
  • 布尔值字面量对应 BooleanLiteral 节点,表示一个布尔值字面量。
  • 对象字面量对应 ObjectExpression 节点,表示一个对象字面量。对象字面量中的每个属性对应一个 Property 节点。
  • 数组字面量对应 ArrayExpression 节点,表示一个数组字面量。数组字面量中的每个元素对应一个 ArrayExpression 的子节点。
    例如,以下代码:
    1
    2
    3
    4
    5
    const x = 123;
    const y = 'hello';
    const z = true;
    const obj = { a: 1, b: 2 };
    const arr = [1, 2, 3];
    对应的 AST 可能长这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    Program
    ├── VariableDeclaration
    │ ├── VariableDeclarator
    │ │ ├── Identifier (x)
    │ │ └── NumericLiteral (123)
    │ └── ;
    ├── VariableDeclaration
    │ ├── VariableDeclarator
    │ │ ├── Identifier (y)
    │ │ └── StringLiteral ('hello')
    │ └── ;
    ├── VariableDeclaration
    │ ├── VariableDeclarator
    │ │ ├── Identifier (z)
    │ │ └── BooleanLiteral (true)
    │ └── ;
    ├── VariableDeclaration
    │ ├── VariableDeclarator
    │ │ ├── Identifier (obj)
    │ │ └── ObjectExpression
    │ │ ├── Property
    │ │ │ ├── Identifier (a)
    │ │ │ └── NumericLiteral (1)
    │ │ └── Property
    │ │ ├── Identifier (b)
    │ │ └── NumericLiteral (2)
    │ └── ;
    └── VariableDeclaration
    ├── VariableDeclarator
    │ ├── Identifier (arr)
    │ └── ArrayExpression
    │ ├── NumericLiteral (1)
    │ ├── NumericLiteral (2)
    │ └── NumericLiteral (3)
    └── ;

标识符 Identifier

在上面的例子中也可以看到不少 Identifier, 标识符也是最小的语法单元之一。
在 JavaScript 中,标识符是指变量、常量、函数名或属性名。标识符是由一个或多个字母、数字或下划线组成的,且必须以字母或下划线开头。

标识符的作用是在程序中标记变量、常量、函数或属性的名字,便于程序中的变量、常量、函数或属性能够被引用和使用。

例如,以下代码中的 xyadd 都是标识符:

1
2
3
4
5
6
7
8
let x = 123;
const y = 'hello';

function add(a, b) {
return a + b;
}

console.log(add(x, y));

JavaScript 的标识符遵循一些命名规则,例如:

  • 不能使用关键字作为标识符名。
  • 不能使用保留字作为标识符名。
  • 不能使用特殊字符作为标识符名。

标识符在 JavaScript 抽象语法树(AST)中对应的节点为 Identifier
例如,在以下代码的 AST 中:

1
const x = 123;

变量 x 对应的节点是:

1
2
3
4
VariableDeclaration
└── VariableDeclarator
├── Identifier (x)
└── NumericLiteral (123)

根节点(Program)

Program 节点是 AST 中的顶级节点,它表示 JavaScript 代码中的整个程序。Program 节点通常包含多个子节点,表示程序中的各个部分,如函数声明、变量声明、表达式等。

Derective 属性

表示 JavaScript 代码中的指令语句。指令语句是 JavaScript 代码中的一种特殊语句,它们以 'use strict' 开头,用于指定 JavaScript 代码的运行模式。

与位置有关的属性

与位置有关的节点在 AST 中通常会包含有关该节点在源代码中的位置的信息。这些信息可以用于在编译器或代码编辑器中显示错误信息,或者在源代码转换过程中确定节点在源代码中的位置。

例如,在 JavaScript 中,可以使用 startend 属性来表示节点在源代码中的起始位置和结束位置。这些属性通常是一个对象,包含有关该位置的行号和列号的信息。

1
const a = 1

AST explorer (以babel为例)中:

![[../../images/quicker_306054b7-0f7e-4c5c-8e3f-01017d0b1521.png]]

注释节点

在 JavaScript 中,注释节点可以使用两种类型之一表示:LineBlock

  • Line 表示单行注释,其中注释以两个斜杠(//)开头。
  • Block 表示多行注释,其中注释以一对星号(/*)开始,以一对星号(*/)结束。

例如,以下代码片段中的注释将生成一个 Line 节点:

1
2
// This is a single-line comment
let x = 123;

AST:

1
2
3
4
5
Program
└── VariableDeclaration
└── VariableDeclarator
├── Identifier (x)
└── NumericLiteral (123)

以下代码片段中的注释将生成一个 Block 节点:

1
2
3
/* This is a
multi-line comment */
let x = 123;

AST:

1
2
3
4
5
6
7
Program
├── Block
│ └── Comment (This is a\nmulti-line comment)
└── VariableDeclaration
└── VariableDeclarator
├── Identifier (x)
└── NumericLiteral (123)

对 AST 进行处理

在生成 AST 以后就可以对 AST 进行处理

  • 分析 AST 结构进行规范性检查 -> lint 工具的本质
  • 从 AST 中抽取注释 -> 文档自动生成工具
  • 分析类型信息 -> 类型工具
  • 直接执行语句 -> js 解释器

总结

在 JavaScript 中,AST(抽象语法树)是一种表示代码的数据结构。它使用节点来表示代码中的各种元素,如变量、常量、表达式、函数声明等。AST 可以用来表示 JavaScript 代码的语法结构,并且可以通过遍历 AST 来分析、修改或生成代码。

链接

AST explorer 是探索 AST 的好地方。