这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
最近在捣鼓自己的项目时,遇到了AST这个似曾相识的家伙。熟悉是因为我大概知道它是我们写的代码的一种抽象表示,陌生是因为自己对它的了解也仅限于此,所以我趁着这个机会对AST好好研究了一番,收获还是蛮多的,所以决定整理成文分享出来,希望能让它也成为你熟悉的伙伴
初识AST
AST(abstract tree)全称 抽象语法树,见名知其义,AST就是对我们所写源码的一种抽象表示,表示的形式就是树形结构,它与我们熟悉的 virtual dom 的底层逻辑是一致的:为了能更方便地对目标(dom,source code)进行操作(新增,删除,查询,更新),借助增加一层抽象层(VDOM,AST)的方式来达到目的
由此可见,AST的出现不是目的,而是手段,这个目的在不同的工具下会有所不同,下面以部分常见工具为例进行说明
- Babel,借助AST将js高版本语法转换为我们设定的较低版本的语法,从而可以让我们提前用上目标浏览器还未支持的js新特性
- Uglify,借助AST将js代码中的注释,空白符删除掉,从而实现压缩代码的目的
- Eslint,借助AST分析代码中存在的问题,从而可以反馈给开发者
- 编辑器,借助AST实现实时分析用户输入的代码,从而实现代码高亮、智能提示、语法错误提示等功能
- V8引擎,AST在V8引擎执行js的过程中起着承上启下的作用,其大致分为以下三步,
- 借助解析器(Parser)将js源码解析成AST
- 借助解释器(Ignition)将AST编译为字节码文件
- 在代码真正被执行时,借助解释器将字节码编译为机器码,这一步也解释了为什么js是解释型的语言,因为从字节码到机器码这一步是实时编译的,也就是一边编译一边执行。
AST虽然在我们的日常开发中不会直接接触,但是它其实存在于开发过程中的各个环节里,默默地为我们撑起了前端的一片天
因此,打好AST这个地基也是我们急需完成的事情,因为只有地基稳了,我们的上层建筑才能稳定且持续发展,所以接下来让我们深入对它研究一番吧
生成AST的过程
在经过上文对AST的描述后,你肯定会有疑问:我们所写的源码是怎么转换为一棵抽象语法树的呢? 是的,我一开始也跟你有一样的疑问并觉得这个过程很'黑魔法',因为能把两个看似完全不一样的东西进行转化,想想都很神奇
在经过对AST的研究后,我已经解开了心中的疑问,接下来就让我为你解开心中疑问吧🤪。
AST的生成分为两步:
词法分析
词法分析也称为扫描,在词法分析阶段,会一个一个字符读取源码,然后与js中有特定含义的字符串进行比对,从而生成很多个Token(Token是一个不可分割的最小单元),在词法分析器里,每个关键字是一个Token,每个标识符是一个Token,每个操作符是一个Token,每个标点符号也是一个Token,例如 import 就是一个token,它属于关键字并且不能再被切分,否则就会失去原本的语义
需要注意的是词法分析会过滤掉源代码中的注释和空白字符(换行符、空格、制表符等),这也就是说明AST并不能完全表示源代码,而如果要完全表示源代码,是需要另一种树形结构,那便是CST(具体语法树),这里就不展开讲了
最终,整个源代码将被分割进一个Tokens列表(或者说一维数组),下面给出一个示例
/* 源代码 */
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属性里有一组属性来描述该令牌:
{
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到底长啥样
/* 源代码 */
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却包含很多信息,细心观察可以发现,每一层都具有相似的结构
{
type: "FunctionDeclaration",
id: {...},
params: [...],
body: {...}
}
{
type: "Identifier",
name: ...
}
{
type: "BinaryExpression",
operator: ...,
left: {...},
right: {...}
}
这些都可以看成是AST的一个节点(Node),一棵AST可以包含一个或多个节点,它们组合在一起可以描述用于静态分析的程序语法,webpack中的 tree-shaking 能力也正是来源于此
看到这里你又会产生疑问了:为什么AST一定要是这个结构,而不是其他结构呢?
其实这里的AST结构是社区规范 EStree 所定义的,里面包含了各种节点类型的定义,有空了可以多瞅瞅,但是并不是所有解析器都遵循了这个规范,有一些会根据这个规范进行一定程度的调整来满足自身的需要,据我所知,完全遵循规范的解析器有:Accorn、Esprima、Espree
总结
AST的威力其实是非常强大的,我们可以通过它做许多非常有意思的事情,常见的如编写babel和webpack插件,不常见的就留给你自行脑洞🤪
最后推荐一个在线解析AST的网站:astexplorer,功能非常强大,可以转换各种类型的语言或者使用各种类型的解析器,可以根据自己的需要进行选择,好啦,就写到这里啦,完结,撒花🎉~
一点小小的请求
既然都看到这里啦,如果你喜欢我的文章,那么请动动你的手指,帮我的文章点个赞或收个藏,xdm的支持是我创作的最大动力,自己单机真不好玩!
最近自己搭建了个人博客,上面会最先发布我写的文章,希望感兴趣的小伙伴都去逛逛,如果能评论留言就更好啦,嘿嘿,期待你们的光临哦~