什么是AST?
借用一下百度百科的解释:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
其实意思就是将代码以树的格式展现出来,举个例子你就明白了:
大家可以在这个网站 https://astexplorer.net/ 上输入代码,会被解析为 AST,如图:
可以看到,代码的每一个字符都以树的形式展示了出来。
AST的应用场景
常见的场景:
- IDE的错误提示、代码格式化、代码高亮、代码自动补全等
- JSLint、JSHint对代码错误或风格的检查等
- webpack、rollup进行代码打包等
- CoffeeScript、TypeScript、JSX等转化为原生Javascript、babel把es6转换成es5语法
代码执行的步骤
从js程序到机器可执行的机器码需要经历两个阶段:
- 语法检查(源代码 -> 抽象语法树AST)
- 词法分析(源代码 -> Token序列)
- 语法分析(Token序列 -> 抽象语法树AST)
- 编译运行(抽象语法树AST -> 机器码)
1、词法分析
源代码 -> Token序列
将整个代码字符串分割成最小语法单元数组,也就是一个个的标识Tokens(type 和 value )。
https://esprima.org/demo/parse.html#
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "a"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "1"
},
{
"type": "Punctuator",
"value": ";"
}
]
常见的type:
Keyword (关键词)
Identifier (标识符)
Punctuator (标点符号)
Numberic(数字)
String (字符串)
Boolean(布尔)
Null(空值)
2、语法分析
Token序列 -> 抽象语法树
{
// 表示是一段程序代码
"type": "Program",
// 代码的具体内容
"body": [
{
// 表示这个内容块是干什么的
"type": "VariableDeclaration",
// 装变量内容的块
"declarations": [
{
// 声明的类型是个变量
"type": "VariableDeclarator",
// 表示变量名
"id": {
"type": "Identifier",
"name": "a"
},
// 表示为这个变量设置的初值
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "var"
}
],
// 表示语言的种类
"sourceType": "script"
}
3、编译运行
这个阶段会处理AST,生成机器可执行的机械码。
举个例子
上面讲了 AST 可以对代码进行转换,那么我们举个例子详细说明一下。
比如我们要实现:
- 将源代码中的
==
变成===
- 将源代码中的 var 变成 let
- 将源代码中的 console注释掉
这个例子用到的工具:
- esprima(源代码 -> 抽象语法树AST)
- estraverse(遍历AST)
- escodegen(将AST反编译为js代码)
originCode.js 为要转换的源代码
function fun() {
var opt = 1;
console.log(1);
if (opt == 1) {
console.log(2);
}
}
index.js 是我们要执行的 node 文件
let fs = require('fs');
const esprima = require('esprima');//将JS代码转化为语法树模块
const estraverse = require('estraverse');//JS语法树遍历各节点
const escodegen = require('escodegen');//将JS语法树反编译成js代码模块
/**
* 由源代码得到抽象语法树
*/
function getAst(jsFile) {
let jsCode;
return new Promise((resolve)=>{
fs.readFile(jsFile, (error, data) => {
jsCode = data.toString();
resolve(esprima.parseScript(jsCode));
});
});
}
/**
* 设置全等
*/
function setEqual(node) {
if (node.operator === '==') {
node.operator = '===';
}
}
/**
* 删除console
*/
function delConsole(node) {
if (node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' && node.expression.callee.object.name==='console') {
node.expression.callee.object.name = '//console';
}
}
/**
* 把var变成let
*/
function setLet(node){
if(node.kind === 'var'){
node.kind = 'let';
}
}
/**
* 遍历语法树
*/
function travel(ast){
estraverse.traverse(ast, {
enter: (node) => {
setEqual(node);
setLet(node);
delConsole(node);
}
});
}
/**
* 生成文件
*/
function writeCode(file,data) {
fs.writeFile(file,data,(error)=>{
console.log(error);
});
}
/**
* 入口函数
*/
function main(){
let file = './originCode.js';
let distFile = './distCode.js';
getAst(file).then(function(jsCode) {
travel(jsCode);
// 删掉 console , 通过parseScript在生成一变ast去掉注释的内容
// let distCode = escodegen.generate( esprima.parseScript( escodegen.generate(jsCode)));
// 注释 console
let distCode = escodegen.generate(jsCode);
console.log('distcode',distCode);
writeCode(distFile,distCode);
});
}
main();
执行
node index.js
得到的结果 distCode.js
function fun() {
let opt = 1;
//console.log(1);
if (opt === 1) {
//console.log(2);
}
}
总结
再看一遍这个流程:
源代码 -> 抽象语法树AST -> 机器码/新的js
从上面的这个例子可以看出,我们可以通过修改 AST,从而修改成我们想要的结果,那么像IDE代码高亮、代码混淆压缩、Babel es6转es5、ts转js等等就很好理解了。
babel原理
babel 原理也是 AST,babel 有许多工具可以直接使用,如下:
babel把es6转换成es5语法的原理?
- 解析(parse)。将源代码解析变成AST。@babel/parser模块
- 转换(transform)。对AST 进行遍历,在此过程中对节点进行添加、更新及移除等操作。因此这是bebel处理代码的核心步骤,是我们的讨论重点,主要使用@babel/traverse和@babel/types模块。
- 生成(generate)。将更改后的AST,再变回代码。@babel/generate模块。
举例:将 var 变为 let
const generator = require('@babel/generator');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const transToLet = code => {
const ast = parser.parse(code);
// 访问者对象
const visitor = {
// 遍历声明表达式
VariableDeclaration(path) {
if (path.node.type === 'VariableDeclaration') {
// 替换
if (path.node.kind === 'var') {
path.node.kind = 'let';
}
}
},
};
traverse.default(ast, visitor);
// 生成代码
const newCode = generator.default(ast, {}, code).code;
return newCode;
};
const code = `const a = 1
var b = 2
let c = 3`;
transToLet(code);
结果
const a = 1;
let b = 2;
let c = 3;