最近在捣鼓自己的项目时,遇到了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列表(或者说一维数组),下面给出一个示例
1 | /* 源代码 */ |
type属性里有一组属性来描述该令牌:
1 | { |
语法分析
在经过词法分析后,我们得到了一个Tokens列表,语法分析则是对这个Tokens列表进行转化,生成对应的AST,同时还会验证语法,如果存在错误,则会抛出语法错误,下面还是以上述例子中的源码为例,来看看最终生成的AST到底长啥样
1 | /* 源代码 */ |
可以看到虽然源代码很少,但是生成的AST却包含很多信息,细心观察可以发现,每一层都具有相似的结构
1 | { |
这些都可以看成是AST的一个节点(Node),一棵AST可以包含一个或多个节点,它们组合在一起可以描述用于静态分析的程序语法,webpack中的 tree-shaking 能力也正是来源于此
看到这里你又会产生疑问了:为什么AST一定要是这个结构,而不是其他结构呢?
其实这里的AST结构是社区规范 EStree 所定义的,里面包含了各种节点类型的定义,有空了可以多瞅瞅,但是并不是所有解析器都遵循了这个规范,有一些会根据这个规范进行一定程度的调整来满足自身的需要,据我所知,完全遵循规范的解析器有:Accorn、Esprima、Espree
总结
AST的威力其实是非常强大的,我们可以通过它做许多非常有意思的事情,常见的如编写babel和webpack插件,不常见的就留给你自行脑洞🤪
最后推荐一个在线解析AST的网站:astexplorer,功能非常强大,可以转换各种类型的语言或者使用各种类型的解析器,可以根据自己的需要进行选择,好啦,就写到这里啦,完结,撒花🎉~