做者:陈晓强javascript
平常工做中,咱们会碰到js代码解析的场景,好比分析代码中require了哪些包,有些什么关键API调用,大部分状况使用正则表达式来处理,可一旦场景复杂,或者依赖于代码上下文时,正则就很难处理了,这时候就要用到抽象语法树。常见的uglify、eslint、babel、webpack等等都是基于抽象语法树来处理的,如此强大,有必要好好了解一下。java
抽象语法树即:Abstract Syntax Tree。简称AST,见下图。node
图中树状数据结构即AST,从这个过程能够看到将代码转成AST后,经过操做节点来改变代码。webpack
得到抽象语法树的过程为:代码 => 词法分析 => 语法分析 => AST
词法分析
:把字符串形式的代码转换为令牌(tokens)流。
语法分析
:把一个令牌流转换成 AST 的形式。这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操做。
以下图,代码为一个简单的函数声明。词法分析阶段,将代码做为字符串输入得到关键词,图中function
、square
、(
、)
、{
、}
等都被识别为关键词(稍微回忆下编译原理,字符挨个入栈,符合必定规则即出栈)。语法分析阶段,对关键词的组合造成一个个节点,如n*n这3个关键词组合成二元表达式
,关键词return与二元表达式组合成return语句
。最后组合成一个函数声明语句
。git
如何得到AST已经简单介绍了,那AST最终应该以什么样的数据结构存在呢,先看看上述函数声明的AST结构github
那解析的依据是什么,为何要以上图的结构出现,业界已经有了一套成熟的规范。web
在v8引擎以前,最先js引擎是SpiderMonkey,第一个版本由js做者Brendan Eich设计,后交给Mozilla组织维护。js引擎在执行js文件时,都会先将js代码转换成抽象语法树(AST)。有一天,一位Mozilla工程师在FireFox中公开了这个将代码转成AST的解析器Api,也就是Parser_API[1],后来被人整理到github项目estree[2],慢慢的成了业界的规范。正则表达式
上面提到的Parser_API
是规范的原文,中文版:Parser_API[3],但读起来并不太友好,推荐直接读整理后的git项目estree,打开项目地址,以下图 express
es5.md
为ES5规范,仅列出ES5的内容,
es2015.md
为ES6规范,但只列出了针对ES5新增的内容,依次类推,最后的
es2019.md
即ES10是对ES9的补充,仅有一条规则。
打开最基础的es5.md
,能够看到全部语法基础,这里跟你们一块儿读一下大类,细分类别就略过了。读规范时可使用https://astexplorer.net/ 辅助阅读,能够实时输出AST。npm
interface Node {
type: string;
loc: SourceLocation | null;
}
复制代码
定义AST中节点基本类型,其余全部具体节点都须要实现以上接口,即每一个节点都必须包含type、loc两个字段
type
字段表示不一样的节点类型,下边会再讲一下各个类型的状况,分别对应了 JavaScript 中的什么语法。你能够从这个字段看出这个节点实现了哪一个接口
loc
字段表示源码的位置信息,若是没有相关信息的话为 null,不然是一个对象,包含了开始和结束的位置。接口以下
interface SourceLocation {
source: string | null;
start: Position;
end: Position;
}
复制代码
每一个 Position
对象包含了行(从1开始)和列(从0开始)信息,接口以下
interface Position {
line: number; // >= 1
column: number; // >= 0
}
复制代码
interface Program <: Node {
type: "Program";
body: [ Directive | Statement ];
}
复制代码
一棵完整的程序代码树,通常做为根节点
interface Identifier <: Expression, Pattern {
type: "Identifier";
name: string;
}
复制代码
标识符,咱们写代码时自定义的名称,如变量名、函数名、属性名。
interface Literal <: Expression {
type: "Literal";
value: string | boolean | null | number | RegExp;
}
复制代码
字面量,如“hello”
、true
、null
、100
、/\d/
这些,注意字面量自己也是一个表达式语句(ExpressionStatement)
interface Function <: Node {
id: Identifier | null;
params: [ Pattern ];
body: FunctionBody;
}
复制代码
一个函数声明或者表达式,id是函数名,params是标识符数组,body是函数体,也是一个语句块。
interface Statement <: Node { }
复制代码
语句,子类有不少,块语句
、if/switch语句
、return语句
、for/while语句
、with语句
等等
interface Declaration <: Statement { }
复制代码
声明,子类主要有变量申明、函数声明。
interface Expression <: Node { }
复制代码
表达式,子类不少,有二元表达式(n*n
)、函数表达式(var fun = function(){}
)、数组表达式(var arr = []
)、对象表达式(var obj = {}
)、赋值表达式(a = 1
)等等
interface Pattern <: Node { }
复制代码
模式,主要在 ES6 的解构赋值中有意义(let {name}
= user,其中{name}部分为ObjectPattern
),在 ES5 中,能够理解为和Identifier
差很少的东西。
经过以上规范解读,知道了最终要生成的AST以什么样的结构存在,对于javascript的解析,业界已经有不少成熟的解析器,能够将js代码转换成符合规范的AST
AST基础篇介绍完毕,下篇将从实践的角度继续介绍
References
[1] Parser_API
[2] estree
[3] Parser_API(中文)
若是你以为这篇内容对你有价值,请点赞,并关注咱们的官网和咱们的微信公众号(WecTeam):