做者:东北烤冷面@毛豆前端javascript
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。前端
抽象语法树在不少领域有普遍的应用,好比浏览器,智能编辑器,编译器等。在JavaScript中,虽然咱们并不会经常与AST直接打交道,但却也会常常的涉及到它。例如使用UglifyJS来压缩代码,bable对代码进行转换,ts类型检查,语法高亮等,实际这背后就是在对JavaScript的抽象语法树进行操做。java
javascript的抽象语法树的生成主要依靠的是Javascript Parser(js解析器),整个解析过程分为两个阶段: node
词法分析是计算机科学中将字符序列转换为单词(Token)序列的过程,进行词法分析的程序叫作词法分析器,也叫扫描器(Scanner)。webpack
//code
let age='18'
//tokens
[
{
value: 'let',
type:'identifier'
},
{
type:'whitespace',
value:' '
},
{
value: 'age',
type:'identifier'
},
{
value: '=',
type:'operator'
},
{
value: '=',
type:'operator'
},
{
value: '18',
type:'num'
},
]
复制代码
语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成语法树,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确。源程序的结构由上下文无关文法描述。git
{
"type": "Program",
"start": 0,
"end": 12,
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "age"
},
"init": {
"type": "Literal",
"value": "18",
"raw": "'18'"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}
复制代码
常见的Javascript Parser有不少:github
Babel是一个经常使用的工具,它的工做过程通过三个阶段,解析(parsing)、转换(transform)、生成(generate),以下图所示,在parse阶段,babel使用babylon库将源代码转换为AST,在transform阶段,利用各类插件进行代码转换,在generator阶段,再利用代码生成工具,将AST转换成代码。
web
咱们想在代码中的console打印出来的内容前面加上它所在的函数名称,代码以下:编程
// index.js
function compile(code) {
// todo
}
const code = ` function foo(){ console.log('bar') } `
const result = compile(code)
console.log(result.code)
复制代码
首先咱们先安装bable的全家桶工具:浏览器
yarn add @babel/{parser,traverse,types,generator}
复制代码
而后将其引入文件中:
const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
//tode
}
const code = ` function foo(){ console.log('bar') } `
const result = compile(code)
console.log(result.code)
复制代码
咱们能够经过AST Explorer查看code代码的抽象语法树结构,注意,这里面咱们的解析工具要选用babylon7,这样和咱们例子中代码解析出的结构才匹配
先解析拿到AST,直接生成代码片断:
const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
// 1. 解析
const ast = parser.parse(code)
// 2. 遍历
// 3. 生成代码片断
return generator.default(ast, {}, code)
}
const code = ` function foo(){ console.log('bar') } `
const result = compile(code)
console.log(result.code)
复制代码
运行一下
node index.js
复制代码
输出结果
第二阶段
须要使用到访问者(Visitors),访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。这么说有些抽象因此让咱们来看一个例子。
const MyVisitor = {
Identifier() {
console.log("Called!");
}
};
// 你也能够先建立一个访问者对象,并在稍后给它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}
复制代码
这是一个简单的访问者,把它用于遍历中时,每当在树中碰见一个 Identifier
的时候会调用 Identifier()
方法。
因此在下面的代码中 Identifier()
方法会被调用四次(包括 square
在内,总共有四个 Identifier
)。).
function square(n) {
return n * n;
}
path.traverse(MyVisitor);
Called!
Called!
Called!
Called!
复制代码
回到咱们的例子,咱们只须要建立一个访问者,访问到CallExpression节点,而后经过判断,去修改它arguments属性的参数就能够完成咱们的任务了
const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
// 1. 解析
const ast = parser.parse(code)
// 2. 遍历
//visitor能够对特定节点进行处理
const visitor = {
//定义须要转换的节点CallExpression
CallExpression(path) {
//获取当前的节点
const { callee } = path.node;
//判断
if (
t.isMemberExpression(callee)
&&
callee.object.name === 'console'
&&
callee.property.name === 'log'
) {
// 获取上层FunctionDeclaration路径
const funcPath = path.findParent(p => {
return p.isFunctionDeclaration();
})
// 将上层函数名添加到参数前
path.node.arguments.unshift(
t.stringLiteral(`function name ${funcPath.node.id.name}:`)
)
}
}
}
traverse.default(ast, visitor)
// 3. 生成代码片断
return generator.default(ast, {}, code)
}
const code = ` function foo(){ console.log('bar') } `
const result = compile(code)
console.log(result.code)
复制代码
咱们再来打印下
这样咱们就完成了整个任务,固然这只是一个很简单的例子,在实际开发中,咱们还须要进行更复杂的判断才能保证咱们的功能完善。