故事起源于一个很小问题,我写了个代码,被质疑有问题:简化之后大概如下:
let a; const x = {b: 123}; a = 123, delete x
被质疑的主要原因是第三行 a =123 的后面为什么是逗号,不是分号。坦白来说,我是简单的手误,将分号错写成了逗号。但是感觉貌似应该也没有什么问题,毕竟 uglifyjs 会将某些语句进行合并,将分号变成逗号。继而再一想,uglifyjs 是如何来进行代码压缩的、它是如何知道该合并哪些语句,不合并哪些语句的、它又有哪些合并规则?于是有了本文。
1. AST(抽象语法树)
要想了解 JS 的压缩原理,需要首先了解 AST。
抽象语法树:AST(Abstract Syntax Tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
举个例子:
从上面两个例子中,可以看出 AST 是源代码根据其语法结构,省略一些细节(比如:括号没有生成节点),抽象成树形表达。抽象语法树在计算机科学中有很多应用,比如编译器、IDE、压缩代码、格式化代码等。[1]
2. 代码压缩原理
了解了 AST 之后,我们再分析一下 JS 的代码压缩原理。简单的说,就是
1. 将 code 转换成 AST
2. 将 AST 进行优化,生成一个更小的 AST
3. 将新生成的 AST 再转化成 code
PS:具体的 AST 树大家可以在 astexplorer 上在线获得
babel,eslint,v8 的逻辑均与此类似,下图是我们引用了 babel 的转化示意图:
以我们之前被质疑的代码为例,看看它在 uglify 中是怎么样一步一步被压缩的:
// uglify-js 的版本需要为 2.x, 3.0 之后 uglifyjs 不再暴露 Compressor api
// 2.x 的 uglify 不能自动解析 es6,所以这里先切换成 es5
// npm install uglify-js@2.x
var UglifyJS = require('uglify-js');
// 原始代码
var code = `var a;
var x = {b: 123};
a = 123,
delete x`;
// 通过 UglifyJS 把代码解析为 AST
var ast = UglifyJS.parse(code);
ast.figure_out_scope();
// 转化为一颗更小的 AST 树
compressor = UglifyJS.Compressor();
ast = ast.transform(compressor);
// 再把 AST 转化为代码
code = ast.print_to_string();
// var a,x={b:123};a=123,delete x;
console.log("code", code);
到这里,我们已经了解了 uglifyjs 的代码压缩原理,但是还没有解决一个问题——为什么某些语句间的分号会被转换为逗号,某些不会转换。这就涉及到了 uglifyjs 的压缩规则。
3. 代码压缩规则
由于 uglifyjs 的代码压缩规则很多,我们这里只分析与本文中相关的部分:
uglifyjs 的全部压缩规则可以参见:《[解读 uglifyJS(四)——Javascript 代码压缩](https://rapheal.sinaapp.com/2014/05/22/uglifyjs-squeeze/#more-705)》
连续的 ” 表达式语句 ” 可以合并成一个逗号表达式
PS:在线 demo
这其中需要注意的是只有“表达式语句”才能被合并,那么什么是表达式语句呢?
表达式 VS 语句 VS 表达式语句
表达式 :表达式都会返回一个值,可以放在任何一个需要值的地方
例如:
a; // 返回 a 的值
b + 3; // 返回 b + 3 的结果
语句: 语句是一个行为,通常利用一个或多个关键字来完成给定的任务。程序由一系列语句构成。其中流控制语句有:if/while/for 等。
例如:
if(x > 0) {...}
for(var i = 0;i < arr.length; i ++) {...}
const a = 123;
表达式语句 :既是表达式,又是语句
例如:
A();
function() {}();
delete x.b;
b = b + 3;
综上所述,因为 a = 123 和 delete x 都是表达式语句,所以分号被转换为逗号。而 var x = {b:123} 则因为是声明语句,所以和 a =123 不会合并,分号不会被转换。但 var x = {b:123} 和第一行 var a 又触发了另外一条规则,
多个 var 声明可以压缩成一个 var 声明
所以第一行和第二行会被合并为 var a,x={b:123}
4. 总结
在本文中,我们讨论了什么是抽象语法树,uglifyjs 的压缩原理,以及相应的压缩规则,最终明晰了为什么代码会被压缩成我们得到的样子,希望对大家有所帮助。
参考文献
[1]《抽象语法树在 JavaScript 中的应用》
[2]《javascript 代码是如何被压缩的》
[3]《[译]JavaScript 中: 表达式和语句的区别》
[4]《解读 uglifyJS(四)——Javascript 代码压缩》