AST 抽象语法树

抽象语法树(AST,Abstract Syntax Tree)概述

抽象语法树(AST)是一种以树状结构表达代码的方式,它将源代码中的各个语法元素(如变量、运算符、函数等)抽象成树的节点。每个节点表示代码中的一个语法构造,且每一层的节点都代表了代码的不同组成部分。AST 是编译器和工具(如 Babel、TypeScript、ESLint 等)用于分析、转换、优化和生成代码的核心数据结构。

AST 的作用

  1. 语法分析: AST 是语法分析的结果,它为编译器提供了对源代码的深度理解。通过 AST,编译器能够了解代码的结构和语义,进而进行优化和转换。
  2. 代码转换: 在工具链(如 Babel)中,AST 是将代码从一种语法转化为另一种语法的核心。例如,将 ES6 转换为 ES5。
  3. 代码检查: AST 是 ESLint、Prettier 等工具分析代码规范的基础。它能够帮助这些工具检查代码中的潜在错误、风格问题等。
  4. 优化: 通过 AST,编译器可以对代码进行优化,删除冗余代码,进行内联等优化操作。

AST 的结构

一个抽象语法树通常由以下几个元素组成:

  • 节点(Node): 每个节点代表了源代码中的某个语法元素。节点包含了语法的类型和该语法元素的具体信息。
  • 子节点(Children): 节点的子节点表示语法结构中的更小的部分。节点可以有多个子节点,也可能没有子节点(如常量、变量名等)。

例如,下面是一个简单的 ES6 箭头函数的代码:

1
const add = (a, b) => a + b

这个代码片段的 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
36
37
38
39
40
41
42
43
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "add"
},
"init": {
"type": "ArrowFunctionExpression",
"params": [
{
"type": "Identifier",
"name": "a"
},
{
"type": "Identifier",
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Identifier",
"name": "b"
}
}
}
}
],
"kind": "const"
}
]
}

AST 结构解析

  1. Program 节点:

    • AST 的根节点是 Program,表示整个程序的结构。
    • Program 有一个 body 属性,它包含程序的所有语句。
  2. VariableDeclaration 节点:

    • VariableDeclaration 代表声明变量的语句(在这个例子中是 const add)。
    • declarations 属性包含一个或多个变量声明,这里只有一个 add
  3. VariableDeclarator 节点:

    • VariableDeclarator 代表单个变量声明,它包含变量名(id)和初始化表达式(init)。
    • id 节点包含变量的名字 "add"
    • init 节点包含右边的箭头函数表达式(ArrowFunctionExpression)。
  4. ArrowFunctionExpression 节点:

    • ArrowFunctionExpression 节点表示箭头函数,它有 paramsbody 属性。
    • params 是一个数组,包含箭头函数的参数(ab)。
    • body 是箭头函数的执行体,这里是一个二元表达式(a + b)。
  5. BinaryExpression 节点:

    • BinaryExpression 节点表示一个二元操作(如加法、减法等)。
    • operator 属性表示操作符,这里是加法符号 "+"
    • leftright 属性分别是操作符左右的表达式,表示变量 ab

如何生成 AST?

AST 通常是通过 解析器(Parser) 生成的,解析器会根据语法规则分析源代码,并创建相应的 AST。不同语言或不同版本的 JavaScript 会有不同的语法规则和 AST 结构。

以 Babel 为例,当你使用 Babel 转换 ES6+ 代码时,它会执行以下步骤:

  1. 输入代码: Babel 会接收 JavaScript 源代码,如 ES6+ 语法的代码。
  2. 解析代码: Babel 使用 JavaScript 解析器(如 @babel/parser)将源代码解析为 AST。
  3. 转换 AST: Babel 会根据配置的插件对 AST 进行转换(例如,将箭头函数转换为普通函数)。
  4. 生成代码: 转换后的 AST 会被 Babel 转换回 JavaScript 代码,通常是 ES5 代码。

AST 示例:

考虑以下简单的代码:

1
const sum = (a, b) => a + b

这个代码会被解析成以下结构的 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
36
37
38
39
40
41
42
43
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "sum"
},
"init": {
"type": "ArrowFunctionExpression",
"params": [
{
"type": "Identifier",
"name": "a"
},
{
"type": "Identifier",
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Identifier",
"name": "b"
}
}
}
}
],
"kind": "const"
}
]
}

解释:

  • Program: 表示整个程序。
  • VariableDeclaration: 代表 const sum = ... 这行代码的声明。
  • VariableDeclarator: 表示 sum 的声明和它的初始化值(即箭头函数)。
  • ArrowFunctionExpression: 代表箭头函数,包含两个参数 ab,以及函数体(加法表达式)。
  • BinaryExpression: 表示加法操作,a + b

AST 在工具中的应用

  1. Babel

    • 用于将 ES6+ 转换为 ES5。Babel 会首先将 JavaScript 代码转换成 AST,然后根据插件对 AST 进行转换,最后生成新的 JavaScript 代码。
  2. ESLint

    • 用于静态代码分析。ESLint 会通过解析代码生成 AST,然后根据预定义的规则分析 AST 结构,找出代码中的潜在问题。
  3. Prettier

    • 用于格式化代码。Prettier 会生成 AST,对代码进行重新排列,生成一致的格式。

总结

  • 抽象语法树(AST) 是代码的树状表示,用于分析、转换、优化代码。
  • AST 将源代码的语法结构转化为可操作的树形数据结构,每个节点表示代码中的一个构造。
  • Babel、ESLint、Prettier 等工具都依赖 AST 来执行代码转换、检查和格式化等操作。