抽象语法树(Abstract Syntax Tree)是实现js转译、CSS 预处理、代码压缩、ESLint、Prettier等的基石
AST是什么
是一种源代码的抽象结构的树形表示。树中的每个节点都表示源代码中出现的一个构造
AST的生成
词法分析
在词法分析阶段扫描输入的源代码字符串,生成一系列的词法单元,词法单元包括数字,标点符号,运算符等,且词法单元相互独立。(对代码语句进行拆分,不用考虑相互之间的联系)
Code: Hello('krystal')
--->
Tokenization
Token:[
{ type:'Identifer',value:'Hello' },
{ type:'Punctuator',value:'{' },
{ type:'String',value:'krystal' },
{ type:'Punctuator',value:'}' }
]
语法分析
语法分析会根据生成的token列表转换为AST,AST代表了代码语句中每一个片段以及他们之间的联系。AST是一个嵌套很深的对象,代表了代码本身,也能给到我们更多关于代码的信息。
Token:
[
{ type:'Identifer',value:'Hello' },
{ type:'Punctuator',value:'{' },
{ type:'String',value:'krystal' },
{ type:'Punctuator',value:'}' }
]
---> AST:{
"program"{
"type": "Program",
"body": {
"type":"ExpressionStatement",
"expression": {
"type": "CallExpression", //调用表达式
"callee": {
"type":"Identifier",
"name": "Hello"
},
"arguments": {
"type": "StringLiteral"
"value": "krystal"
}
}
}
}
}
代码生成
首先需要经历转换,生成新的AST,这个阶段仅仅是将上个阶段生成的AST进行修改。转换AST的过程中可以对节点进行添加、移除、替换等操作,仅仅是在旧有的AST基础上创建一个全新的AST
old AST ---> new AST:{
"program":{
"type":"Program",
"body":{
"type": "CallExpression",
"name": "Hello",
"params": {
"type": "StringLiteral",
"value": "krystal"
}
}
}
}
接下来经历的是对AST树的遍历,遍历的过程会议DFS的方式到达每个节点。
- Program -从AST的顶层开始
- CallExpression -移动到program列表的第一个元素
- CallExpression(Hello) -移动到callExpression的params列表的第一个元素
针对这里的对树的遍历,会产生一个访问的概念,也就是一步一步的对对象结构的元素进行 操作。需要创建一个访问器对象,来提供接受不同节点类型的方法。
根据我举的例子,仅涉及到StringLiteral,CallExpression。每当遇到对应的匹配的节点时,便会调用访问器对应的方法。
var visitor = { StringLiteral(node,parent) {} CallExpression(node, parent) {} }
访问的同时也需要存在退出的可能性,所以根据所举的例子,最终的访问流程是这样的
-> Program(enter)
->CallExpression(enter)
->StringLiteral(enter)
<-StringLiteral(exit)
<-CallExpression(exit)
<- Program(exit)
最终的访问器应该设计成以下形式
var visitor = {
StringLiteral: {
enter(node,parent){}
exit(node,parent){}
}
CallExpression: {
enter(node,parent){}
exit(node,parent){} }
}
生成代码
代码生成器会递归调用AST对象上的所有嵌套节点,直到所有内容都被打印到一个常常的代码字符串中。
AST的基本结构
js编译器的通用规范 --ESTree中对于AST结构的基本定义
AST的使用场景
- 代码高亮、格式化、错误提示、自动补全
- 代码压缩: uglifyJS
- 代码转译: webpack、babel、TS