AST

AST(Abstract Syntax Tree) 抽象语法树

抽象语法树(Abstract Syntax Tree)简称 AST,是源代码的抽象语法结构的树形表现形式

浏览器就是通过将 js 代码转化为抽象语法树来进行分析

程序运行方式

通常程序有两种运行方式:

  • 静态编译, 静态编译的程序在执行前全部被翻译为机器码

  • 动态解释, 而动态解释执行的则是一句一句边运行边翻译

AOT (Ahead-Of-Time - 预先编译) 编译器在程序创建期间(运行时之前)编译代码

JIT (Just-In-Time - 实时编译) 编译器在程序执行期间运行,即时编译代码

一般来说,只有静态语言才适合 AOT 编译为本地机器代码,因为机器语言通常需要知道数据的类型,而动态语言中的类型事先并不确定。因此,动态语言通常被解释或 JIT 编译

分析AST

JavaScript 是动态解释型语言,一般通过 词法分析 -> 语法分析 -> 语法树,分析转化后就可以开始执行

词法分析(lexical analysis)

也叫扫描, 当词法分析源代码的时候,它会一个一个字符的读取代码, 然后按照一定的规则合成标识

esprima

1
2
3
4
5
6
7
8
9
10
11
const esprima = require('esprima');
const code = 'var a = 3';
const word = esprima.tokenize(code);
console.log(word);

// [
// { type: 'Keyword', value: 'var' },
// { type: 'Identifier', value: 'a' },
// { type: 'Punctuator', value: '=' },
// { type: 'Numeric', value: '3' }
// ]

语法分析

将词法分析出来的数组转换成树的形式,同时验证语法错误

1
2
3
4
5
6
7
8
9
10
11
Script {
type: 'Program',
body: [
VariableDeclaration {
type: 'VariableDeclaration',
declarations: [Array],
kind: 'var'
}
],
sourceType: 'script'
}

示例1

结合上图, 可以初见简单的 js 语句转换成语法树的结构

  • 左侧 var 关键字对应着 VariableDeclaration 下的 kind 值

  • 在 declarations 字段下有声明的标识符和初始值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Program: {
"type": "Program",
"body": [
{
"type": "VariableDeclaration", // 该语句的类型, 变量声明的语句
"declarations": [ // 声明内容的数组,里面每一项也是一个对象
{
"type": "VariableDeclarator", // 该语句的类型
"id": { // 变量名称的对象
"type": "Identifier", // 类型标示符
"name": "ast" // 标识符名称
},
"init": { // 初始化变量值的对象
"type": "Literal",
"value": "ast", // ast 值不带引号
"raw": "'ast'" // "'ast'" 值带引号
}
}
],
"kind": "var" // 变量声明的关键字 --> var
}
],
"sourceType": "module"
}

示例2

一段代码转换成的抽象语法树是一个对象,顶层有 type 和 body 两个属性; body 是一个数组, body 的每一项都是一个对象,里面包含了所有的对于该语句的描述信息; for 语句内嵌套if语句, if 语句将存在于 for 的 body 数组里

AST应用

  • 代码检查eslint, babel转码

  • 代码混淆压缩

  • 优化变更代码,改变代码结构使达到想要的结构

  • 代码打包工具webpack

  • TypeScript、JSX等转化为原生Javascript

AST 三步走

  • esprima 把源码转化为AST

  • estraverse 遍历并更新AST

  • escodegen 将AST重新生成源码

实践

实现箭头函数转化

首先看一下尖头函数和普图函数的区别

分别声明两个函数

1
2
const fn = () => 3
const fun = function () {return 3}

两个函数解析成语法树后分别对应的结构如下

const fn = () => 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14

{
"type": "ArrowFunctionExpression",
"id": null,
"expression": true,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "Literal",
"value": 3,
"raw": "3"
}
}

const fun = function () {return 3}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"type": "FunctionExpression",
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "Literal",
"value": 3,
"raw": "3"
}
}
]
}
}

从上面两段代码来看, 两者的主要区别是type 和 body 属性的子集合, 我们只需要把箭头函数的语法树结构转成普通函数后, 在生成新的代码即可

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');
const code = `const fn = () => 3`;
// 生成 AST
const ast = esprima.parseScript(code);
// 转换 AST,只会遍历有 type 的属性, traverse 方法中有进入和离开两个钩子函数
estraverse.traverse(ast, {
// 进入节点的钩子函数
enter(node) {
if (node.type === 'ArrowFunctionExpression') {
node.type = 'FunctionExpression';
let param = {};
if (node.body.type === 'BlockStatement') {
param = { type: 'BlockStatement', body: node.body.body };
} else {
param = { type: 'BlockStatement', body: [{
type: 'ReturnStatement',
argument: node.body
}]};
}
node.body = param;
node.expression = false;
}
console.info(`entry ===== ${node.type}`);
},
// 离开节点的钩子函数
leave(node) {
console.info(`leave ===== ${node.type}`);
},
})

// 处理完之后的语法树结构, 从新生成代码
const newFun = escodegen.generate(ast);
console.info(newFun);

事实上 babel 语法转化, 也是基于 ast 处理的, 我们可以直接使用 babel 写好的功能, 实现箭头函数转化

1
2
3
4
5
6
const babel = require('@babel/core');
const code = `const fn = () => 3`;
const newCode = babel.transform(code, {
plugins: ['@babel/plugin-transform-arrow-functions'],
})
console.log(newCode.code)

使用 babel

如果我们自己去写, 需要花费大量的时间去了解, 学习相关节点类型等知识, 如果真的工作中有需求, 可以考虑 babel + @babel/type 开发 plugin. @babel/type提供了大量的api供开发者使用

babel 插件返回一个对象,其 visitor 属性是这个插件的主要访问者, Visitor 中的每个函数接收2个参数:path 和 state

path.node 则存放着语法树的节点信息

1
2
3
4
5
6
7
8
export default function({ types: t }) {
return {
visitor: {
Identifier(path, state) {},
ASTNodeTypeHere(path, state) {}
}
};
};

实现箭头函数转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const babel = require('@babel/core');
const type = require('@babel/types');
const code = `const fn = () => {return 3}`;

const arrowFnPlugin = {
visitor: {
ArrowFunctionExpression(path) {
const node = path.node;
const params = node.params;
const body = node.body;
// babel type 的方法
// https://babeljs.io/docs/en/babel-types#bindexpression
if (!type.isBlockStatement(body)) {
body = type.blockStatement([body]);
}
const functionExpression = type.functionExpression(null, params, body);
// babel plugin 方法
// https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-writing-your-first-babel-plugin
path.replaceWith(functionExpression);
},
},
};

const newArrow = babel.transform(code, {
plugins: [arrowFnPlugin]
});

console.log(newArrow.code);

demo

按需加载

分析语法树


将import {Button} from ‘antd’的形式转换为 import Button from ‘antd/es/button’ 的形式的过程

附件资料

在线解析AST explorer

支持可视化AST visualizer

词法解析 esprima

node类型参考附录

babel 插件开发

babel 对抽象语法的简介

编写一个babel插件

参考

一文助你搞懂 AST

AST 抽象语法树学习

返回
顶部