AST(Abstract Syntax Tree)抽象语法树是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。在代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等场景均有广泛的应用,是前端工程化的基石。
可以使用AST Explorer来在线查看一段代码的AST。
核心概念
本文所说所有AST结点类型都是在Bebel中的,因为这是目前最流行的AST工具。
不同的代码类型分别有着不同的AST类型,每种 AST 都有自己的属性,但是它们也有一些公共的属性:
type
: AST 节点的类型start、end、loc
:start 和 end 代表该节点在源码中的开始和结束下标。而 loc 属性是一个对象,有 line 和 column 属性分别记录开始和结束的行列号。leadingComments、innerComments、trailingComments
: 表示开始的注释、中间的注释、结尾的注释,每个 AST 节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,想拿到某个 AST 的注释就通过这三个属性。
下面就来展开各种代码对应的AST类型。
字面量 Literal
字面量应该很熟悉,let a = 10
这里的10
就是一个number类型的字面量,字面量类型的AST类型如下:
- StringLiteral:字符串字面量,如
"string"
; - NumbericLiteral:数字字面量,如
123
; - TemplateLiteral:字母串模板字面量,如`string-${a}`;
- RegExpLiteral:正则表达式字面量,如
/\b[a-z]{1,5}/
; - BooleanLiteral:布尔值字面量,如
true
; - BigIntLiteral:BigInt字面领,如
1.23456n
; - NullLiteral:空值字面量,
null
; - UndefinedLiteral:为定义值字面量,
undefined
; - ArrayLiteral:数组字面量,如
[1,2]
; - ObjectLiteral:对象字面量,如
{ a: 1 }
。
标识符 Identifer
变量名、属性名、参数名等各种声明和引用的名字,都是标识符,Identifier 通常表示为一个字符串,它可以出现在表达式、语句和声明等多种语法结构中。
const a = 1;
function b(x) {
return x + 1;
}
例如上面这段代码中,属于标识符的有a
、b
、x
。
其解析后的AST如下
Program
├── VariableDeclaration (const a = 1)
│ ├── VariableDeclarator
│ │ ├── Identifier (a)
│ │ └── NumericLiteral (1)
│ └── const
└── FunctionDeclaration (function b(x) {...})
├── Identifier (b)
├── FunctionExpression
│ ├── Identifier (x)
│ ├── BlockStatement
│ │ └── ReturnStatement
│ │ └── BinaryExpression (x + 1)
│ └── []
└── []
语句 Statement
是可以独立执行的单位,比如 break、continue、debugger、return 或者 if 语句、while 语句、for 语句,还有声明语句,表达式语句等。
常见的语句类型如下
语句 | AST类型 |
---|---|
break; | BreakStatement |
continue; | ContinueStatement |
return; | ReturnStatement |
debugger; | DebuggerStatement |
throw Error(); | ThrowStatement |
{} | BlockStatement |
try {} catch(e) {} finally{} | TryStatement |
for (let key in obj) {} | ForInStatement |
for (let i = 0;i < 10;i ++) {} | ForStatement |
while (true) {} | WhileStatement |
do {} while (true) | DoWhileStatement |
switch (v){case 1: break;default:;} | SwitchStatement |
label: console.log(); | LabeledStatement |
with (a){} | WithStatement |
声明 Declaration
声明语句是一种特殊的语句,在作用域内声明一个变量、函数、class、import、export 等。
声明语句主要用于定义变量,常见的声明语句类型如下:
- VariableDeclaration:定义变量,
const a = 1
- FunctionDeclaration:函数声明,
function f() {};
; - ClassDeclaration:类定义,
class Animal {}
; - ImportDeclaration:导入模块,
import React from 'react'
; - ExportDefaultDeclaration:默认导出,
export default {}
; - ExportNamedDeclaration:具名导出,
export {a}
; - ExportAllDeclaration:全部导出,
export * from './utils'
。
表达式 Expression
表达式和语句的区别是:表达式执行完成之后有返回值。
例如以下常见的表达式
代码 | AST类型 |
---|---|
function(){}; | FunctionExpression |
() => {}; | ArrowFunctionExpression |
class{}; | ClassExpression |
a = 1 | AssignmentExpression |
1 + 2; | BinaryExpression |
-1; | UnaryExpression |
a; | Identifier |
this; | ThisExpression |
super; | Super |
a::b; | BindExpression |
注意这里的是匿名函数和匿名class。
解析AST
除了使用开头提到的在线AST解析工具,我们也可以通过代码来手动解析AST,这就要用到了前端工程化的基础工具——Babel。
首先安装必要的依赖包
$ pnpm install @babel/parser @babel/traverse @babel/generator @babel/types @babel/core -D
@babel/parser
:Babel 的 JavaScript 解析器,用于将 JavaScript 代码解析为 AST。它支持最新的 ECMAScript 标准,包括 ECMAScript 2022 中的特性。
@babel/traverse
:Babel 的 AST 遍历工具,可以遍历 AST 并执行操作。它可以用于修改 AST,例如添加、修改或删除节点等。@babel/generator
:Babel 的 AST 代码生成工具,用于将 AST 转换回 JavaScript 代码。它可以将修改后的 AST 转换回修改过的 JavaScript 代码。@babel/types
:Babel 的 AST 节点类型定义库,用于定义 AST 节点类型和节点操作的 API。它包含了 AST 中所有可能的节点类型,以及对这些节点类型的操作和方法。
使用Babel转换代码的过程如下图所示
我们分别来看一下不同的模块都有怎么样的效果,我们将利用下面这段代码来进行练习
function func(p: string) {
console.log(p);
return p + 'a'
}
let a = func('1');
console.log(a);
我们要把这段代码中的console.log打印的内容添加所在位置的行列号
@babel/parser
使用@babel/parser来解析这一段代码
import { parse } from '@babel/parser';
import {readFileSync} from 'node:fs';
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
// ESM 环境下构造__dirname
const __dirname = dirname(fileURLToPath(import.meta.url))
// 导入源码
const sourceCode = readFileSync(resolve(__dirname, './sourceCode.ts'), 'utf-8');
// 解析
const ast = parse(sourceCode, {
plugins: ['typescript']
})
@babel/traverse
使用@babel/traverse来遍历转化所有的console.log调用,注意每次替换节点之后需要使用skip方法跳过当前节点,因为替换节点之后还会再次遍历新的节点,避免触发死循环。
注意,真正的traverse需要通过.default
调用。
import traverse from '@babel/traverse';
traverse.default(ast, {
CallExpression(path) {
if (path.get('callee').matchesPattern('console.log')) {
// 获取行列号
const loc = path.node.loc;
const lineNumberNode = loc
? types.stringLiteral(`[${loc.start.line}, ${loc.start.column}] `)
: types.stringLiteral('(unknown location): ');
const newArguments = [lineNumberNode, ...path.node.arguments];
const newCallExpression = types.callExpression(types.memberExpression(types.identifier('console'), types.identifier('log')), newArguments);
// 替换原有的节点
path.replaceWith(newCallExpression);
// 跳过当前节点,避免死循环
path.skip()
}
},
})
@babel/generator
最后需要把转化后的AST进行编码,这个过程需要使用@babel/generator这个包,同样的也需要通过 .default
调用。
import generator from '@babel/generator';
// 将修改后的 AST 转换回代码
const { code } = generator.default(ast, {
sourceMaps: true,
}, sourceCode);
最终实现的效果如下
以上这三个包分别提供了Babel在不同阶段的能力,可以通过@babel/core
来统一调用这些功能。
如果在TS中使用Babel,通过安装@types/babel__xx来安装各个包的类型支持,例如
@babel/traverse
的类型支持为@types/babel__traverse
。
除了这三个提供基本能力的包之外,Babel还有其他的工具,可以去babel官网的工具目录进行查看。