AST 内容分享

161 阅读5分钟

那开始吧~ 今天要分享的主题是 面向 JS 开发人员的 AST。

主要内容有

  1. 为什么要谈论AST
  2. AST是什么
  3. AST是怎么生成的
  4. AST有什么应用

为什么要谈论 AST

我们查看项目的devDependencies就会发现很多工具模块都与AST有关。

例如:JS转译、代码压缩、elint、prettier等。

这些工具,都是建立在AST基础之上。

AST 是什么

AST是抽象语法树,是源代码抽象语法结构的树状表现形式。

简单来说,它就是一个深度的嵌套对象,这个对象能够描述我们书写代码的所有信息。

我们来看这个例子,左边的纯文本与它对应的是右边的语法树。可以看到,抽象语法树是一个对象,这个对象会有一个顶级的 type 属性 Program,第二个属性是 body,body 数组中存放的每一项,也是一个对象,里面包含了所有对于语句的描述信息,例如描述该语句的类型是变量声明,描述变量的名称,描述变量的初始值以及描述变量声明的关键字等。

AST 是怎么生成的

第一步是词法分析,也叫做扫描。

它会一个字母一个字母地读取代码,当它遇到空格、操作符或者特殊符号的时候它就认为这一个话已经完成了。

例如左边的这段变量声明,这段代码会被分割成var、a、=、2,每个词会按照预定的语法规则成为一个标识token。例如标识这个词是关键词还是变量名还是操作符,最后就生成了右边的tokens列表。

第二步语法分析

语法分析,也称为解析器。

它会将词法分析得出来的tokens列表转化成树形的表达,同时还会验证语法,语法不对的话就会抛出错误。

将代码生成AST了,那AST 能用来做什么呢?像最开始说的那样AST可以用来做JS转译、代码压缩和elint等。下面会对这些方面一一介绍

AST有什么应用

提到 AST 我们肯定会首先想到 babel,大家都知道,它主要是将ES6 转换为 ES5用来解决一些浏览器不兼容ES6 新特性的问题。其中,babel 转换代码就是用了 AST。那么我们就来看看babel 是如何编译代码的。

首先看解析阶段,像刚才说的那样它会经过词法分析语法分析后将源码转换为AST,得到AST后进入转换阶段,这个阶段就是将一系列的转换作用于AST,例如对AST节点进行添加、更新以及移除等操作。最后将转换之后的 AST生成目标代码。

对于图中各个阶段使用到的包,我们先介绍一下,后续的例子都会用到

  • @babel/parser:将源码转换为AST
  • @babel/traverse:遍历和操作AST
  • @babel/types: 类似Lodash工具库,包含了对AST 节点的 构造、验证以及变换。
  • @babel/generator:将AST转成目标源码

转换箭头函数

接下来我们利用上述提到的babel工具包,编写将箭头函数转成普通函数的例子来更深入地理解babel是如何编译代码的。目的就是将左边的箭头函数转换为右边的普通函数。

首先我们需要看一下两者 AST 的区别

可以看到,只需要把箭头函数表达式节点(ArrowFunctionExpression)转换为函数表达式节点(FunctionExpression),把红框的计算表达式包裹在返回语句(ReturnStatement),然后 push到代码块中(BlockStatement)。

来看一下代码,引入工具包,先把源码转换成AST,然后遍历AST找到箭头函数节点,取出函数参数、函数体,构造ReturnStatement和BlockStatement,生成普通函数表达式节点,然后用这个普通函数表达式节点替换原来的就可以了。最后将转换后的AST生成新的代码。至此就成功地将箭头函数转成普通函数了。

简易版的ESLint

另外接下来通过实现简易版的ESLint来更好的理解ESLint的工作原理。ESLint中的一个比较简单的校验规则:no-console,也就是代码中不允许打印console.log。

实现思路就是遍历AST,然后找出console函数节点,如果有console就报错,如果开启自动修复就把console移除。

其实各种大大小小的规则,都是基于此大同小异。

代码压缩

代码压缩一般是在项目打包上线的时候做的,平时大家可能更多的是直接使用插件,现在也来看一看它的工作原理。压缩其实也比较简单就是把变量从有意义变成无意义,保证尽可能的短,例如变成:_、a、b等。当然其实远远不止这些,还有将空格缩进取消等等。

咱们以变量压缩举例,希望将代码中的命名进行压缩,将变量变短且无意义。

实现思路:

第一步:可以通过Scopable这个别名来捕获所有作用域节点,这些作用域节点包括函数、语句块、if else等,然后通过path.scope.bindings取出作用域内的所有变量。

第二步:把这些作用域内的捕获到的变量重新命名,进行简化

如上就可以实现对变量命名进行压缩。

结尾

综上,之后如果想对源码进行分析,转换等,都可以考虑用AST来实现。

参考