本文尝试以一个最简单的例子来展现抽象语法树的魔力。
主要从六方面阐述:
- 抽象语法树简介
- 代码执行的三个步骤
- 词法分析
- 语法分析
- 一个简单的小例子
- 抽象语法树的应用
抽象语法树的简介
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
上面这是百度百科的定义,一如既往的让人摸不着头脑。
我们总结一下百度百科的定义:
- 抽象语法树是一颗树
- 树的每个节点都表示了源代码的一种语法结构
上面这段总结有几个关键词:抽象语法树、树、节点、源代码、语法结构,其中语法结构是比较难理解的,那么什么是语法结构呢?
举几个简单的例子:
//变量声明
var a = 1;
//循环
while(true){
console.log(1);
}
//判断
if(true){
console.log(1);
}
//函数声明
function a(){
console.log(1);
}
以上这些例子是js的语法声明(statement),这种声明就可以看成js的语法结构! 也就是说抽象语法树的每个节点都在描述这种结构。
比如说一个节点是变量声明,那么这个节点的子节点都会去描述变量声明的具体内容:变量名是什么,变量是什么类型,变量的初始值是什么等等。
就是这样一个一个的声明,构成了抽象语法树。
代码执行的三个步骤
从js程序到机器可执行的机器码需要经历两个阶段:
- 语法检查
- 编译运行
语法检查又分为语法分析和词法分析,所以分成三个步骤就是:
- 词法分析
- 语法分析
- 编译运行
这里先简单介绍下每个阶段都干了什么活:
第一步:词法分析,也叫做扫描scanner。它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识Tokens(type 和 value )。这个阶段,它会移除空白符,注释等。最后,整个代码将被分割进一个Tokens列表(一个一维数组)。
第二步:语法分析,也叫做解析器。它会将词法分析出来的Token数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。
第三步:编译阶段,也叫编译器。这个阶段会处理AST,生成机器可执行的机械码。
词法分析
先以一个简单的例子看下token序列长什么样
var a = 1;
他的token长这样
其实就是一个一维数组,里面有一些对象用于描述每个单词。
我整理了下常见的type:
- Keyword (关键词)
- Identifier (标识符)
- Punctuator (标点符号)
- Numberic(数字)
- String (字符串)
- Boolean(布尔)
- Null(空值)
语法分析
词法分析由源代码生成了Token序列,语法分析则是由Token序列生成了抽象语法树。
还是看上一个例子:
var a = 1;
他的抽象语法树长这样:
1、先看最外层的三个属性
- type(表示是一段程序代码)
- body(代码的具体内容)
- sourceType(表示语言的种类)
2、再看body里面的具体内容,body是一个数组,这是因为程序可能有多个内容块(statement),每个内容块用一个对象表示。
3、再看每个内容块的内容
- type(表示这个内容块是干什么的)
- declarations(乘装变量内容的块,可以看到这个块也是一个数组,因为变量声明可能生命多个,所以一个生命对应一个对象 例如 var a=1,b=2;) kind(关键字)
4、再看declarations里面对象里面的内容
- type (声明的类型是个变量)
- id(表示变量名)
- init(表示为这个变量设置的初值)
上面提到statement,statement有很多类型,比如说变量声明,函数定义,if语句,while循环,等都是一个statement,大家如果想看更多的类型,点击这里。
一个超级简单的例子
好了,说了这么多,终于要写代码了。
这个例子实现的功能:
- 将源代码中的 == 变成 ===
- 将源代码中的 var 变成 let
- 将源代码中的 console注释掉
这个例子用到的工具:
- Esprima (将源代码转化为ast)
- Estraverse(遍历语法树)
- Escodegen(讲语法书反编译为js代码)
初始化一个项目
npm init
安装用到的依赖包
npm install esprima estraverse escodegen --save
新建index.js入口文件 和 originCode.js 源代码文件
在 originCode.js 中输入要转换的源代码
function fun() {
var opt = 1;
console.log(1);
if (opt == 1) {
console.log(2);
}
}
在 index.js 中实现我们的功能
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);
}
}
抽象语法树的应用
通过上面这个例子可以看出,抽象语法树具有改变源代码的魔力,这样的话抽象语法树的应用就不难总结了。
- IDE插件,用来检查语法,高亮语法等功能。
- 代码的混淆压缩,比如UglifyJS2。
- 代码转换工具,比如Webpack、Babel,或者ts转js等各种代码规范之间转换的工具。