什么是AST
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有三个分支的节点来表示。
和抽象语法树相对的是具体语法树(通常称作分析树)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树,然后从分析树生成AST。一旦AST被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。 (摘自维基百科)
JS的编译执行流程
js执行的第一步是读取js文件中的字符流,然后通过词法分析生成token,之后再通过语法分析生成AST(Abstract Syntax Tree),最后生成机器码执行。
词法分析
词法分析是计算机科学中将字符序列转换为单词(Token)序列的过程。进行词法分析的程序或者函数叫作词法分析器(Lexical analyzer,简称Lexer),也叫扫描器(Scanner)。
其作用是将一行行的源码拆解成一个个 token。所谓 token,指的是语法上不可能再分的、最小的单个字符或字符串。以代码为例子,以下就是生成的token序列
例如将 const a = 1 转换为token的话,结果大概如下
[
{type: 关键字, value: const},
{type: 标识符, value: a},
{type: 赋值操作符, value: =},
{type: 常数, value: 1}
]
词法分析器里,每个关键字是一个Token,每个标识符是一个Token,每个操作符是一个Token,每个标点符号也都是一个Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等)。最终,整个代码将被分割进一个tokens列表(或者说一维数组)。
语法分析
语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确.
其作用是将上一步生成的 token 数据,根据语法规则转为 AST。同时也会去验证语法,语法有错的话会抛出语法错误
面对一串代码,先通过词法分析,获得第一个token,为其建立一个ast节点,此时的ast节点的属性以及子节点都不完整。
为了补充这些缺少的部分,接下来移动到下一个单词,生成token,并且将其转换成子节点,添加进现有的ast中,然后重复这个 移动&生成 的递归的过程。
根据上面的词法分析的例子看const a = 1是怎么变成一颗ast的。
在线生成ast网站:https://astexplorer.net/
-
读取const,生成一个VariableDeclaration节点
-
读取a,新建VariableDeclarator节点
-
读取=
-
读取1,新建NumericLiteral节点
-
将NumericLiteral赋值给VariableDeclarator的init属性
-
将VariableDeclarator赋值给VariableDeclaration的declaration属性
补充说明:
每一个含有type属性的对象这样的每一层结构也被叫做 节点(Node)。一个AST可以由单一的节点或是成百上千个节点构成。它们组合在一起可以描述用于静态分的程序语法。每一种类型的节点定义了一些附加属性用来进一步描述该节点类型。 -
节点类型
针对上图出现的节点说明
【VariableDeclaration】
变量声明,kind属性表示是什么类型的声明,因为ES6引入了const/let。declarations表示声明的多个描述,因为我们可以这样:let a = 1, b = 2
【VariableDeclarator】
变量声明的描述,id表示变量名称节点,init表示初始值的表达式
【Identifier】
标志符,如变量名,函数名,属性名,都归为标识符。
【Literal】
字面量,一个值的字面量
除了上述出现的节点类型,ast中还有很多其他节点类型,如函数申明节点,if语句节点,switch语句节点等等;
JavaScript 生成的所有 AST 节点类型可以访问https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md -
公共属性
类型 | 说明 |
---|---|
type | AST 节点的类型 |
start | 记录该节点代码字符串起始下标 |
end | 记录该节点代码字符串结束下标 |
loc | 内含 line、column 属性,分别记录开始结束的行列号 |
leadingComments | 开始的注释 |
innerComments | 中间的注释 |
trailingComments | 结尾的注释 |
extra | 额外信息 |
换流程图来看
问题:操作符等号去哪里了?
在 AST(抽象语法树)中,赋值操作的等号(=)通常不会以节点的形式显示在树中。这是因为 AST 更关注表达式的组织结构和语义,而不是每个具体的字符。
赋值操作的等号通常被表示为 AST 中的一个赋值表达式节点,该节点具有左侧和右侧两个子节点,分别表示被赋值的目标和赋值的值
在这个示例中,我们有一个变量声明语句(VariableDeclaration),它有一个变量声明器(VariableDeclarator)作为子节点。变量声明器包含一个标识符(Identifier)节点表示变量名(“a”),和一个数值字面量(NumericLiteral)节点表示赋给变量的值(1)。
所以,等号本身并没有直接显示在树形结构中,而是通过赋值表达式节点的左右子节点来表示赋值操作
AST的应用:
- 开发辅助:eslint、prettier、ts检查
- 代码变更:压缩、混淆、css modules
- 代码转换:jsx、vue、ts转换为js
- 代码混淆和反混淆
- wx小程序热更新
- 一套代码多端使用
反混淆
- 应对反爬虫:通过找规律解析出密钥,然后把加密过的路径进行解密
- 阅读他人网站:可以找到插件或者AI,对源码进行反混淆
wx小程序热更新
想在原有代码上执行一段后端接口获取的代码,但微信小程序禁用了eval,new Function,因此可以把想要运行的代码转化为AST,再转为要运行的代码。
Babel
编译原理
- 我们需要知道 3 个 Babel 处理流程中的重要工具;
- 解析
- Babylon是一个解析器,它可以将javascript字符串,转化为更加友好的表现形式,称之为抽象语法树;
- 在解析过程中有两个阶段:词法分析和语法分析,
- 词法分析阶段:字符串形式的代码转换为令牌(tokens)流,令牌类似于AST中的节点;
- 语法分析阶段:把一个令牌流转化为AST的形式,同时这个阶段会把令牌中的信息转化为AST的表述结构
- Parser
- 转换
- babel-traverse 模块允许你浏览、分析和修改抽象语法树(AST Abstract Syntax Tree)
- Babel接收解析得到的AST并通过babel-traverse对其进行深度优先遍历,在此过程中对节点进行添加、更新及移除操作。
- babel-traverse 模块允许你浏览、分析和修改抽象语法树(AST Abstract Syntax Tree)
- 生成
- babel-generator 模块用来将转换后的抽象语法树(AST Abstract Syntax Tree)转化为Javascript 字符串
- 将经过转换的AST通过babel-generator再转换为js代码,过程及时深度遍历整个AST,然后构建转换后的代码字符串。
- babel-generator 模块用来将转换后的抽象语法树(AST Abstract Syntax Tree)转化为Javascript 字符串
- 解析
Babel 中重要的对象Visitor
babel在处理一个节点时,是以访问者的形式获取节点的信息,并进行相关的操作,这种操作是通过visitor对象实现的。
在visitor中定义了处理不同节点的函数。
visitor: {
Program: {
enter(path, state) {
console.log('start processing this module...');
},
exit(path, state) {
console.log('end processing this module!');
}
},
ImportDeclaration:{
enter(path, state) {
console.log('start processing ImportDeclaration...');
},
exit(path, state) {
console.log('end processing ImportDeclaration!');
}
}
}
转换&生成
举个例子
test.map(i=>{
return i
})
使用在线转AST,AST树形遍历转换后的结构
我们从 ExpressionStatement开始往树形结构里面走,看到它的内部属性有callee,type,arguments,所以我们再依次访问每一个属性及它们的子节点。
ExpressionStatement 进入
CallExpression 进入
MemberExpression 进入
Identifier 进入
Identifier 离开
Identifier 进入
Identifier 离开
MemberExpression 进入
ArrowFunctionExpression 进入
···
ArrowFunctionExpression 离开
CallExpression 离开
ExpressionStatement 离开
Program 离开
Babel 的转换步骤全都是这样的遍历过程。
解析好树结构后,我们手动对箭头函数进行转换。
对比两张图,发现主要不一样的地方就是两个函数的arguments.type
babel实现转换的代码
//babel核心库,用来实现核心的转换引擎
let babel = require('babel-core');//可以实现类型判断,生成AST节点
let types = require('babel-types');
let code = `codes.map(code=>{return code.toUpperCase()})`;//转换语句
//visitor可以对特定节点进行处理
let visitor = {
ArrowFunctionExpression(path) {//定义需要转换的节点,这里拦截箭头函数
let params = path.node.params
let blockStatement = path.node.body
//使用babel-types的functionExpression方法生成新节点
let func = types.functionExpression(null, params, blockStatement, false, false) //替换节点
path.replaceWith(func) //
}
}
//将code转成ast
let result = babel.transform(code, {
plugins: [
{ visitor }
]
})
console.log(result.code)
直接使用babel的核心包@babel/traverse和@babel/genertator来实现转化和生成
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
const code = `function square(n) {
return n * n;
}`;
const ast = parser.parse(code);
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {//标志符为nde 替换成x
path.node.name = "x";
}
},
});
generate(ast, {}).code
何时使用setTimeout(fn, 0)
- 等待渲染结束
- ios不可连续调用原生API