官方描述: 是源代码的抽象语法结构的树状表现形式 简单的来说,就是js代码的map描述(普通的js对象)
抽象语法树的用处非常多,比如我们常见的代码压缩(之前我天真的以为代码压缩应该就是正则匹配替换掉换行和空格,在一次使用uglifyjs压缩代码测试的时候,发现一个没有用到的函数声明在压缩后的代码中彻底没找到!!),IDE的代码提示,错误提示,编译器等等。
就拿上面的我遇到的问题来说,它是怎么实现的检测到我定义了这个函数,但是没有使用呢。其实内部原来就是把整个代码解析成一棵树,包含了各种类型的节点,类似于我们熟悉的dom树(这点来看,其实很多技术的思想都是相通的)
我是在研究yeoman生成器mock-server的时候,需要修改已经存在的文件内容,遇到这个问题。
//AST
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "sau"//函数名
},
"params": [],
"defaults": [],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "inner"
},
"init": {
"type": "Identifier",
"name": "answer"
}
}
],
"kind": "var"
}
]
},
"generator": false,
"expression": false
},
{
"type": "ExpressionStatement",//节点类型
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "sau"//调用的函数名
},
"arguments": []
}
}
],
"sourceType": "script"
}
- tree结构
tree
- node
- node
...
- tree根节点结构
{
type:'Program',
body:[
]
}
- node 结构
{
type:'xxx',
body:[
],
...
}
- 除了变量node,其他的大概都有的两个属性 type body
- 除了type,body两个属性 每个节点上还包含了很多其他描述属性。比如:我们的函数声明node中包含一个id.name 就是我们的函数名, 而函数调用中有个expression.callee指向调用的函数名,我们只需要遍历整个树,查看定义的node 的id.name 在expression.callee中是否存在 就能知道我们的这个函数是否被调用过,如果没有调用过 我们就可以把这个函数定义的node删除掉!然后再把抽象语法树转换成js代码就ok了
上面的实现中,有三个比较重要的步骤
- js => ast
- 遍历ast 修改
- ast => js
这里,个人能力的缘故还不到去关注js=>ast转换的实现细节的时候,我们只要能实现就行,比较流行的有两个库:
- esprima 把js转换成ast
- esprima-fb 来自facebook,基于esprima 兼容jsx js|jsx => ast
demos: esprima-fb/
- js2ast.js => js解析成ast
- js2ast-with-options.js 通过配置不同的解析参数 [在线查看不同配置参数的解析结果](http://esprima.org/demo/parse.html)
demos: esprima-walk
- walk.js
- 生成的抽象语法树 是一个js对象 所以对树的操作 就和修改普通的js对象一样
demos: escodegen-wallaby/
- gen.js 修改tree属性 重新生成js代码 保存
- escodegen
- escodegen-wallaby 兼容jsx 对应上面的esprima-fb