团队:skFeTeam  本文作者:李世伟

作为前端程序员,webpack,rollup,babel,eslint这些是不是经常用到?他们是打包工具,代码编译工具,语法检查工具。他们是如何实现的呢?本文介绍的抽象语法树,就是他们用到的技术,是不是应该了解一下呢?

本文没有晦涩难懂的理论,也没有大段大段的代码,完全从零开始,小白阅读也无任何障碍。通过本文的阅读,您将会了解AST的基本原理以及使用方法。

前言

什么是抽象语法树?

AST(Abstract Syntax Tree)是源代码的抽象语法结构树状表现形式。下面这张图示意了一段JavaScript代码的抽象语法树的表现形式。

抽象语法树有什么用呢?

IDE的错误提示、代码格式化、代码高亮、代码自动补全等

JSLint、JSHint、ESLint对代码错误或风格的检查等

webpack、rollup进行代码打包等

Babel 转换 ES6 到 ES5 语法

注入代码统计单元测试覆盖率

目录

1.AST解析器

2.AST in Babel

3.Demo with esprima

4.思考题

1.AST解析器

1.1 JS Parser解析器

AST是如何生成的?

能够将JavaScript源码转化为抽象语法树(AST)的工具叫做JS Parser解析器。

JS Parser的解析过程包括两部分

词法分析(Lexical Analysis):将整个代码字符串分割成最小语法单元数组

语法分析(Syntax Analysis):在分词基础上建立分析语法单元之间的关系

常见的AST parser

早期有uglifyjs和esprima

Espree,基于esprima,用于eslint

Acorn,号称是相对于esprima性能更优,体积更小

Babylon,出自acorn,用于babel

Babel-eslint,babel团队维护,用于配合使用ESLint

1.2 词法分析(Lexical Analysis)

语法单元是被解析语法当中具备实际意义的最小单元,简单的来理解就是自然语言中的词语。

Javascript 代码中的语法单元主要包括以下这么几种:

关键字:例如 var、let、const等

标识符:没有被引号括起来的连续字符,可能是一个变量,也可能是 if、else 这些关键字,又或者是 true、false 这些内置常量

运算符: +、-、 *、/ 等

数字:像十六进制,十进制,八进制以及科学表达式等

字符串:因为对计算机而言,字符串的内容会参与计算或显示

空格:连续的空格,换行,缩进等

注释:行注释或块注释都是一个不可拆分的最小语法单元

其他:大括号、小括号、分号、冒号等

1.3 语法分析(Syntax Analysis)

组合分词的结果,确定词语之间的关系,确定词语最终的表达含义,生成抽象语法树。

1.4 示例

以赋值语句为例,使用esprima来解析:

var a = 1;

复制代码

词法分析结果如下,可以看到,分词的结果是一个数组,每一个元素都是一个最小的语法单元:

[

{

"type": "Keyword",

"value": "var"

},

{

"type": "Identifier",

"value": "a"

},

{

"type": "Punctuator",

"value": "="

},

{

"type": "Numeric",

"value": "1"

},

{

"type": "Punctuator",

"value": ";"

}

]

复制代码

语法分析结果如下,把分词的结果按照相互的关系组成一个树形结构:

{

"type": "Program",

"body": [

{

"type": "VariableDeclaration",

"declarations": [

{

"type": "VariableDeclarator",

"id": {

"type": "Identifier",

"name": "a"

},

"init": {

"type": "Literal",

"value": 1,

"raw": "1"

}

}

],

"kind": "var"

}

],

"sourceType": "script"

}

复制代码

1.5 工具网站

经典的JavaScript抽象语法树解析器,网站提供的功能也非常丰富

可以在线查看分词和抽象语法树

Syntax展示抽象语法树,Tokens展示分词

还提供了各种parse的性能比较,看起来Acorn的性能更优秀一点。

AST的可视化工具网站,可以使用各种parse对代码进行AST转换

相同的JavaScript代码,通过各种parser解析的AST结果都是一样的,这是因为他们都参照了同样的AST解析规范

The Estree Spec 规范是 Mozilla 的工程师给出的 SpiderMonkey 引擎输出的 JavaScript AST 的规范文档,也可以参考:SpiderMonkey in MDN

2.AST in Babel

前面已经介绍了AST的内容,下面我们来看看babel是如何使用AST的。

Babel的运行原理

Babel的工作过程经过三个阶段,parse、transform、generate

parse阶段,将源代码转换为AST

transform阶段,利用各种插件进行代码转换

generator阶段,再利用代码生成工具,将AST转换成代码

Parse-解析

Babel 使用 @babel/parser 解析代码,输入的 js 代码字符串根据 ESTree 规范生成 AST

Babel 使用的解析器是 babylon

Transform-转换

接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。也是Babel插件接入工作的部分。

Babel提供了@babel/traverse(遍历)方法维护AST树的整体状态,方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。

Generator-生成

代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。

遍历整个 AST,然后构建可以表示转换后代码的字符串。

Babel使用 @babel/generator 将修改后的 AST 转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap。

3.Demo with esprima

了解了babel的运行原理,我们根据babel的三个步骤来动手写一个demo,加深对AST的理解。

我们准备使用esprima来模拟两个代码转换的功能:

把 == 改为全等 ===

把parseInt(a) 改为 parseInt(a,10)

转换前的代码,before.js:

function fun1(opt) {

if (opt.status == 1) {

console.log('1');

}

}

function fun2(age) {

if (parseInt(age) >= 18) {

console.log('2');

}

}

复制代码

期望转换后的代码,after.js:

function fun1(opt) {

if (opt.status === 1) {//==变成===

console.log('1');

}

}

function fun2(age) {

if (parseInt(age, 10) >= 18) {//parseInt(a)变成parseInt(a,10)

console.log('2');

}

}

复制代码

开始动手,先引入工具包

//引入工具包

const esprima = require('esprima');//JS语法树模块

const estraverse = require('estraverse');//JS语法树遍历各节点

const escodegen = require('escodegen');//JS语法树反编译模块

const fs = require('fs');//读写文件

复制代码

使用esprima parse把源代码转换成AST。怎么样,是不是很简单,一句代码就搞定了。

const before = fs.readFileSync('./before.js', 'utf8');

const ast = esprima.parseScript(before);

复制代码

遍历AST,找到符合转换规则的代码进行转换

estraverse.traverse(ast, {

enter: (node) => {

toEqual(node);//把 == 改为全等 ===

setParseInt(node); //把 parseInt(a) 改为 parseInt(a,10)

}

});

复制代码

再来看看toEqual和setParseInt函数的实现

function toEqual(node) {

if (node.operator === '==') {

node.operator = '===';

}

}

function setParseInt(node) {

//判断节点类型,方法名称,方法的参数的数量,数量为1就增加第二个参数

if (node.type === 'CallExpression' && node.callee.name === 'parseInt' && node.arguments.length === 1) {

node.arguments.push({//增加参数,其实就是数组操作

"type": "Literal",

"value": 10,

"raw": "10"

});

}

}

复制代码

最后,把转换后的AST生成字符串代码,写入文件。

//生成目标代码

const code = escodegen.generate(ast);

//写入文件

fs.existsSync('./after.js') && fs.unlinkSync('./after.js');

fs.writeFileSync('./after.js', code, 'utf8');

复制代码

好了,打开after.js文件看看,是不是已经转换成功了?是不是和我们期望的一样?有没有一种babel的感觉?是的,其实babel也是这么做的,只不过它的转换规则函数相当的复杂,因为需要考虑各种JavaScript的语法情况,工作量巨大,这也就是babel最核心的地方。

再回头看看我们写的demo,是完全遵循babel的三个步骤来做的。第一步parse和第三步generate都非常简单,一句话的事,没什么好说的。重点是Transform,转换规则函数的实现,有人可能会问,你怎么知道,toEqual和setParseInt转换函数要这么写呢?

好的,为了回答这个问题,我们来看看这两个规则的代码转换前后的AST就明白了。

把 == 改为全等 ===

a==b的AST如下:

{

"type": "Program",

"body": [

{

"type": "ExpressionStatement",

"expression": {

"type": "BinaryExpression",

"operator": "==",

"left": {

"type": "Identifier",

"name": "a"

},

"right": {

"type": "Identifier",

"name": "b"

}

}

}

],

"sourceType": "script"

}

复制代码

a===b的AST如下:

{

"type": "Program",

"body": [

{

"type": "ExpressionStatement",

"expression": {

"type": "BinaryExpression",

"operator": "===",

"left": {

"type": "Identifier",

"name": "a"

},

"right": {

"type": "Identifier",

"name": "b"

}

}

}

],

"sourceType": "script"

}

复制代码

比较上面两个AST,是不是只有一个"operator"字段有区别,一个是==, 另一个是===。

再来看看toEqual函数,是不是明白了?只要修改一下node.operator的值就能完成转换了。

function toEqual(node) {

if (node.operator === '==') {

node.operator = '===';

}

}

复制代码

把parseInt(a) 改为 parseInt(a,10)

parseInt(a)的AST如下:

{

"type": "Program",

"body": [

{

"type": "ExpressionStatement",

"expression": {

"type": "CallExpression",

"callee": {

"type": "Identifier",

"name": "parseInt"

},

"arguments": [

{

"type": "Identifier",

"name": "a"

}

]

}

}

],

"sourceType": "script"

}

复制代码

parseInt(a, 10)的AST如下:

{

"type": "Program",

"body": [

{

"type": "ExpressionStatement",

"expression": {

"type": "CallExpression",

"callee": {

"type": "Identifier",

"name": "parseInt"

},

"arguments": [

{

"type": "Identifier",

"name": "a"

},

{

"type": "Literal",

"value": 10,

"raw": "10"

}

]

}

}

],

"sourceType": "script"

}

复制代码

比较这两个AST,看到了吗?只是arguments数组多了下面这个元素。

{

"type": "Literal",

"value": 10,

"raw": "10"

}

复制代码

所以在转换规则函数中,我们把这个元素加进去就能实现转换了。是不是非常简单?

function setParseInt(node) {

//判断节点类型,方法名称,方法的参数的数量,数量为1就增加第二个参数

if (node.type === 'CallExpression' && node.callee.name === 'parseInt' && node.arguments.length === 1) {

node.arguments.push({//增加参数,其实就是数组操作

"type": "Literal",

"value": 10,

"raw": "10"

});

}

}

复制代码

好了,到此为止,这个Demo应该完全理解了吧。

4.思考题

看到这里,你已经明白了AST的原理以及使用方法。下面来看一道题目,检验一下学习成果。

假设a是一个对象,var a = { b : 1},那么a.b和a['b'] ,哪个性能更高呢?

a.b和a['b']的写法,大家经常会用到,也许没有注意过这两种写法会有性能差异。事实上,有人做过测试,两者的性能差距不大,a.b会比a['b']性能稍微好一点。那么,为什么a.b比a['b']性能稍微好一点呢?

我认为,a.b可以直接解析b为a的属性,而a['b']可能会多一个判断的过程,因为[]里面的内容可能是一个变量,也可能是个常量。

这种说法看起来好像很有道理,事实上是不是这样呢?有没有什么证据来证明这个说法吗?

好吧,要想解释清楚这个问题,就只能从V8引擎说起了。

js代码能在cpu上运行,主要是js引擎的功劳,V8引擎是google开发,应用在chrome浏览器和nodejs上,是一个经典的js引擎。上图可以看出,在V8引擎中,js从源代码到机器码的转译主要有三个步骤:Parser(AST) ->Ignition(Bytecode)->TurboFan(Machine Code)

Parser:负责将JavaScript源码转换为Abstract Syntax Tree (AST)

Ignition:interpreter,即解释器,负责将AST转换为Bytecode,解释执行Bytecode;同时收集TurboFan优化编译所需的信息,比如函数参数的类型

TurboFan:compiler,即编译器,利用Ignitio所收集的类型信息,将Bytecode转换为优化的汇编代码

Parser-AST解析器

大家应该很熟悉了,就是我们今天介绍的AST解析器

Ignition-解释器

把AST解析成一种类汇编语言,样子和汇编语言很相似,叫做Bytecode。这种语言和CPU无关,不同cpu的机器上生成的Bytecode都是相同的。

TurboFan-编译器

大家知道,每种cpu的架构和指令集是不同的,对应的汇编语言会有差异。V8在这一步,针对不同的cpu,把Bytecode解析成适合不同cpu的汇编语言。V8可以支持十几种cpu的汇编语言。

现在,我们就来比较一下a.b和a['b']在V8的解析下,到底有什么不同

a.b的测试代码如下:

function test001() {

var a = { b: 1 };

console.log(a.b)

}

test001();

复制代码

a['b']的测试代码如下:

function test002() {

var a = { b: 1 };

console.log(a['b'])

}

test002();

复制代码

先看下他们生成的Bytecode

a.b的Bytecode代码如下:

[generated bytecode for function: test001]

Parameter count 1

Frame size 32

16 E> 000001F6C03D7192 @ 0 : a0 StackCheck

33 S> 000001F6C03D7193 @ 1 : 79 00 00 29 fa CreateObjectLiteral [0], [0], #41, r1

000001F6C03D7198 @ 6 : 27 fa fb Mov r1, r0

46 S> 000001F6C03D719B @ 9 : 13 01 01 LdaGlobal [1], [1]

000001F6C03D719E @ 12 : 26 f9 Star r2

54 E> 000001F6C03D71A0 @ 14 : 28 f9 02 03 LdaNamedProperty r2, [2], [3]

000001F6C03D71A4 @ 18 : 26 fa Star r1

60 E> 000001F6C03D71A6 @ 20 : 28 fb 03 05 LdaNamedProperty r0, [3], [5]

000001F6C03D71AA @ 24 : 26 f8 Star r3

54 E> 000001F6C03D71AC @ 26 : 57 fa f9 f8 07 CallProperty1 r1, r2, r3, [7]

000001F6C03D71B1 @ 31 : 0d LdaUndefined

63 S> 000001F6C03D71B2 @ 32 : a4 Return

Constant pool (size = 4)

Handler Table (size = 0)

复制代码

a['b']的Bytecode代码如下:

[generated bytecode for function: test002]

Parameter count 1

Frame size 32

16 E> 0000022E1C7D6DC2 @ 0 : a0 StackCheck

33 S> 0000022E1C7D6DC3 @ 1 : 79 00 00 29 fa CreateObjectLiteral [0], [0], #41, r1

0000022E1C7D6DC8 @ 6 : 27 fa fb Mov r1, r0

46 S> 0000022E1C7D6DCB @ 9 : 13 01 01 LdaGlobal [1], [1]

0000022E1C7D6DCE @ 12 : 26 f9 Star r2

54 E> 0000022E1C7D6DD0 @ 14 : 28 f9 02 03 LdaNamedProperty r2, [2], [3]

0000022E1C7D6DD4 @ 18 : 26 fa Star r1

59 E> 0000022E1C7D6DD6 @ 20 : 28 fb 03 05 LdaNamedProperty r0, [3], [5]

0000022E1C7D6DDA @ 24 : 26 f8 Star r3

54 E> 0000022E1C7D6DDC @ 26 : 57 fa f9 f8 07 CallProperty1 r1, r2, r3, [7]

0000022E1C7D6DE1 @ 31 : 0d LdaUndefined

66 S> 0000022E1C7D6DE2 @ 32 : a4 Return

Constant pool (size = 4)

Handler Table (size = 0)

复制代码

比较一下两者的Bytecode,你会发现它们完全相同,这就说明,这两种写法在Bytecode层及以下的执行,性能是没有差别的。事实上,它们有差别,就只能往上找,上面就只有Parser阶段了。我们再来看看它们的AST有什么区别。

a.b的AST代码如下:

{

"type": "Program",

"body": [

{

"type": "ExpressionStatement",

"expression": {

"type": "MemberExpression",

"computed": false,

"object": {

"type": "Identifier",

"name": "a"

},

"property": {

"type": "Identifier",

"name": "b"

}

}

}

],

"sourceType": "script"

}

复制代码

a['b']的AST代码如下:

{

"type": "Program",

"body": [

{

"type": "ExpressionStatement",

"expression": {

"type": "MemberExpression",

"computed": true,

"object": {

"type": "Identifier",

"name": "a"

},

"property": {

"type": "Literal",

"value": "b",

"raw": "'b'"

}

}

}

],

"sourceType": "script"

}

复制代码

我们发现唯一的区别就是"computed"属性,a.b是false,a['b']是true,说明在解析成AST时,a['b']比a.b多了一个计算的过程。由此我们断定,两者微小的差异应该就在这里。好了,证据找到了,现在应该没有疑问了吧。

收尾

看到这里,你不但了解了AST的相关知识,还知道了V8引擎是如何解析js代码的,是不是有所收获呢?如果你觉得这篇文章对你有用,还请顺便点个赞,非常感谢(90度鞠躬)。

想了解skFeTeam更多的分享文章,可以点这里,谢谢~

java抽象语法树(ast),【你应该了解的】抽象语法树AST相关推荐

  1. java AST 表达式_java 编译时注解-AST 抽象语法树简介

    AST 语法入门 以前使用 Lombok 一直觉得是一个很棒的设计,可以同时兼顾注解的遍历和运行的性能. 运行时注解一直因为性能问题被人诟病. 自己尝试写过一些框架,但是耗费了比较多的精力,因为 AS ...

  2. java抽象语法树_抽象语法树(AST)

    抽象语法树(AST) 最近在做一个类JAVA语言的编译器,整个开发过程,用抽象语法树(Abstract SyntaxTree,AST)作为程序的一种中间表示,所以首先就要学会建立相对应源代码的AST和 ...

  3. java抽象语法树_抽象语法树AST的全面解析(一)

    Javac编译概述 将.java源文件编译成.class文件,这一步大致可以分为3个过程: 1.把所有的源文件解析成语法树,输入到编译器的符号表: 2.注解处理器的注解处理过程: 3.分析语法树并生成 ...

  4. 【转】抽象语法树简介(AST)

    引用地址:http://blog.chinaunix.net/uid-26750235-id-3139100.html 抽象语法树简介 (一)简介 抽象语法树(abstract syntax code ...

  5. AST抽象语法树的基本思想

    AST抽象语法树的基本思想 前言 AST概述 AST结构 AST解析 转换 生成 前言 在阅读java ORM框架spring data jpa的源码时,发现Hibernate(spring data ...

  6. AST(抽象语法树)超详细

    自己研究的东西会用到AST,就自己通过查阅资料,整理一下. 本文目录 第一部分:AST的作用 第二部分:AST的流程 第三部分: Eclipse AST的获取与访问 第一部分:AST的作用 首先来一个 ...

  7. php ast 抽象语法树,AST抽象语法树的基本思想

    AST抽象语法树的基本思想 前言 AST概述 AST结构 AST解析 转换 生成 前言 在阅读java ORM框架spring data jpa的源码时,发现Hibernate(spring data ...

  8. 抽象语法树手动生成--java实现

    本人博客内编译原理文章的配套资源jar包,包括词法分析,语法分析,中间代码生成,静态语义检查,代码解释执行以及抽象语法树的手动生成:https://download.csdn.net/download ...

  9. ast抽象语法树_新抽象语法树(AST)给 PHP7 带来的变化

    本文大部分内容参照 AST 的 RFC 文档而成:https://wiki.php.net/rfc/abstract_syntax_tree,为了易于理解从源文档中节选部分进行介绍. 我的官方群点击此 ...

  10. AST(抽象语法树)实战入门:js逆向中滑块加密if语句转化

    概述:AST 抽象语法树 实战 入门 案例 js逆向 js滑块 js加密 极验 瑞数 阿里滑块 5秒盾 ​引言: AST算得上是高端技能.如果把爬虫技能分为初中高三个阶段的话.常规的JS逆向找找参数, ...

最新文章

  1. Android调用前置摄像头的方法
  2. IT人员健康信号之大脑保养
  3. [转载]iphone开发--改变UIPageControl里的小点的颜色
  4. 短作业优先算法的缺点
  5. 捕获事件要比冒泡事件先触发
  6. Linux 系统启动
  7. python把工作簿拆分为工作表_如何批将Excel的多个Sheet工作表拆分为独立的工作簿?...
  8. Saas 多租户模式介绍
  9. 【实践】关于智能蛇的三次尝试
  10. MOS管开启过程中VGS的台阶——米勒平台?
  11. JavaScript学习第十九天
  12. 德语语法笔记——名词的变格
  13. 15瓶可乐,其中有一瓶过期了,找出有毒的可乐的问题
  14. 59.java编程思想——创建窗口和程序片 Swing
  15. 第三批入围企业公示!年度TOP100智能网联供应商评选
  16. coloros11跟Android,ColorOS11好不好用 ColorOS11升级使用体验
  17. 一些代码和心得记录我的成长经历
  18. How to import IDF files within Icepak
  19. 收到私信问:怕在试用期被辞退!我:......
  20. HTPC知识普及第四讲:解码需软硬兼施2

热门文章

  1. ADSP21489之CCES开发笔记(二)
  2. 电流互感器同名端是什么意思
  3. 如何让CocosCreator3.x引擎启动提速60%
  4. 数字取证autopsy工具用法
  5. bert生成句子向量
  6. Vue 获取dom元素中的自定义属性值
  7. html教程自适应,html自适应界面
  8. java彩虹雨_Java字符串分割
  9. 他们凭什么赢?近看“2020大数据产业最具投资价值企业”
  10. 超好用的RAW图片处理工具:RAW Power for Mac中文版