抽象语法树(AST)这个词,凡是研究过前端工程化的人都绕不开。Babel转译新语法、ESLint检查代码风格、Prettier格式化代码、Webpack的Tree Shaking优化,这些工具的背后都站着同一套机制:先把JavaScript源代码解析成一棵结构化的树,再对树上的节点进行分析和改写,最后重新生成代码。理解AST,就等于拿到了这些工具的底层说明书。这篇文章从解析流程、树的结构、遍历方式三个层面展开,配合可运行的代码示例,帮你把AST这个概念彻底吃透。

从源代码到AST:解析的两个阶段
JavaScript引擎和各类工具处理源代码的第一步都不是直接执行,而是先做解析。整个解析过程分为两个阶段:词法分析和语法分析。词法分析负责把字符流切分成一个个有意义的单元,也就是Token。比如const x = 1 + 2;这行代码,词法分析器会把它切分成const、x、=、1、+、2、;共七个Token,同时标记每个Token的类型是关键字、标识符、运算符还是字面量。
语法分析阶段做的事情,是按照JavaScript的语法规则,把这些线性的Token序列组织成树形结构。解析器会从最外层的程序节点开始,递归下降地识别每一条语句。遇到const关键字,它知道后面应该跟一个变量声明节点;等号右边是一个二元表达式,就再挂一个表达式节点。这个过程结束后,源代码就从一串字符变成了一棵层次分明的树,树上的每个节点都记录了类型、位置、子节点等信息。
你可以打开AST Explorer这个在线工具,随便粘贴一段代码观察生成的树。树的根节点类型通常是Program,它的body属性是一个数组,装着所有的顶层语句。需要注意的是,AST之所以叫「抽象」语法树,是因为它抛弃了括号、分号、注释这些对语义没有影响的细节,只保留程序结构本身。这也是为什么两段格式完全不同但逻辑相同的代码,解析出的AST可能是同一个。
AST节点的结构:类型决定一切
树上的每个节点都是一个普通JavaScript对象,最核心的属性是type。类型决定了节点代表什么语法结构,也决定了它还有哪些附加属性。常见的基础类型有:Identifier表示标识符,Literal表示字面量,VariableDeclaration表示变量声明。复杂一点的如BinaryExpression表示二元表达式,它有left、right和operator三个关键属性;FunctionDeclaration表示函数声明,包含id、params和body。
用Babel的解析器实际看一下效果,代码如下:
// 先安装:npm install @babel/parser @babel/traverse
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const code = `const sum = 10 + 20;`;
const ast = parser.parse(code);
console.log(JSON.stringify(ast, null, 2));
// 输出的树中可以看到:
// Program 节点
// └── VariableDeclaration (kind: "const")
// └── VariableDeclarator
// ├── id: Identifier (name: "sum")
// └── init: BinaryExpression (operator: "+")
// ├── left: NumericLiteral (10)
// └── right: NumericLiteral (20)从输出结果能清楚看到节点的嵌套关系。变量声明节点下面挂着一个声明器节点,声明器又分成左边的标识符和右边的初始化表达式,初始化表达式再拆成左右两个数字字面量。这种结构非常规整,工具开发者只需要针对不同类型编写处理逻辑,就能对代码做任意维度的分析和修改。
还有一个细节值得注意:节点上通常会附带loc属性,记录该节点在源代码中的行列位置。ESLint报告错误时能精确到第几行第几列,靠的就是这个属性。另外,AST遵循的是ESTree规范,各家解析器产出的节点结构基本一致,这意味着你为一个解析器写的分析逻辑,稍作调整就能用到另一个解析器上。
遍历AST:深度优先与访问者模式
拿到树之后,接下来要做的是遍历。AST最常用的遍历方式是深度优先,也就是从根节点出发,沿着一条路径一直走到叶子节点,再回头处理兄弟分支。Babel的traverse库把这个过程封装成了访问者模式:你声明一个对象,键是节点类型,值是处理函数,遍历器进入某个类型的节点时就自动调用对应的函数。
下面这段代码演示了如何统计代码中所有函数的名字:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const code = `
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
const result = add(1, multiply(2, 3));
`;
const ast = parser.parse(code);
traverse(ast, {
// 进入函数声明节点时触发
FunctionDeclaration(path) {
console.log('发现函数:', path.node.id.name);
},
// 进入函数调用表达式时触发
CallExpression(path) {
console.log('发现调用:', path.node.callee.name);
}
});
// 输出:
// 发现函数: add
// 发现函数: multiply
// 发现调用: multiply
// 发现调用: add注意处理函数接收的参数不是节点本身,而是一个path对象。这个设计很关键,path封装了当前节点和它的父节点、兄弟节点之间的关系,还提供了replaceWith、remove等修改方法。有了path,你不仅能读树,还能改树。比如把所有var声明改成let,只需要在VariableDeclaration的访问者里执行path.node.kind = 'let'即可。
访问者模式还支持进入和退出两个钩子,写成enter和exit。默认的处理函数是进入时调用,但有些场景必须用退出时机,比如需要先处理完所有子节点再处理父节点的场景,像计算表达式求值或者做作用域分析时,退出钩子能保证子树已经处理完毕。
实战:用AST实现代码转换
看完原理,动手做一个完整的小工具:把代码中所有的console.log调用删掉。这种需求在打包生产代码时很常见,虽然压缩工具也能做,但自己实现一遍能加深对整个流程的理解。完整代码如下:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const code = `
console.log('调试信息');
function calc() {
console.log('进入函数');
return 42;
}
console.log(calc());
`;
// 第一步:解析成AST
const ast = parser.parse(code);
// 第二步:遍历并修改
traverse(ast, {
CallExpression(path) {
// 判断是否是 console.log(...) 调用
const callee = path.node.callee;
if (
callee.type === 'MemberExpression' &&
callee.object.name === 'console' &&
callee.property.name === 'log'
) {
// 如果父节点是表达式语句,直接删除整条语句
if (path.parent.type === 'ExpressionStatement') {
path.parentPath.remove();
} else {
// 作为表达式使用时,替换成 undefined 避免语法错误
path.replaceWith({ type: 'Identifier', name: 'undefined' });
}
}
}
});
// 第三步:把AST重新生成代码
const output = generate(ast, {}, code);
console.log(output.code);
// 输出中所有 console.log 都被移除了,calc 函数正常保留这个例子把AST的完整工作流串了起来:解析、遍历、修改、生成。代码里有个容易忽略的坑:console.log作为独立语句时可以整行删除,但如果它出现在赋值右边或者作为参数传递,直接删会破坏语法结构,所以要分情况处理。这也体现了改写AST时的基本原则——任何修改都要保证树的合法性,不然生成阶段会直接报错。
如果你想更进一步,可以尝试实现更复杂的转换,比如把回调风格的异步函数转成async/await风格,或者检测项目中未被使用的变量做死代码提示。这些工具的套路完全一致,区别只在于访问者函数里写的判断逻辑。
常见误区与学习建议
初学者最容易犯的错,是把AST想得太神秘。本质上它就是一堆普通对象组成的嵌套结构,没有任何黑魔法。第二个常见误区是忽略作用域:比如想把变量a重命名为b,如果只遍历Identifier节点无脑替换,会把其他作用域里同名的a也改掉。正确做法是使用path.scope.rename,它会做作用域分析,只替换当前绑定范围内的引用。
关于工具选择,解析器除了Babel还有acorn(Webpack内置它)、espree(ESLint在用)、TypeScript编译器自带的解析器等。学习阶段推荐直接用Babel全家桶,文档完善、生态成熟。AST Explorer网站支持切换各种解析器实时查看结果,是调试转换逻辑的利器。掌握了AST,你再看各类前端工程化工具,视角会完全不同——它们不再是黑盒,而是你能亲手拆解、改造甚至重新发明的玩具。
JavaScript语法解析抽象语法树AST遍历修改时间:2026-09-07 15:40:55