一、什么是 AST
抽象语法树(Abstract Syntax Tree)简称 AST,是源代码的抽象语法结构的树状表现形式。webpack、eslint 等很多工具库的核心都是通过抽象语法树这个概念来实现对代码的检查、分析等操作。
像我们常用的浏览器就是通过将 js 代码转化为抽象语法树来进行下一步的分析等其他操作。所以将 js 转化为抽象语法树更利于程序的分析。
如上图中变量声明语句,转换为 AST 之后就是右图中显示的样式
左图中对应的:
- var 是一个关键字
- AST 是一个定义者
- is tree 是一个字符串
- ; 是 Semicoion
首先一段代码转换成的抽象语法树是一个对象,该对象会有一个顶级的 type 属性 Program;第二个属性是 body 是一个数组。
body 数组中存放的每一项都是一个对象,里面包含了所有的对于该语句的描述信息
- type:描述该语句的类型 -->变量声明的语句
- kind:变量声明的关键字 -->var
- declarations:声明内容的数组,里面每一项也是一个对象
- type:描述该语句的类型
- id:描述变流量名称的对象
- type:定义
- name:变量的名字
- init:初始化变量值的对象
- type:类型
- value:值 “is value”不带引号
- row:""is tree"" 带引号
二、词法分析和语法分析
JavaScript 是解释型语言,一般通过 -->词法分析-->语法分析-->语法树,就可以开始解释执行了
2.1 词法分析
也叫扫描,是将字符串流转换为记号流(kokens),他会读取我们的代码然后按照一定的规则合成一个个的标识,比如说:const a = 2,这段代码通常会被分解成 const、a、=、2
[
{ type: "Keyword", value: "const" },
{ type: "Identifier", value: "a" },
{ type: "Punctuator", value: "=" },
{ type: "Numeric", value: "2" },
];
当词法分析源代码的时候,他会一个一个字符的读取代码,所以很形象的称之为扫描- scans。当他遇到空格、操作符,或者特殊符号的时候,他会认为一个话已经完成了。
2.2 语法分析
也成解析器,将词法分析出来的数组转换成树的形式,同时验证语法。语法如果有错的话,抛出语法错误。
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 2,
"raw": "2"
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}
三、AST 能做什么
语法检查、代码风格检查、格式化代码、语法高亮、错误提示、自动补全等
代码混淆压缩
优化变更代码,改变代码结构等等
3.1插件工具推荐
比如说,有个函数 function a(){} 我想把它变成 function b(){} ,在 webpack 中代码编译完成后 require('a') -->_ _webpack__require("*/**/a.js")
可以使用的插件工具
- esprima:code=>AST 代码转 AST
- estraverse:traverse AST 转换树
- escodegen:AST=>code
使用示例:
const esprima = require("esprima");
const estraverse = require("estraverse");
const code = `function getUser(){}`;
// 生成AST
const ast = esprima.parseScript(code);
// 转换 AST,只会遍历 type 属性
// traverse 方法中有进入和离开两个钩子函数
estraverse.traverse(ast, {
enter(node) {
console.log("enter -> node.type", node.type);
},
leave(node) {
console.log("leave -> node.type", node.type);
},
});
/*
enter -> node.type Program
enter -> node.type FunctionDeclaration
enter -> node.type Identifier
leave -> node.type Identifier
enter -> node.type BlockStatement
leave -> node.type BlockStatement
leave -> node.type FunctionDeclaration
leave -> node.type Program
*/
3.2 修改函数的名字
语法解析
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "getUser"
},
"params": [],
"body": {
"type": "BlockStatement",
"body": []
},
"generator": false,
"expression": false,
"async": false
}
],
"sourceType": "script"
}
根据语法解析后,可以看到 type 为 Identifier 的时候就是该函数的名字,我们可以直接修改它便可实现一个更改函数名字的 AST 工具。
实现:
// 转换树
estraverse.traverse(ast, {
// 进入离开修改都是可以的
enter(node) {
console.log("enter -> node.type", node.type);
if (node.type === "Identifier") {
node.name = "hello";
}
},
leave(node) {
console.log("leave -> node.type", node.type);
},
});
// 生成新的代码
const result = escodegen.generate(ast);
console.log(result);
// function hello() {}