抽象语法树(Abstract Syntax Tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是"抽象"的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有两个分支的节点来表示。
AST不依赖于具体的文法,不依赖于语言的细节,我们将源代码转化为AST后,可以对AST做很多的操作。
直接来看把一个简单的函数转换成AST之后的样子。
- // 简单函数
- function square(n) {
- return n * n;
- }
- // 转换后的AST
- {
- type: "FunctionDeclaration",
- id: {
- type: "Identifier",
- name: "square"
- },
- params: [
- {
- type: "Identifier",
- name: "n"
- }
- ],
- ...
- }
从纯文本转换成树形结构的数据,每个条目和树中的节点一一对应。
纯文本转AST的实现
当下的编译器都做了纯文本转AST的事情。
一款编译器的编译流程是很复杂的,但我们只需要关注词法分析和语法分析,这两步是从代码生成AST的关键所在。
第一步:词法分析,也叫扫描scanner
它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识 tokens。同时,它会移除空白符、注释等。最后,整个代码将被分割进一个 tokens 列表(或者说一维数组)。
- const a = 5;
- // 转换成
- [{value: 'const', type: 'keyword'}, {value: 'a', type: 'identifier'}, ...]
当词法分析源代码的时候,它会一个一个字母地读取代码,所以很形象地称之为扫描 - scans。当它遇到空格、操作符,或者特殊符号的时候,它会认为一个话已经完成了。
第二步:语法分析,也称解析器
它会将词法分析出来的数组转换成树形的形式,同时,验证语法。语法如果有错的话,抛出语法错误。
- [{value: 'const', type: 'keyword'}, {value: 'a', type: 'identifier'}, ...]
- // 语法分析后的树形形式
- {
- type: "VariableDeclarator",
- id: {
- type: "Identifier",
- name: "a"
- },
- ...
- }
当生成树的时候,解析器会删除一些没必要的标识 tokens(比如:不完整的括号),因此 AST 不是 100% 与源码匹配的。
解析器100%覆盖所有代码结构生成树叫做CST(具体语法树)。
用例:代码转换之babel
babel 是一个 JavaScript 编译器。宏观来说,它分3个阶段运行代码:解析(parsing) — 将代码字符串转换成 AST抽象语法树,转译(transforming) — 对抽象语法树进行变换操作,生成(generation) — 根据变换后的抽象语法树生成新的代码字符串。
我们给 babel 一段 js 代码,它修改代码然后生成新的代码返回。它是怎么修改代码的呢?没错,它创建了 AST,遍历树,修改 tokens,最后从 AST中生成新的代码。
事实上,在javascript世界中,你可以认为抽象语法树(AST)是最底层。 再往下,就是关于转换和编译的“黑魔法”领域了。
现在,我们拆解一个简单的add函数
- function add(a, b) {
- return a + b
- }
首先,我们拿到的这个语法块,是一个FunctionDeclaration(函数定义)对象。
用力拆开,它成了三块:
- 一个id,就是它的名字,即add
- 两个params,就是它的参数,即[a, b]
- 一块body,也就是大括号内的一堆东西
add没办法继续拆下去了,它是一个最基础Identifier(标志)对象,用来作为函数的唯一标志,就像人的姓名一样。
- {
- name: 'add'
- type: 'identifier'
- ...
- }
params继续拆下去,其实是两个Identifier组成的数组。之后也没办法拆下去了。
- [
- {
- name: 'a'
- type: 'identifier'
- ...
- },
- {
- name: 'b'
- type: 'identifier'
- ...
- }
- ]
接下来,我们继续拆开body
我们发现,body其实是一个BlockStatement(块状域)对象,用来表示是{return a + b}
打开Blockstatement,里面藏着一个ReturnStatement(Return域)对象,用来表示return a + b
继续打开ReturnStatement,里面是一个BinaryExpression(二项式)对象,用来表示a + b
继续打开BinaryExpression,它成了三部分,left,operator,right
- operator 即+
- left 里面装的,是Identifier对象 a
- right 里面装的,是Identifer对象 b
就这样,我们把一个简单的add函数拆解完毕,用图表示就是