提起 AST 抽象语法树,大家可能并不感冒。但是提到它的使用场景,也许会让你大吃一惊。原来它一直在你左右与你相伴,而你却不知。
一、什么是抽象语法树
在计算机科学中,抽象语法树(abstract syntax tree
或者缩写为 AST
),或者语法树(syntax tree
),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。
之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
二、使用场景
- JS 反编译,语法解析
- Babel 编译 ES6 语法
- 代码高亮
- 关键字匹配
- 作用域判断
- 代码压缩
三、AST Explorer
我们来看一个 ES6
的解释器,声明如下的代码:
1 let tips = [ 2 "Jartto‘s AST Demo" 3 ];
看看是如何解析的, JSON 格式如下:
1 { 2 "type": "Program", 3 "start": 0, 4 "end": 38, 5 "body": [ 6 { 7 "type": "VariableDeclaration", 8 "start": 0, 9 "end": 37, 10 "declarations": [ 11 { 12 "type": "VariableDeclarator", 13 "start": 4, 14 "end": 36, 15 "id": { 16 "type": "Identifier", 17 "start": 4, 18 "end": 8, 19 "name": "tips" 20 }, 21 "init": { 22 "type": "ArrayExpression", 23 "start": 11, 24 "end": 36, 25 "elements": [ 26 { 27 "type": "Literal", 28 "start": 15, 29 "end": 34, 30 "value": "Jartto‘s AST Demo", 31 "raw": "\"Jartto‘s AST Demo\"" 32 } 33 ] 34 } 35 } 36 ], 37 "kind": "let" 38 } 39 ], 40 "sourceType": "module" 41 }
而它的语法树大概如此:
每个结构都看的清清楚楚,这时候我们会发现,这和 Dom
树真的差不了多少。再来看一个例子:
1 (1+2)*3
AST Tree:
我们删掉括号,看看规则是如何变化的?JSON
格式会一目了然:
1 { 2 "type": "Program", 3 "start": 0, 4 "end": 6, 5 "body": [ 6 { 7 "type": "ExpressionStatement", 8 "start": 0, 9 "end": 5, 10 "expression": { 11 "type": "BinaryExpression", 12 "start": 0, 13 "end": 5, 14 "left": { 15 "type": "Literal", 16 "start": 0, 17 "end": 1, 18 "value": 1, 19 "raw": "1" 20 }, 21 "operator": "+", 22 "right": { 23 "type": "BinaryExpression", 24 "start": 2, 25 "end": 5, 26 "left": { 27 "type": "Literal", 28 "start": 2, 29 "end": 3, 30 "value": 2, 31 "raw": "2" 32 }, 33 "operator": "*", 34 "right": { 35 "type": "Literal", 36 "start": 4, 37 "end": 5, 38 "value": 3, 39 "raw": "3" 40 } 41 } 42 } 43 } 44 ], 45 "sourceType": "module" 46 }
可以看出来,(1+2)*3
和 1+2*3
,语法树是有差别的:
1.在确定类型为 ExpressionStatement
后,它会按照代码执行的先后顺序,将表达式 BinaryExpression
分为 Left
,operator
和 right
三块;
2.每块标明了类型,起止位置,值等信息;
3.操作符类型;
再来看看我们最常用的箭头函数:
1 const mytest = (a,b) => { 2 return a+b; 3 }
JSON 格式如下:
1 { 2 "type": "Program", 3 "start": 0, 4 "end": 42, 5 "body": [ 6 { 7 "type": "VariableDeclaration", 8 "start": 0, 9 "end": 41, 10 "declarations": [ 11 { 12 "type": "VariableDeclarator", 13 "start": 6, 14 "end": 41, 15 "id": { 16 "type": "Identifier", 17 "start": 6, 18 "end": 12, 19 "name": "mytest" 20 }, 21 "init": { 22 "type": "ArrowFunctionExpression", 23 "start": 15, 24 "end": 41, 25 "id": null, 26 "expression": false, 27 "generator": false, 28 "params": [ 29 { 30 "type": "Identifier", 31 "start": 16, 32 "end": 17, 33 "name": "a" 34 }, 35 { 36 "type": "Identifier", 37 "start": 18, 38 "end": 19, 39 "name": "b" 40 } 41 ], 42 "body": { 43 "type": "BlockStatement", 44 "start": 24, 45 "end": 41, 46 "body": [ 47 { 48 "type": "ReturnStatement", 49 "start": 28, 50 "end": 39, 51 "argument": { 52 "type": "BinaryExpression", 53 "start": 35, 54 "end": 38, 55 "left": { 56 "type": "Identifier", 57 "start": 35, 58 "end": 36, 59 "name": "a" 60 }, 61 "operator": "+", 62 "right": { 63 "type": "Identifier", 64 "start": 37, 65 "end": 38, 66 "name": "b" 67 } 68 } 69 } 70 ] 71 } 72 } 73 } 74 ], 75 "kind": "const" 76 } 77 ], 78 "sourceType": "module" 79 }
AST Tree 结构如下图:
我们注意到了,增加了几个新的字眼:
ArrowFunctionExpression
BlockStatement
ReturnStatement
到这里,其实我们已经慢慢明白了:
抽象语法树其实就是将一类标签转化成通用标识符,从而结构出的一个类似于树形结构的语法树。
四、深入原理
可视化的工具可以让我们迅速有感官认识,那么具体内部是如何实现的呢?
继续使用上文的例子:
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
的。
五、关于 Babel
由于 ES6
的兼容问题,很多情况下,我们都在使用 Babel
插件来进行编译,那么有没有想过 Babel
是如何工作的呢?先来看看:
1 let sum = (a, b)=>{return a+b};
AST
大概如此:
JSON
格式可能会看的清楚些:
1 { 2 "type": "Program", 3 "start": 0, 4 "end": 31, 5 "body": [ 6 { 7 "type": "VariableDeclaration", 8 "start": 0, 9 "end": 31, 10 "declarations": [ 11 { 12 "type": "VariableDeclarator", 13 "start": 4, 14 "end": 30, 15 "id": { 16 "type": "Identifier", 17 "start": 4, 18 "end": 7, 19 "name": "sum" 20 }, 21 "init": { 22 "type": "ArrowFunctionExpression", 23 "start": 10, 24 "end": 30, 25 "id": null, 26 "expression": false, 27 "generator": false, 28 "params": [ 29 { 30 "type": "Identifier", 31 "start": 11, 32 "end": 12, 33 "name": "a" 34 }, 35 { 36 "type": "Identifier", 37 "start": 14, 38 "end": 15, 39 "name": "b" 40 } 41 ], 42 "body": { 43 "type": "BlockStatement", 44 "start": 18, 45 "end": 30, 46 "body": [ 47 { 48 "type": "ReturnStatement", 49 "start": 19, 50 "end": 29, 51 "argument": { 52 "type": "BinaryExpression", 53 "start": 26, 54 "end": 29, 55 "left": { 56 "type": "Identifier", 57 "start": 26, 58 "end": 27, 59 "name": "a" 60 }, 61 "operator": "+", 62 "right": { 63 "type": "Identifier", 64 "start": 28, 65 "end": 29, 66 "name": "b" 67 } 68 } 69 } 70 ] 71 } 72 } 73 } 74 ], 75 "kind": "let" 76 } 77 ], 78 "sourceType": "module" 79 }
结构大概如此,那我们再用代码模拟一下:
1 const babel = require(‘babel-core‘); //babel核心解析库 2 const t = require(‘babel-types‘); //babel类型转化库 3 let code = `let sum = (a, b)=>{return a+b}`; 4 let ArrowPlugins = { 5 //访问者模式 6 visitor: { 7 //捕获匹配的API 8 ArrowFunctionExpression(path) { 9 let { node } = path; 10 let body = node.body; 11 let params = node.params; 12 let r = t.functionExpression(null, params, body, false, false); 13 path.replaceWith(r); 14 } 15 } 16 } 17 let d = babel.transform(code, { 18 plugins: [ 19 ArrowPlugins 20 ] 21 }) 22 console.log(d.code);
记得安装 babel-core
,babel-types
这俩插件,之后运行 babel.js
,我们看到了这样的输出:
1 let sum = function (a, b) { 2 return a + b; 3 };
这里,我们完美的将箭头函数转换成了标准函数。
那么问题又来了,如果是简写呢,像这样,还能正常编译吗?
1 let sum = (a, b)=>a+b
Body
部分的结构发生了变化,所以,我们的 babel.js
运行就会报错了。
TypeError: unknown: Property body of FunctionExpression expected node to be of a type ["BlockStatement"] but instead got "BinaryExpression"
意思很明了,我们的 body
类型变成 BinaryExpression
不再是 BlockStatement
,所以需要做一些修改:
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 };
看起来不错,堪称完美~
六、深入 Babel
当然,上文我们简单演示了 Babel
是如何来编译代码的,但是并非简单如此。
Babel
使用一个基于 ESTree
并修改过的 AST
,它的内核说明文档可以在这里找到。
正如我们上面示例代码一样,Babel 的三个主要处理步骤分别是: 解析(parse
),转换(transform
),生成(generate
)。
1.解析(parse
):解析步骤接收代码并输出 AST
。 这个步骤分为两个阶段:词法分析 Lexical Analysis
和语法分析Syntactic Analysis
。
- 词法分析:词法分析阶段把字符串形式的代码转换为令牌(
tokens
) 流。你可以把令牌看作是一个扁平的语法片段数组: -
n * n;
- 例如上面的代码片段,解析结果如下:
[ { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } }, { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } }, { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } }, ... ]
每一个
type
有一组属性来描述该令牌,和AST
节点一样它们也有start
,end
,loc
属性:{ type: { label: ‘name‘, keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, ... }
- 语法分析:语法分析阶段会把一个令牌流转换成
AST
的形式。 这个阶段会使用令牌中的信息把它们转换成一个AST
的表述结构,这样更易于后续的操作。
2.转换(transform
):接收 AST
并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel
或是其他编译器中最复杂的过程,同时也是插件将要介入工作的部分。
3.生成(generate
):代码生成步骤把最终(经过一系列转换之后)的 AST
转换成字符串形式的代码,同时还会创建源码映射(source maps
)。
代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
了解这这些过程,我们回头再来参悟一下之前的示例代码:
1 const babel = require(‘babel-core‘); //babel核心解析库 2 const t = require(‘babel-types‘); //babel类型转化库 3 let code = `let sum = (a, b)=>{return a+b}`; 4 let ArrowPlugins = { 5 //访问者模式 6 visitor: { 7 //捕获匹配的API 8 ArrowFunctionExpression(path) { 9 let { node } = path; 10 let body = node.body; 11 let params = node.params; 12 let r = t.functionExpression(null, params, body, false, false); 13 path.replaceWith(r); 14 } 15 } 16 } 17 let d = babel.transform(code, { 18 plugins: [ 19 ArrowPlugins 20 ] 21 }) 22 console.log(d.code);
是不是发现突然简单易懂了。
七、关于遍历
想要转换 AST
你需要进行递归的树形遍历。
比方说我们有一个 FunctionDeclaration
类型。它有几个属性:id
,params
,和 body
,每一个都有一些内嵌节点。
1 { 2 type: "FunctionDeclaration", 3 id: { 4 type: "Identifier", 5 name: "square" 6 }, 7 params: [{ 8 type: "Identifier", 9 name: "n" 10 }], 11 body: { 12 type: "BlockStatement", 13 body: [{ 14 type: "ReturnStatement", 15 argument: { 16 type: "BinaryExpression", 17 operator: "*", 18 left: { 19 type: "Identifier", 20 name: "n" 21 }, 22 right: { 23 type: "Identifier", 24 name: "n" 25 } 26 } 27 }] 28 } 29 }
按照上面的代码结构,我们来说一下具体流程:
1.首先我们从 FunctionDeclaration
开始并且我们知道它的内部属性(即:id
,params
,body
),所以我们依次访问每一个属性及它们的子节点;
2.然后我们来到 id
,它是一个 Identifier
。Identifier
没有任何子节点属性,所以我们继续;
3.紧接着是 params
,由于它是一个数组节点所以我们访问其中的每一个,它们都是 Identifier
类型的单一节点,然后我们继续;
4.此时我们来到了 body
,这是一个 BlockStatement
并且也有一个 body
节点,而且也是一个数组节点,我们深入访问其中的每一个;
5.这里唯一的一个属性是 ReturnStatement
节点,它有一个 argument
,我们访问 argument
就找到了 BinaryExpression
;
6.BinaryExpression
有一个 operator
,一个 left
,和一个 right
。 Operator
不是一个节点,它只是一个值。因此我们不用继续向内遍历,我们只需要访问 left
和 right
。
Babel 的转换步骤基本都是是这样的遍历过程。
八、具体语法树
看到抽象语法树,我们脑海中会出现这样一个疑问:有没有具体语法树呢?
和抽象语法树相对的是具体语法树(通常称作分析树)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦AST
被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。
文章首发于 Jartto‘s blog
原文地址:https://www.cnblogs.com/wind-alex/p/10375236.html