本文首发于 hzzly的博客
原文链接:AST的简单实践
什么是AST(抽象语法树)?
It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.
AST是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
AST是一个非常基础但是同时非常重要的知识点,我们熟知的 TypeScript、babel、webpack、vue-cli 都是依赖 AST 进行开发的。
这里我们就以 babel 为例来实践一下 AST。
Babel运行原理
Babel 作为当今最为常用的 JavaScript 编译器,在前端开发中扮演着极为重要的角色。大多数情况下,Babel 被用来转译 ECMAScript 2015+ 至可兼容浏览器的版本。
Babel 的三个主要处理步骤分别是:
- 解析(parse)
- 转换(transform)
- 生成(generate)

整个过程中,parsing和generation是固定不变的,最关键的是transforming步骤,通过babel插件来支持,这是其扩展性的关键。
这三个阶段分别由 @babel/parser、@babel/core、@babel/generator 执行。Babel 本质上只是一个代码的搬运工,如果不给 Babel 装上插件,它将会把输入的代码原封不动地输出。正是因为有插件的存在, Babel 才能将输入的代码进行转变,从而生成新的代码。
解析
输入JS源码,输出AST
parsing(解析),对应于编译器的词法分析,及语法分析阶段。输入的源码字符序列经过词法分析,生成具有词法意义的token序列(能够区分出关键字、数值、标点符号等),接着经过语法分析,生成具有语法意义的AST(能够区分出语句块、注释、变量声明、函数参数等)。
利用 @babel/parser 对源代码进行解析 得到 AST。
栗如:
console.log(info)
经过parsing后,生成的AST如下:
{
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"loc": {
"identifierName": "console",
},
"name": "console",
},
"property": {
"type": "Identifier",
"loc": {
"identifierName": "log",
},
"name": "log",
}
},
"arguments": [
"Identifier": {
"type": "Identifier",
"loc": {
"identifierName": "log",
},
"name": "info",
}
]
}
🔥Tip: JS代码对应的AST结构可以通过AST Explorer工具查看
仔细的小伙伴可能就会发现从我们的源代码到AST的过程其实就是一个分词的过程,将我们的 console.log(info) 分成 console、log、info。
有了这个 AST 树结构,我们就能进行语义层面转换了。
转换
输入AST,输出修改过的AST
利用 @babel/traverse 对 AST 进行遍历,并解析出整个树的 path,通过挂载的 metadataVisitor 读取对应的元信息,这一步叫 set AST 过程。
@babel/traverse 是一款用来自动遍历抽象语法树的工具,它会访问树中的所有节点,在进入每个节点时触发 enter 钩子函数,退出每个节点时触发 exit 钩子函数。开发者可在钩子函数中对 AST 进行修改。
import traverse from "@babel/traverse";
traverse(ast, {
enter(path) {
// 进入 path 后触发
},
exit(path) {
// 退出 path 前触发
},
});
transforming(转换),对应于编译器的机器无关代码优化阶段(稍微有点牵强,但二者工作内容都是修改AST),对 AST 做一些修改,比如针对上面的 log 增加一些信息方便我们调试:
console.log(info) => console.log('[info]', info)
修改过后的 AST 结构:
{
"type": "CallExpression",
"callee": {
// ....
},
"arguments": [
"StringLiteral": {
"type": "StringLiteral",
"value": "'[info]'",
},
"Identifier": {
"type": "Identifier",
"loc": {
"identifierName": "log",
},
"name": "info",
}
]
}
语义层面的转换具体而言就是对AST进行增、删、改操作,修改后的AST可能具有不同的语义,映射回代码字符串也不同
生成
输入AST,输出JS源码
generation(生成),对应于编译器的代码生成阶段,把AST映射回代码字符串。
利用 @babel/generator 将 AST 树输出为转码后的代码字符串。
实践
说了这么多接下来我们就用代码实践一下上面的例子
相关npm包
- @babel/parser 解析输入源码,创建AST
- @babel/traverse 遍历操作AST
- @babel/generator 把AST转回JS代码
- @babel/types AST操作工具库
代码
const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const generate = require('@babel/generator');
const t = require('@babel/types');
function compile(code) {
// 1. parse
const ast = parser.parse(code);
// 2. traverse
const visitor = {
CallExpression(path) {
const { callee, arguments } = path.node;
if (
t.isMemberExpression(callee)
&& callee.object.name === 'console'
&& callee.property.name === 'log'
&& arguments.length > 0
) {
const variableName = arguments[0].name;
path.node.arguments.unshift(
t.StringLiteral(`[${variableName}]`)
)
}
},
};
traverse.default(ast, visitor);
// 3. generate
return generate.default(ast, {}, code);
}
const code = `console.log(info)`;
const result = compile(code);
console.log(result.code);
总结
看到这,我们的 AST 实践也告一段落了。当然,文章所讲的只是一个简单的例子,但基本的原理思路八九不离十,更多的类型还得自己去探究。总之,掌握好 AST,你真的可以做很多事情。