本文首发于 hzzly的博客javascript
原文连接: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.vue
AST是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。java
AST是一个很是基础可是同时很是重要的知识点,咱们熟知的 TypeScript、babel、webpack、vue-cli 都是依赖 AST 进行开发的。node
这里咱们就以 babel 为例来实践一下 AST。webpack
Babel 做为当今最为经常使用的 JavaScript 编译器,在前端开发中扮演着极为重要的角色。大多数状况下,Babel 被用来转译 ECMAScript 2015+ 至可兼容浏览器的版本。web
Babel 的三个主要处理步骤分别是:vue-cli
整个过程当中,parsing和generation是固定不变的,最关键的是transforming步骤,经过babel插件来支持,这是其扩展性的关键。npm
这三个阶段分别由 @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 树输出为转码后的代码字符串。
说了这么多接下来咱们就用代码实践一下上面的例子
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,你真的能够作不少事情。