# AST抽象语法树
# 前言
每种语言都有很多解析器,使用方式和生成的结果各不相同,开发者可以根据需要选择合适的解析器。(总结就是前端的任何一种语言都可以转化成可以描述的json)
常见的语言转换器: JavaScript
- 最知名的当属babylon,因为他是babel的御用解析器,一般JavaScript的AST这个库比较常用
- acron:babylon就是从这个库fork来的
HTML
- htmlparser2:比较常用
- parse5:不太好用,还需要配合jsdom这个类库
CSS
- cssom、csstree等
- less/sass
XML
- Xmldom
虚拟dom
- snabbdom
注意:个人观点,我们常说的虚拟dom其实也是一种解析器,主要是解决js与html的依赖关系。
# 常见的语法树转换工具
babel-parser,recast,esprima
Babel为当前最流行的代码JavaScript编译器了,其使用的JavaScript解析器为babel-parser (opens new window),最初是从Acorn
项目fork
出来的。Acorn 非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性) 设计了一个基于插件的架构。esprima
的实现也是基于Acorn的。
recast
是基于 @babel/core
、@babel/parser
、@babel/types
等包进行封装开发的。
总结:基本上都来源于 Acorn ,接下来我们主要分析baber-paser。 **
# Babel 运行阶段
三个重要的步骤。
# 解析
首先需要将 JavaScript 字符串经过词法分析、语法分析后,转换为计算机更易处理的表现形式,称之为“抽象语法树(AST)”,这个步骤我们使用了 Babylon (opens new window) 解析器。
# 转换
当 JavaScript 从字符串转换为 AST 后,我们就能更方便地对其进行浏览、分析和有规律的修改,根据我们的需求,将其转换为新的 AST,babel-traverse (opens new window) 是一个很好的转换工具,使得我们能够很便利的操作 AST 。
# 生成
最后,我们将修改完的 AST 进行反向处理,生成 JavaScript 字符串,整个转换过程也就完成了,这一步当中,我们使用到了 babel-generator (opens new window) 模块。
# 什么是AST
之前听过一句话:“如果你能熟练地操作 AST ,那么你真的可以为所欲为。”,当时并不理解其含义,直到真正了解 AST 后,才发现 AST 对编程语言的重要性是不可估量的。 在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。
之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
JavaScript 程序一般是由一系列字符组成的,我们可以使用匹配的字符([], {}, ()),成对的字符('', "")和缩进让程序解析起来更加简单,但是对计算机来说,这些字符在内存中仅仅是个数值,并不能处理这些高级问题,所以我们需要找到一种方式,将其转换成计算机能理解的结构。 我们简单看下面的代码:
let a = 2;
将其转换为 AST 会是怎样的呢,我们使用 astexplorer (opens new window) 在线 AST 转换工具,可以得到以下树结构: json 结构如下:
为了更形象表述,我们将其转换为更直观的结构图形:
AST 的根节点都是 Program ,这个例子中包含了两部分:
一个变量申明(VariableDeclarator),将标识符(Identifier) a 赋值为数值(NumericLiteral) 3。
一个二元表达式语句(BinaryExpression),描述为标志符(Identifier)为 a,操作符(operator) + 和数值(NumericLiteral) 5。
这只是一个简单的例子,在实际开发中,AST 将会是一个巨型节点树,将字符串形式的源代码转换成树状的结构,计算机便能更方便地处理,我们使用的 Babel 插件,也就是对 AST 进行插入/移动/替换/删除节点,创建成新的 AST ,再将 AST 转换为字符串源代码,这便是 Babel 插件的原理,之所以能够“为所欲为”,其原因就是可以将原始代码按照指定逻辑转换为你想要的代码。
很多时候我们不明白这个代表什么含义:VariableDeclarator,Identifier,这个有篇文章JavaScript抽象语法树AST (opens new window),都解释了单词对应含义。(或者babel的官方文档 (opens new window))
例如
# VariableDeclarator
变量声明,kind 属性表示是什么类型的声明,因为 ES6 引入了 const/let。
interface VariableDeclaration <: Declaration {
type: "VariableDeclaration";
declarations: [ VariableDeclarator ];
kind: "var" | "let" | "const";
}
# Identifier
标识符,就是我们写 JS 时自定义的名称,如变量名,函数名,属性名,都归为标识符。相应的接口是这样的:
interface Identifier {
type: 'Identifier';
name: string;
}
# 怎么使用?
简单案例
let a=2;
[1,2].push((val)=>{console.log(val)})
var {parse} = require('@babel/parser');
var t = require('@babel/types');
var traverse = require('@babel/traverse').default
var generate = require('@babel/generator').default;
var code = "let a=2;[1,2].push((val)=>{console.log(val)})";
const ast = parse(code);
traverse(ast, {
CallExpression(path, state) {
if (path.get('callee').isMemberExpression()) {
if (path.get('callee').get('object').isIdentifier()) {
if (path.get('callee').get('object').get('name').node == 'console') path.remove()
}
}
// path.remove()
},
VariableDeclaration:function (path) {
path.node.kind ='var';``
}
})
const output = generate(ast, {
/* options */ }, code);
console.log(output);
{ code: 'var a = 2;\n[1, 2].push(val => {});',
map: null,
rawMappings: null }
总结:菜鸟千万不要尝试使用抽象语法改变代码,否者可能你的代码讲不是你的代码。
有兴趣可以看这个项目 (opens new window)
通过开发 Babel 插件来理解什么是抽象语法树(AST) (opens new window)
高级前端基础-JavaScript抽象语法树AST (opens new window)
**