抽象语法树(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 节点类型:
声明语句:对应 AST 的 VariableDeclaration 节点,表示一个变量声明。
表达式语句:对应 AST 的 ExpressionStatement 节点,表示一个表达式语句。
控制语句:对应 AST 的 IfStatement、ForStatement、WhileStatement 等节点,分别表示 if、for、while 等控制语句。
语句块:对应 AST 的 BlockStatement 节点,表示一个语句块。
空语句:对应 AST 的 EmptyStatement 节点,表示一个空语句。
注意:AST 节点的类型是由 JavaScript 语言本身定义的,并且可能会在不同的 JavaScript 引擎中有所差异。
声明语句(Declaration)
变量声明 (VariableDeclaration)
1 | const x = 123; |
对应的 AST
1 | VariableDeclaration |
函数声明(FunctionDeclaration)
1 | function add(x, y) { |
对应的 AST
1 | FunctionDeclaration |
类声明(ClassDeclaration)
1 | class Point { |
对应的 AST
1 | ClassDeclaration |
导入声明(ImportDeclaration)
named import:用于导入另一个模块中命名的导出
1 | import { sum } from './math'; |
对应的 AST
1 | ImportDeclaration |
default import:用于导入另一个模块中默认的导出
1 | import foo from './module.js'; |
AST:
1 | ImportDeclaration |
namespace import:用于导入另一个模块中的所有导出,并将它们作为一个对象。
1 | import * as module from './module.js'; |
AST:
1 | ImportDeclaration |
导出声明(ExportDeclaration)
1. named export:用于将变量、函数、类等命名并导出(ExportNamedDeclaration)
1 | export function add(x, y) { |
对应的AST
1 | ExportNamedDeclaration |
default export:用于将变量、函数、类等作为模块的默认导出(ExportDefaultDeclaration)
1 | export default function add(x, y) { |
对应的 AST
1 | ExportDefaultDeclaration |
all exports:用于导出模块中的所有导出
1 | export * from './module.js'; |
AST
1 | ExportAllDeclaration |
表达式语句(ExpressionStatement)
在 JavaScript 中,表达式语句是指一种语句,其中包含表达式,并且在执行表达式时不会返回任何值。表达式语句通常用于对变量进行赋值、调用函数或执行其他操作。
注意:表达式和表达式语句是有区别的。
表达式是指一段代码,它可以被计算出一个值。表达式可以用于赋值、函数调用或其他操作。
1 | 10 + 20 |
表达式语句是指一种语句,其中包含表达式,并且在执行表达式时不会返回任何值。表达式语句通常用于对变量进行赋值、调用函数或执行其他操作。
例如,下面是一些表达式语句的例子
1 | x = 10; |
常见纠结问题:赋值语句会返回值,还是表达式语句吗?
在 JavaScript 中,赋值表达式会返回赋值后的值。但是,当赋值表达式作为一个独立的语句时,它就是一个表达式语句。
例如,下面的代码中的赋值表达式会返回赋值后的值:
1 | let x = 10; |
但是,如果将赋值表达式放在独立的语句中,它就是一个表达式语句,在执行时不会返回任何值:
1 | let x = 10; |
赋值语句
1 | x = 456; |
对应的 AST 结构如下:
1 | ExpressionStatement |
函数调用语句
1 | console.log(x); |
对应的 AST 结构如下:
1 | ExpressionStatement |
return 语句
1 | return x + y; |
1 | ReturnStatement |
throw 语句
1 | throw new Error('Something went wrong'); |
1 | ThrowStatement |
控制语句(control statement)
在 JavaScript 中,控制语句是指用于控制程序流程的语句。控制语句可以用于执行不同的操作、跳转到不同的位置或做出决策。
if 语句(IfStatement)
1 | if (x > 100) { |
对应的 AST 结构如下:
1 | IfStatement |
switch 语句
1 | switch (x) { |
对应的 AST
1 | SwitchStatement |
while 循环
1 | let i = 0; |
对应的AST
1 | While Statement |
for 循环
1 | for (let i = 0; i < 10; i++) { |
对应的 AST 结构如下:
1 | ForStatement |
while循环
1 | while (x > 0) { |
对应的 AST 结构如下:
1 | WhileStatement |
do-while 循环
1 | let i = 0; |
AST:
1 | DoWhile Statement |
for-in 循环
1 | const obj = {x: 10, y: 20}; |
AST:
1 | ForIn Statement |
for-of 循环
1 | const arr = [1, 2, 3]; |
1 | ForOf Statement |
AST:
字面量(Literal)
字面量是指直接在程序中写出来的常量值,JavaScript 支持五种基本的字面量:数字、字符串、布尔值、对象和数组。
对应在 JavaScript 抽象语法树(AST)中,这些字面量会用到以下几种节点:
- 数字字面量对应
NumericLiteral节点,表示一个数字字面量。 - 字符串字面量对应
StringLiteral节点,表示一个字符串字面量。 - 布尔值字面量对应
BooleanLiteral节点,表示一个布尔值字面量。 - 对象字面量对应
ObjectExpression节点,表示一个对象字面量。对象字面量中的每个属性对应一个Property节点。 - 数组字面量对应
ArrayExpression节点,表示一个数组字面量。数组字面量中的每个元素对应一个ArrayExpression的子节点。
例如,以下代码:对应的 AST 可能长这样:1
2
3
4
5const x = 123;
const y = 'hello';
const z = true;
const obj = { a: 1, b: 2 };
const arr = [1, 2, 3];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
35Program
├── 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 中,标识符是指变量、常量、函数名或属性名。标识符是由一个或多个字母、数字或下划线组成的,且必须以字母或下划线开头。
标识符的作用是在程序中标记变量、常量、函数或属性的名字,便于程序中的变量、常量、函数或属性能够被引用和使用。
例如,以下代码中的 x、y 和 add 都是标识符:
1 | let x = 123; |
JavaScript 的标识符遵循一些命名规则,例如:
- 不能使用关键字作为标识符名。
- 不能使用保留字作为标识符名。
- 不能使用特殊字符作为标识符名。
标识符在 JavaScript 抽象语法树(AST)中对应的节点为 Identifier。
例如,在以下代码的 AST 中:
1 | const x = 123; |
变量 x 对应的节点是:
1 | VariableDeclaration |
根节点(Program)
Program 节点是 AST 中的顶级节点,它表示 JavaScript 代码中的整个程序。Program 节点通常包含多个子节点,表示程序中的各个部分,如函数声明、变量声明、表达式等。
Derective 属性
表示 JavaScript 代码中的指令语句。指令语句是 JavaScript 代码中的一种特殊语句,它们以 'use strict' 开头,用于指定 JavaScript 代码的运行模式。
与位置有关的属性
与位置有关的节点在 AST 中通常会包含有关该节点在源代码中的位置的信息。这些信息可以用于在编译器或代码编辑器中显示错误信息,或者在源代码转换过程中确定节点在源代码中的位置。
例如,在 JavaScript 中,可以使用 start 和 end 属性来表示节点在源代码中的起始位置和结束位置。这些属性通常是一个对象,包含有关该位置的行号和列号的信息。
1 | const a = 1 |
在 AST explorer (以babel为例)中:
![[../../images/quicker_306054b7-0f7e-4c5c-8e3f-01017d0b1521.png]]
注释节点
在 JavaScript 中,注释节点可以使用两种类型之一表示:Line 或 Block。
Line表示单行注释,其中注释以两个斜杠(//)开头。Block表示多行注释,其中注释以一对星号(/*)开始,以一对星号(*/)结束。
例如,以下代码片段中的注释将生成一个 Line 节点:
1 | // This is a single-line comment |
AST:
1 | Program |
以下代码片段中的注释将生成一个 Block 节点:
1 | /* This is a |
AST:
1 | Program |
对 AST 进行处理
在生成 AST 以后就可以对 AST 进行处理
- 分析 AST 结构进行规范性检查 -> lint 工具的本质
- 从 AST 中抽取注释 -> 文档自动生成工具
- 分析类型信息 -> 类型工具
- 直接执行语句 -> js 解释器
总结
在 JavaScript 中,AST(抽象语法树)是一种表示代码的数据结构。它使用节点来表示代码中的各种元素,如变量、常量、表达式、函数声明等。AST 可以用来表示 JavaScript 代码的语法结构,并且可以通过遍历 AST 来分析、修改或生成代码。
链接
AST explorer 是探索 AST 的好地方。