介绍
AST概述
抽象语法树(AST)是JavaScript编译器处理代码创建的一种树形结构。是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
它有什么用?
更好的理解代码结构,使用代码进行应用开发,写个页面,写个业务功能,可能并不需要了解AST,但是一旦想用代码表达代码,用代码操作代码,就绕不开抽象语法树。比如:代码转换:
这是一个原生的小程序,转换成uniapp的代码工具,uniapp出来之后,很多用原生小程序写法写的代码,转换成uniapp,然后再编译到其他多个小程序或者APP。
原生的小程序写法和uniapp的写法还是有差别的,具体的包括:
咱们以微信小程序
为例:(其他都是模仿,除了差不多的语法,其他就是不同的平台特殊的功能API差异了,比如抖音会有商城,拍视频,抖音群等等,百度小程序有搜索,支付宝的支付交易组件)包括WXML和WXS,对应uniapp或者说vue,的template和css
还有比较典型的是给data里面的数据赋值,用
setData
这种。所以想要完成这样一个转换工具,需要用一个东西,来描述原生的小程序语言,对于前端来说,最熟悉的就是json格式,然后去操作这套json,让它可以描述成uniapp,不就可以转换成uniapp了吗。
所以,我们看下这个转换工具的代码,确实是这样的逻辑。
我们看他的package.json文件,这里有一个库,叫gogocode,这个就是市面上的其中一种AST操作工具库。
这里面,核心的逻辑,就是通过AST操作工具库,把原生小程序写的文件内容,包括:静态文件,云函数,wxml里面的模板,生命周期等等,都转换成抽象语法树的json,然后去操作它,比如用正则表达式去替换标签,然后再拼接成uniapp支持的文件。
应用场景介绍
- 代码转换:就行上面说的,把一种代码转换成另一种代码。
- 代码格式化:使用AST来重新格式化代码,实现代码的统一风格和格式。比如咱们常用的prettier。
- 代码优化:可以更容易的分析代码结构和含义,对代码进行审查,或者重构。找出代码中的性能瓶颈并进行优化。批量去除代码里面的
console.log
,也是使用了AST。 - 代码安全:利用AST检查代码中的安全漏洞和不符合安全规范的地方。
常用的工具
常见的AST节点
- 字面量(Literal):固定的值
- 比如:
let name = 'fengjiaheng'
,其中这个'fengjiaheng'
就是一个字符串字面量,其他的还有数字字面量(NumericLiteral)、布尔字面量(BooleanLiteral)、正则表达式字面量(RegExpLiteral)、BigInt字面量(BigintLiteral)、null字面量(NullLiteral)等等。
- 比如:
标识符(Identifier):定义的符号
- 变量名、属性名、参数名等,所有声明和引用的名字,都是标识符。我们都知道,JS 中的标识符只能包含字母或数字或下划线(“_”)或美元符号(“$”),且不能以数字开头。这也是 Identifier 的词法特点。
比如下面这段代码,一共有多少标识符呢?1
2
3
4
5
6
7
8
9const name = 'feng';
function say(name) {
console.log(name);
}
const obj = {
name: 'feng'
}
答案是8个。name/say/name/console/log/name/obj/name
- 变量名、属性名、参数名等,所有声明和引用的名字,都是标识符。我们都知道,JS 中的标识符只能包含字母或数字或下划线(“_”)或美元符号(“$”),且不能以数字开头。这也是 Identifier 的词法特点。
语句(Statement):
statement 是语句,它是可以独立执行的单位,比如
break
、continue
、debugger
、return
或者if 语句
、while 语句
、for 语句
,还有声明语句
,表达式语句
等。我们写的每一条可以独立执行的代码都是语句。语句末尾一般会加一个分号分隔,或者用换行分隔。
下面这些我们经常写的代码,每一行都是一个 Statement:
1
2
3
4
5
6
7
8
9
10
11
12
13
14break;
continue;
return;
debugger;
throw Error();
{}
try {} catch(e) {} finally{}
for (let key in obj) {}
for (let i = 0;i < 10;i ++) {}
while (true) {}
do {} while (true)
switch (v){case 1: break;default:;}
label: console.log();
with (a){}
语句是代码执行的最小单位,可以说,代码是由语句(Statement)构成的。
声明语句(Declaration)
声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个变量、函数、class、import、export 等。比如下面这些语句都是声明语句:
1
2
3
4
5
6
7
8
9const a = 1;
function b(){}
class C {}
import d from 'e';
export default e = 1;
export {e};
export * from 'e';
表达式(Expression)
- expression 是表达式,特点是执行完以后有返回值,这是和语句 (statement) 的区别。下面是一些常见的表达式:
1
2
3
4
5
6
7
8
9
10
11[1,2,3]
a = 1
1 + 2;
-1;
function(){};
() => {};
class{};
a;
this;
super;
a::b;
- expression 是表达式,特点是执行完以后有返回值,这是和语句 (statement) 的区别。下面是一些常见的表达式:
类(Class)
class 的语法也有专门的 AST 节点来表示。整个 class 的内容是 ClassBody,属性是 ClassProperty,方法是ClassMethod(通过 kind 属性来区分是 constructor 还是 method)。
1 | class Guang extends Person{ |
- 模块(Modules):
es module
是语法级别的模块规范,所以也有专门的 AST 节点。- import
- name import:
import {c, d} from 'c';
- default import:
import a from 'a';
- namespaced import:
import * as b from 'b';
- name import:
- export
- name export:
export { a, d };
需要 xxx 中导出 a、b - default export:
export default a;
需要 xxx 中导出 default - all export:
export * from './xxx'
会把 xxx 中所有的非default导出
- name export:
- import
程序体(Program & Directive)
program 是代表整个程序的节点,它有 body 属性代表程序体,存放 statement 数组,就是具体执行的语句的集合。还有 directives 属性,存放 Directive 节点,比如”use strict” 这种指令会使用 Directive 节点表示。
- AST 最外层节点是 File,它有
program、comments、tokens
等属性,分别存放Program
程序体、注释、token
等,是最外层节点。
注释分为块注释和行内注释,对应 CommentBlock
和 CommentLine
节点。
AST的公共属性
每种 AST
都有自己的属性,但是它们也有一些公共的属性:
type:
AST
节点的类型start、end、loc
:start
和end
代表该节点在源码中的开始和结束下标。而loc
属性是一个对象,有line
和column
属性分别记录开始和结束的行列号。leadingComments、innerComments、trailingComments
: 表示开始的注释、中间的注释、结尾的注释,每个AST
节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,想拿到某个AST
的注释就通过这三个属性。extra
:记录一些额外的信息,用于处理一些特殊情况。比如StringLiteral
的value
只是值的修改,而修改extra.raw
则可以连同单双引号一起修改。比如:
const name = 'feng';
,修改value
只能修改值,修改extra.raw
可以连引号一起修改。
常用的库(把源码转换成AST)
- HTML
- htmlparser2
- parse5
- JS
- recast
- uglify-js
- @babel/parser
- Vue
- vue-template-compiler
其他,还有一些支持转换多种语言,比如GoGoCode。
其中用的比较多的有@babel/parser,babel parser
叫 babylon
,是基于 acorn
实现的,扩展了很多语法,可以支持 es next
(现在支持到 es2020)、jsx
、flow
、typescript
等语法的解析。
还有下面会提到的vue-template-compiler
,是vue最常用的解析器。
实战-手写parser(解析)、traverse(转化)、generator(生成代码)
一个代码插件:使用vue-template-compiler把提取template中的class结构
功能:template生成css/less/scss结构树
这里原理很简单,
拓展应用场景(代码混淆、prettier)
代码混淆
我们的代码是给人看的,对于计算机来说,变量名或者执行逻辑没必要容易读。为了让怀有恶意的人很难阅读代码,但不会改变执行的结果。代码混淆的目的就达到了。
其中,最常用就是名字转换。
变量名、函数名,这些,我们平时会注意命名要有含义,但是编译后的代码就不需要了,可以把各种 identifier
的 name
重命名为没有含义的 abcd
,修改作用域中某个变量的名字,同时还要修改用到它的地方,这个可以通过 babel
来演示一下
使用它其中一块,path.scope.rename
的 api。
参考资料:
AST(抽象语法树)以及AST的广泛应用🔥
抽象语法树(AST):理解JavaScript代码的抽象语法树
AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解