一、什么是抽象语法树
在计算机科学中,抽象语法树(abstract syntax tree
或者缩写为 AST
),或者语法树(syntax tree
),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。
之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
二、使用场景
- JS 反编译,语法解析
- Babel 编译 ES6 语法
- 代码高亮
- 关键字匹配
- 作用域判断
- 代码压缩
如果你是一名前端开发,一定用过或者听过babel、eslint、prettier等工具,它们对静态代码进行翻译、格式化、代码检查,在我们的日常开发中扮演了重要的角色,这些工具无一例外的应用了AST。 前端开发中依赖的AST工具集合
这里不得不拉出来介绍一下的是Babel,从ECMAScript的诞生后,它便充当了代码和运行环境的翻译官,让我们随心所欲的使用js的新语法进行代码编写。
-
那么Babel是怎么进行代码翻译的呢?
如下图所示,Babylon首先解析(parse)阶段会生成AST,然后babel-transform对AST进行变换(transform),最后使用babel-generate生成目标代码(generate)。
babel
我们用一个小例子来看一下,例如我们想把
const nokk = 5;
中的变量标识符nokk逆序, 变成const kkon = 5
Step Parse
1234567const
babylon = require(
'babylon'
)
const
code = `
const
nokk = 5;
`
const
ast = babylon.parse(code)
console.log(
'%o'
, ast)
Step Transform
123456789101112const
traverse = require(
'@babel/traverse'
).
default
traverse(ast, {
enter(path) {
if
(path.node.type ===
'Identifier'
) {
path.node.name = path.node.name
.split(
''
)
.reverse()
.
join
(
''
)
}
}
})
Step Generate
123456const
generator = require(
'@babel/generator'
).
default
const
targetCode = generator(ast)
console.log(targetCode)
// { code: 'const kkon = "water";', map: null, rawMappings: null }
1 const babel = require('babel-core'); //babel核心解析库 2 const t = require('babel-types'); //babel类型转化库 3 let code = `let sum = (a, b)=> a+b`; 4 let ArrowPlugins = { 5 //访问者模式 6 visitor: { 7 //捕获匹配的API 8 ArrowFunctionExpression(path) { 9 let { node } = path; 10 let params = node.params; 11 let body = node.body; 12 if(!t.isBlockStatement(body)){ 13 let returnStatement = t.returnStatement(body); 14 body = t.blockStatement([returnStatement]); 15 } 16 let r = t.functionExpression(null, params, body, false, false); 17 path.replaceWith(r); 18 } 19 } 20 } 21 let d = babel.transform(code, { 22 plugins: [ 23 ArrowPlugins 24 ] 25 }) 26 console.log(d.code);
看看输出结果:
1 let sum = function (a, b) { 2 return a + b; 3 };
三、AST Explorer
四、深入原理
可视化的工具可以让我们迅速有感官认识,那么具体内部是如何实现的呢?
继续使用上文的例子:
1 Function getAST(){}
JSON
也很简单:1 { 2 "type": "Program", 3 "start": 0, 4 "end": 19, 5 "body": [ 6 { 7 "type": "FunctionDeclaration", 8 "start": 0, 9 "end": 19, 10 "id": { 11 "type": "Identifier", 12 "start": 9, 13 "end": 15, 14 "name": "getAST" 15 }, 16 "expression": false, 17 "generator": false, 18 "params": [], 19 "body": { 20 "type": "BlockStatement", 21 "start": 17, 22 "end": 19, 23 "body": [] 24 } 25 } 26 ], 27 "sourceType": "module" 28 }
怀着好奇的心态,我们来模拟一下用代码实现:
1 const esprima = require('esprima'); //解析js的语法的包 2 const estraverse = require('estraverse'); //遍历树的包 3 const escodegen = require('escodegen'); //生成新的树的包 4 let code = `function getAST(){}`; 5 //解析js的语法 6 let tree = esprima.parseScript(code); 7 //遍历树 8 estraverse.traverse(tree, { 9 enter(node) { 10 console.log('enter: ' + node.type); 11 }, 12 leave(node) { 13 console.log('leave: ' + node.type); 14 } 15 }); 16 //生成新的树 17 let r = escodegen.generate(tree); 18 console.log(r);
运行后,输出:
1 enter: Program 2 enter: FunctionDeclaration 3 enter: Identifier 4 leave: Identifier 5 enter: BlockStatement 6 leave: BlockStatement 7 leave: FunctionDeclaration 8 leave: Program 9 function getAST() { 10 }
我们看到了遍历语法树的过程,这里应该是深度优先遍历。
稍作修改,我们来改变函数的名字
getAST => Jartto
:1 const esprima = require('esprima'); //解析js的语法的包 2 const estraverse = require('estraverse'); //遍历树的包 3 const escodegen = require('escodegen'); //生成新的树的包 4 let code = `function getAST(){}`; 5 //解析js的语法 6 let tree = esprima.parseScript(code); 7 //遍历树 8 estraverse.traverse(tree, { 9 enter(node) { 10 console.log('enter: ' + node.type); 11 if (node.type === 'Identifier') { 12 node.name = 'Jartto'; 13 } 14 } 15 }); 16 //生成新的树 17 let r = escodegen.generate(tree); 18 console.log(r);
运行后,输出:
1 enter: Program 2 enter: FunctionDeclaration 3 enter: Identifier 4 enter: BlockStatement 5 function Jartto() { 6 }
可以看到,在我们的干预下,输出的结果发生了变化,方法名编译后方法名变成了
Jartto
。这就是抽象语法树的强大之处,本质上通过编译,我们可以去改变任何输出结果。
补充一点:关于
node
类型,全集大致如下:(parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator | ExpressionStatement | BlockStatement | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement | VariableDeclaration | ClassDeclaration | ThisExpression | ArrayExpression | ObjectExpression | YieldExpression | UnaryExpression | UpdateExpression | BinaryExpression | AssignmentExpression | LogicalExpression | MemberExpression | ConditionalExpression | SimpleCallExpression | NewExpression | SequenceExpression | TemplateLiteral | TaggedTemplateExpression | ClassExpression | MetaProperty | AwaitExpression | Property | AssignmentProperty | Super | TemplateElement | SpreadElement | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier
说到这里,聪明的你,可能想到了
Babel
,想到了js
混淆,想到了更多背后的东西。接下来,我们要介绍介绍Babel
是如何将ES6
转成ES5
的。esprima、estraverse 和 escodegen
esprima
、estraverse
和escodegen
模块是操作 AST 的三个重要模块,也是实现babel
的核心依赖,下面是分别介绍三个模块的作用。1、esprima 将 JS 转换成 AST
esprima 模块的用法如下:
/
12345678910111213141516171819202122/ 文件:esprima-test.js
const
esprima = require(
"esprima"
);
let
code =
"function fn() {}"
;
// 生成语法树
let
tree = esprima.parseScript(code);
console.log(tree);
// Script {
// type: 'Program',
// body:
// [ FunctionDeclaration {
// type: 'FunctionDeclaration',
// id: [Identifier],
// params: [],
// body: [BlockStatement],
// generator: false,
// expression: false,
// async: false } ],
// sourceType: 'script' }
通过上面的案例可以看出,通过
esprima
模块的parseScript
方法将 JS 代码块转换成语法树,代码块需要转换成字符串,也可以通过parseModule
方法转换一个模块。2、estraverse 遍历和修改 AST
查看遍历过程:
123456789101112131415161718192021222324// 文件:estraverse-test.js
const
esprima = require(
"esprima"
);
const
estraverse = require(
"estraverse"
);
let
code =
"function fn() {}"
;
// 遍历语法树
estraverse.traverse(esprima.parseScript(code), {
enter(node) {
console.log(
"enter"
, node.type);
},
leave() {
console.log(
"leave"
, node.type);
}
});
// enter Program
// enter FunctionDeclaration
// enter Identifier
// leave Identifier
// enter BlockStatement
// leave BlockStatement
// leave FunctionDeclaration
// leave Program
上面代码通过
estraverse
模块的traverse
方法将esprima
模块转换的 AST 进行了遍历,并打印了所有的type
属性并打印,每含有一个type
属性的对象被叫做一个节点,修改是获取对应的类型并修改该节点中的属性即可。其实深度遍历 AST 就是在遍历每一层的
type
属性,所以遍历会分为两个阶段,进入阶段和离开阶段,在estraverse
的traverse
方法中分别用参数指定的entry
和leave
两个函数监听,但是我们一般只使用entry
。3、escodegen 将 AST 转换成 JS
下面的案例是一个段 JS 代码块被转换成 AST,并将遍历、修改后的 AST 重新转换成 JS 的全过程。
123456789101112131415161718192021222324252627// 文件:escodegen-test.js
const
esprima = require(
"esprima"
);
const
estraverse = require(
"estraverse"
);
const
escodegen = require(
"escodegen"
);
let
code =
"function fn() {}"
;
// 生成语法树
let
tree = esprima.parseScript(code);
// 遍历语法树
estraverse.traverse(tree, {
enter(node) {
// 修改函数名
if
(node.type ===
"FunctionDeclaration"
) {
node.id.name =
"ast"
;
}
}
});
// 编译语法树
let
result = escodegen.generate(tree);
console.log(result);
// function ast() {
// }
在遍历 AST 的过程中
params
值为数组,没有type
属性。实现 Babel 语法转换插件
实现语法转换插件需要借助
babel-core
和babel-types
两个模块,其实这两个模块就是依赖esprima
、estraverse
和escodegen
的。使用这两个模块需要安装,命令如下:
npm install babel-core babel-types
1、plugin-transform-arrow-functions
plugin-transform-arrow-functions
是 Babel 家族成员之一,用于将箭头函数转换 ES5 语法的函数表达式。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657// 文件:plugin-transform-arrow-functions.js
const
babel = require(
"babel-core"
);
const
types = require(
"babel-types"
);
// 箭头函数代码块
let
sumCode = `
const
sum = (a, b) => {
return
a + b;
}`;
let
minusCode = `
const
minus = (a, b) => a - b;`;
// 转化 ES5 插件
let
ArrowPlugin = {
// 访问者(访问者模式)
visitor: {
// path 是树的路径
ArrowFunctionExpression(path) {
// 获取树节点
let
node = path.node;
// 获取参数和函数体
let
params
= node.
params
;
let
body = node.body;
// 判断函数体是否是代码块,不是代码块则添加 return 和 {}
if
(!types.isBlockStatement(body)) {
let
returnStatement = types.returnStatement(body);
body = types.blockStatement([returnStatement]);
}
// 生成一个函数表达式树结构
let
func = types.functionExpression(
null
,
params
, body,
false
,
false
);
// 用新的树结构替换掉旧的树结构
types.replaceWith(func);
}
}
};
// 生成转换后的代码块
let
sumResult = babel.transform(sumCode, {
plugins: [ArrowPlugin]
});
let
minusResult = babel.transform(minusCode, {
plugins: [ArrowPlugin]
});
console.log(sumResult.code);
console.log(minusResult.code);
// let sum = function (a, b) {
// return a + b;
// };
// let minus = function (a, b) {
// return a - b;
// };
我们主要使用
babel-core
的transform
方法将 AST 转化成代码块,第一个参数为转换前的代码块(字符串),第二个参数为配置项,其中plugins
值为数组,存储修改babal-core
转换的 AST 的插件(对象),使用transform
方法将旧的 AST 处理成新的代码块后,返回值为一个对象,对象的code
属性为转换后的代码块(字符串)。内部修改通过
babel-types
模块提供的方法实现,API 可以到 https://github.com/babel/babe... 中查看。ArrowPlugin
就是传入transform
方法的插件,必须含有visitor
属性(固定),值同为对象,用于存储修改语法树的方法,方法名要严格按照 API,对应的方法会修改 AST 对应的节点。在
types.functionExpression
方法中参数分别代表,函数名(匿名函数为null
)、函数参数(必填)、函数体(必填)、是否为generator
函数(默认false
)、是否为async
函数(默认false
),返回值为修改后的 AST,types.replaceWith
方法用于替换 AST,参数为新的 AST。2、plugin-transform-classes
plugin-transform-classes
也是 Babel 家族中的成员之一,用于将 ES6 的class
类转换成 ES5 的构造函数。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586// 文件:plugin-transform-classes.js
const
babel = require(
"babel-core"
);
const
types = require(
"babel-types"
);
// 类
let
code = `
class
Person {
constructor(name) {
this
.name = name;
}
getName () {
return
this
.name;
}
}`;
// 将类转化 ES5 构造函数插件
let
ClassPlugin = {
visitor: {
ClassDeclaration(path) {
let
node = path.node;
let
classList = node.body.body;
// 将取到的类名转换成标识符 { type: 'Identifier', name: 'Person' }
let
className = types.identifier(node.id.name);
let
body = types.blockStatement([]);
let
func = types.functionDeclaration(className, [], body,
false
,
false
);
path.replaceWith(func);
// 用于存储多个原型方法
let
es5Func = [];
// 获取 class 中的代码体
classList.forEach((item, index) => {
// 函数的代码体
let
body = classList[index].body;
// 获取参数
let
params
= item.
params
.length ? item.
params
.map(val => val.name) : [];
// 转化参数为标识符
params
= types.identifier(
params
);
// 判断是否是 constructor,如果构造函数那就生成新的函数替换
if
(item.kind ===
"constructor"
) {
// 生成一个构造函数树结构
func = types.functionDeclaration(className, [
params
], body,
false
,
false
);
}
else
{
// 其他情况是原型方法
let
proto = types.memberExpression(className, types.identifier(
"prototype"
));
// 左侧层层定义标识符 Person.prototype.getName
let
left = types.memberExpression(proto, types.identifier(item.key.name));
// 右侧定义匿名函数
let
right = types.functionExpression(
null
, [
params
], body,
false
,
false
);
// 将左侧和右侧进行合并并存入数组
es5Func.push(types.assignmentExpression(
"="
, left, right));
}
});
// 如果没有原型方法,直接替换
if
(es5Func.length === 0) {
path.replaceWith(func);
}
else
{
es5Func.push(func);
// 替换 n 个节点
path.replaceWithMultiple(es5Func);
}
}
}
};
// 生成转换后的代码块
result = babel.transform(code, {
plugins: [ClassPlugin]
});
console.log(result.code);
// Person.prototype.getName = function () {
// return this.name;
// }
// function Person(name) {
// this.name = name;
// }
上面这个插件的实现要比
plugin-transform-arrow-functions
复杂一些,归根结底还是将要互相转换的 ES6 和 ES5 语法树做对比,找到他们的不同,并使用babel-types
提供的 API 对语法树对应的节点属性进行修改并替换语法树,值得注意的是path.replaceWithMultiple
与path.replaceWith
不同,参数为一个数组,数组支持多个语法树结构,可根据具体修改语法树的场景选择使用,也可根据不同情况使用不同的替换方法。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
2019-05-31 IOPS QPS TPS
2019-05-31 express与koa对比