小引
为什么要谈AST(抽象语法树)?
如果你查看目前任何主流的项目中的devDependencies,会发现前些年的不计其数的插件诞生。我们归纳一下有:javascript转译、代码压缩、css预处理器、elint、pretiier,等。有很多js模块我们不会在生产环境用到,但是它们在我们的开发过程中充当着重要的角色。熟知的 TypeScript、babel、webpack、vue-cli 得都是依赖 AST 进行开发的。所有的上述工具,不管怎样,都建立在了AST基础上。
在前端的大部分实际应用中,其实也并不需要用到太多原理方面的基础知识。工具都帮你解析好 AST 了,操作完也是工具负责生成代码。使用时更多的还是需要去关心 AST 的结构和操作方式,梳理实际使用的思路。不同的解析器可能有不同的抽象语法树格式。比如eslint使用了espree、babel使用了Babylon等。推荐一个网站,把代码贴进去就可以在浏览器里看各种解析器生成的 AST:astexplorer
什么是抽象语法树
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。
我们先看个例子
const a = 1
传统编译语言中,源代码执行会先经历三个阶段
-
词法分析阶段:将字符组成的字符串分解成一个个代码块(词法单元),例子中代码会被解析成 const、a、=、1 四个词法单元。
-
语法分析阶段:将词法单元流转换成一个由元素逐级嵌套组成的语法结构树,即所谓的抽象语法树。例子中被解析出来的 const、a、=、1 这四个词法单元组成的词法单元流则会被转换成如下结构树
- 代码生成阶段:将 AST 转换成一系列可执行的机器指令代码,对应例子的话就是机器通过执行指令会在内存中创建一个变量 a,并将值 1 赋值给它。
再看一个例子
function add (a, b) { return a + b }
首先,进入到词法分析阶段,我们会拿到 function、add、(、a、,、b、)、{、return、a、+、b、} 13 个代码块
然后进入语法分析阶段,具体如图所示
上图中的 FunctionDeclaration、Identifier、BlockStatement 等这些代码块的类型的说明请点击链接自行查看:AST 对象文档
AST螺丝刀:recast
npm i recast -S
接下来,你可以在任意js文件下这把螺丝刀,新建recast.js文件
recast.parse
输入node recast.js你可以查看到add函数的结构,与之前所述一致,通过AST对象文档可查到它的具体属性:
FunctionDeclaration { type: 'FunctionDeclaration', id: Identifier..., params: [Identifier...], body: BlockStatement... }
你也可以继续使用console.log透视它的更内层,如: console.log(add.params[0]) console.log(add.body.body[0].argument.left) 有没有觉得很熟悉??又是一棵树的结构
recast.types.builders
学会了拆零件还不够,还得会组装
官网例子
If you choose to use recast.builders to construct new AST nodes, all builder
arguments will be dynamically type-checked against the Mozilla Parser API.
如果选择使用recast.builders构造新的AST节点,则所有生成器参数将根据Mozilla解析器API进行动态类型检查。 const b = recast.types.builders;
This kind of manipulation should seem familiar if you've used Esprima or the Mozilla Parser API before.
ast.program.body[0] = b.variableDeclaration("var", [
b.variableDeclarator(add.id, b.functionExpression(
null, // Anonymize the function expression.
add.params,
add.body
))
]);
这里我们想把之前function add(a, b){...}函数声明,改成函数表达式声明const add = function(a ,b){...}
第一步,我们创建一个VariableDeclaration变量声明对象,声明头为const, 内容为一个即将创建的VariableDeclarator对象。
第二步,创建一个VariableDeclarator,放置add.id在左边, 右边是将创建的FunctionDeclaration对象
第三步,我们创建一个FunctionDeclaration,如前所述的三个组件,id params body中,因为是匿名函数id设为空,params使用add.params,body使用add.body。
这样,就创建好了const add = function(){}的AST对象。
可以看到,我们打印出了
const add = function(a, b) {
return a +
b
};
最后一行
const output = recast.print(ast).code; 其实是recast.parse的逆向过程,具体公式为 recast.print(recast.parse(source)).code === source
我们其实也可以打印出美化格式的代码段:
const output = recast.prettyPrint(ast, { tabWidth: 2 }).code
执行 node recast.js ,会发现 code 里面的 N 多空格都能被格式化掉,输出如下
function add(a, b) {
return a + b;
}
再来一个例子 将其直接改成 const add = (a, b) => {...} 的箭头函数格式。 当然,recast.type.builders 提供了 arrowFunctionExpression 来允许我们创建一个箭头函数。所以我们第一步先来创建一个箭头函数 const arrow = arrowFunctionExpression([], blockStatement([]) 打印下 console.log(recast.print(arrow)),输出如下 PrintResult { code: '() => {}' }
OK,我们已经获取到一个空的箭头函数了。接下来我们需要基于上面改造的基础进一步进行改造,其实只要将 functionExpression 替换成 arrowFunctionExpression 即可。
除了parse/print/builder以外,Recast的三项主要功能:
run: 通过命令行读取js文件,并转化成ast以供处理。
tnt: 通过assert()和check(),可以验证ast对象的类型。
visit: 遍历ast树,获取有效的AST对象并进行更改。
用AST修改源码
总结
AST 它的用处还非常的多,比如我们熟知的 Vue,它的 SFC(.vue) 文件的解析也是基于 AST 去进行自动解析的,即 vue-loader,它保证我们能正常的使用 Vue 进行业务开发。再比如我们常用的 webpack 构建工具,也是基于 AST 为我们提供了合并、打包、构建优化等非常实用的功能的。
总之,掌握好 AST,你真的可以做很多事情。
文章只是初导篇,更多的有关AST的细节还得大家去研究。