bg-pic

最近在捣鼓自己的项目时,遇到了AST这个似曾相识的家伙。熟悉是因为我大概知道它是我们写的代码的一种抽象表示,陌生是因为自己对它的了解也仅限于此,所以我趁着这个机会对AST好好研究了一番,收获还是蛮多的,所以决定整理成文分享出来,希望能让它也成为你熟悉的伙伴

初识AST

AST(abstract tree)全称 抽象语法树,见名知其义,AST就是对我们所写源码的一种抽象表示,表示的形式就是树形结构,它与我们熟悉的 virtual dom 的底层逻辑是一致的:为了能更方便地对目标(dom,source code)进行操作(新增,删除,查询,更新),借助增加一层抽象层(VDOM,AST)的方式来达到目的

由此可见,AST的出现不是目的,而是手段,这个目的在不同的工具下会有所不同,下面以部分常见工具为例进行说明

AST虽然在我们的日常开发中不会直接接触,但是它其实存在于开发过程中的各个环节里,默默地为我们撑起了前端的一片天

bqb

因此,打好AST这个地基也是我们急需完成的事情,因为只有地基稳了,我们的上层建筑才能稳定且持续发展,所以接下来让我们深入对它研究一番吧

生成AST的过程

在经过上文对AST的描述后,你肯定会有疑问:我们所写的源码是怎么转换为一棵抽象语法树的呢? 是的,我一开始也跟你有一样的疑问并觉得这个过程很’黑魔法’,因为能把两个看似完全不一样的东西进行转化,想想都很神奇

在经过对AST的研究后,我已经解开了心中的疑问,接下来就让我为你解开心中疑问吧🤪。

bqb

AST的生成分为两步:

词法分析

词法分析也称为扫描,在词法分析阶段,会一个一个字符读取源码,然后与js中有特定含义的字符串进行比对,从而生成很多个Token(Token是一个不可分割的最小单元),在词法分析器里,每个关键字是一个Token,每个标识符是一个Token,每个操作符是一个Token,每个标点符号也是一个Token,例如 import 就是一个token,它属于关键字并且不能再被切分,否则就会失去原本的语义

需要注意的是词法分析会过滤掉源代码中的注释和空白字符(换行符、空格、制表符等),这也就是说明AST并不能完全表示源代码,而如果要完全表示源代码,是需要另一种树形结构,那便是CST(具体语法树),这里就不展开讲了

最终,整个源代码将被分割进一个Tokens列表(或者说一维数组),下面给出一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 源代码 */
function a(){
return n * n
}

/* Tokens列表 */
[
{ type: { ... }, value: "function", loc: { ... } },
{ type: { ... }, value: "a", loc: { ... } },
{ type: { ... }, value: "(", loc: { ... } },
{ type: { ... }, value: ")", loc: { ... } },
{ type: { ... }, value: "{", loc: { ... } },
{ type: { ... }, value: "return", loc: { ... } },
{ type: { ... }, value: "n", loc: { ... } },
{ type: { ... }, value: "*", loc: { ... } },
{ type: { ... }, value: "n", loc: { ... } },
{ type: { ... }, value: "}", loc: { ... } }
]

type属性里有一组属性来描述该令牌:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}

语法分析

在经过词法分析后,我们得到了一个Tokens列表,语法分析则是对这个Tokens列表进行转化,生成对应的AST,同时还会验证语法,如果存在错误,则会抛出语法错误,下面还是以上述例子中的源码为例,来看看最终生成的AST到底长啥样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* 源代码 */
function a(){
return n * n
}

/* AST */
{
"type": "Program",
"start": 0,
"end": 30,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 30,
"id": {
"type": "Identifier",
"start": 9,
"end": 10,
"name": "a"
},
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 12,
"end": 30,
"body": [
{
"type": "ReturnStatement",
"start": 16,
"end": 28,
"argument": {
"type": "BinaryExpression",
"start": 23,
"end": 28,
"left": {
"type": "Identifier",
"start": 23,
"end": 24,
"name": "n"
},
"operator": "*",
"right": {
"type": "Identifier",
"start": 27,
"end": 28,
"name": "n"
}
}
}
]
}
}
],
"sourceType": "module"
}

可以看到虽然源代码很少,但是生成的AST却包含很多信息,细心观察可以发现,每一层都具有相似的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
type: "FunctionDeclaration",
id: {...},
params: [...],
body: {...}
}

{
type: "Identifier",
name: ...
}

{
type: "BinaryExpression",
operator: ...,
left: {...},
right: {...}
}

这些都可以看成是AST的一个节点(Node),一棵AST可以包含一个或多个节点,它们组合在一起可以描述用于静态分析的程序语法,webpack中的 tree-shaking 能力也正是来源于此

看到这里你又会产生疑问了:为什么AST一定要是这个结构,而不是其他结构呢?

bqb

其实这里的AST结构是社区规范 EStree 所定义的,里面包含了各种节点类型的定义,有空了可以多瞅瞅,但是并不是所有解析器都遵循了这个规范,有一些会根据这个规范进行一定程度的调整来满足自身的需要,据我所知,完全遵循规范的解析器有:Accorn、Esprima、Espree

总结

AST的威力其实是非常强大的,我们可以通过它做许多非常有意思的事情,常见的如编写babel和webpack插件,不常见的就留给你自行脑洞🤪

最后推荐一个在线解析AST的网站:astexplorer,功能非常强大,可以转换各种类型的语言或者使用各种类型的解析器,可以根据自己的需要进行选择,好啦,就写到这里啦,完结,撒花🎉~