什么是抽象语法树
wiki:
抽象语法树( Abstract Syntax Tree,AST ),或简称语法树( Syntax tree ),是源代码语法结构的一种抽象表示。
它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
在我们前端,可以通过 Javascript
解析器将我们程序的源代码映射成为一棵语法树,而树的每个节点对应着代码里的一种结构;比如表达式,声明语句,赋值语句等都会被映射为语法树上的一个节点,进而我们就可以通过操作语法树上的节点来控制我们的源代码;初步了解时可能感觉这个概念本身就比较抽象,但是基于它的应用我们却一点都不陌生,比如:单文件组件 .vue
文件的解析、为我们转码 ES6
语法的 babeljs
、为我们压缩混淆代码的 UglifyJS
等等;
亮剑 Babeljs
Babel is a JavaScript compiler. Use next generation JavaScript, today.
Babel
是一个Javascript
编译器,他能让你现在就开始使用未来版的Javascript
。曾经的Javascript
由于语言本身的设计缺陷,饱受程序员们的诟病,如今随着ES
语言规范的制定与发展,加上Typescript
的横空出世,Javascript
开始逐年霸占最流行语言的榜首;Babel
是推动这一进程的重要推手,他能将ES6/7/8/9/10
转换为浏览器能够兼容的Javascript
,从现在开始,您就可以使用最新的ES
语法规范,甚至处于草案阶段的规范;而这都是一切都是前所未有的!
接下来我们将使用Babel
提供的相关AST
操作的模块来“改头换面”我们的代码;
-
@babel/parser
通过该模块来解析我们的代码生成AST
抽象语法树; -
@babel/traverse
通过该模块对AST
节点进行递归遍历; -
@babel/types
通过该模块对具体的AST
节点进行进行增、删、改、查; -
@babel/generator
通过该模块可以将修改后的AST
生成新的代码;
无中生有
Talk is cheap, show me your code!
现在假设我们在代码里使用了一个叫log
的方法,如:log('Hello, world!');
,但是我们并没有声明或定义该方法,一般情况下我们的代码将会报一个log is not defined
的错误,现在我们通过操作AST
将log
修改为console.log
,这样我们的代码就不会报错了;
在astexplorer.net
网站里我们可以在线将代码转为AST
,如下图:log('Hello, world!')
的AST
树形结构图;
一般情况下,我们从body
层级看起,其下的每一层级都有一个type
字段,该字段非常重要,直接影响我们该如何对该语句或表达式进行操作,具体请看后面讲到的@babel/types
;
拿到代码,我们首先通过@babel/parser
生成如上图所示结构的AST
:
const {parse} = require('@babel/parser');
const codes = "log('Hello, world!');";
const ast = parse(codes, {
sourceType: "module"
});
对照AST
分析我们需要做什么,新手强烈推荐使用astexplorer.net
,比如在这里,我们的需求是将log
函数转换为console.log
,通过在线AST
解析,我们清楚的看到了log
对应的AST
节点类型为Identifier
,同样的,我们换成console.log('Hello, world!');
,可以看到console.log
对应的AST
节点类型为MemberExpression
,所以,我们的需求变为将此处的Identifier
变为MemberExpression
,不要想当然的以为直接把type
属性改个值就 ok 了,接下来看看,如何将Identifier
类型的节点修改为MemberExpression
类型的节点;
轮到@babel/types
上场了,前面两步,我们的焦点主要在AST
节点的type
字段上,事实上,每一个type
在@babel/types
里都有一个同名的方法(首字母小写)用来创建该类型的节点,比如创建Identifier
类型的节点,我们可以使用t.identifier
方法;
好了,先来创建MemberExpression
类型的console.log
,此处对于新手会比较棘手,推荐好好观察在线的AST
树进行反推,找到目标节点的type
,接着在@babel/types
文档里搜相应type
的方法;如图,我们在文档里搜到memberExpression
方法的定义,接着开始创建console.log
节点;
const t = require('@babel/types');
function createMemberExpression() {
return t.memberExpression(
t.identifier('console'),
t.identifier('log')
);
}
如上,我们就可以通过createMemberExpression
方法来生成console.log
来替换log
了,问题来了,如何替换?
当然,替换之前,我们需要在AST
树上找到对应的节点,通过@babel/traverse
我们可以对AST
树的节点进行遍历;
const {default: traverse} = require('@babel/traverse');
traverse(ast, visitor);
visitor
是一个由各种type
或者是enter
和exit
组成的对象,由此确定在遍历的过程中匹配到某种类型的节点后该如何操作,如我们的需求是将Identifier
类型的log
节点替换为MemberExpression
类型的console.log
,我们可以这样定义visitor
:
const visitor = {
Identifier(path) {
const {node} = path;
if(node && node.name === 'log') {
path.replaceWith(createMemberExpression());
path.stop();
}
}
}
通过traverse
方法我们可以定义各种类型节点的操作方式,回调函数的path
参数提供了丰富的增、删、改、查以及类型断言的方法,比如replaceWith/remove/find/isMemberExpression
;
最后,我们将修改后的AST
转换为Javascript
代码:
const {code} = generate(ast, { /* options */ }, codes);
到这一步,我们就已经可以将代码里的log
方法替换为console.log
了;举一反三,我们是不是可以放开一下想象力:自己定义某种有意思或者创造性的语法规范,然后通过AST
操作变换成常规的Javascript
;
以上例子的代码汇总为:
const t = require('@babel/types');
const {parse} = require('@babel/parser');
const {default: traverse} = require('@babel/traverse');
const {default: generate} = require('@babel/generator');
const codes = "log('Hello, world!');";
const ast = parse(codes, {
sourceType: "module"
});
const visitor = {
Identifier(path) {
const {node} = path;
if(node && node.name === 'log') {
path.replaceWith(createMemberExpression());
path.stop();
}
}
}
traverse(ast, visitor);
const {code} = generate(ast, { /* options */ }, codes);
console.log(code);
// console.log('Hello, world!');
function createMemberExpression() {
return t.memberExpression(
t.identifier('console'),
t.identifier('log')
);
}
总结
以上, 通过babeljs
提供的相关模块对AST
操作进行了初步的实践; 2019 年已过一半,今年无疑跨端应用火了,比如,Taro、uni-app 等,而这些框架的成功统统离不开的就是对AST
的熟练掌握;所以呢,还等什么,现在上车还来得及,如果你对前端报有天马行空的想象,那么我想了解AST
将会助你一臂之力!