解读uglifyJS ——Javascript代码压缩

2,272 阅读6分钟

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

前言

我们都知道,代码在上线前都会进行压缩,以此来减少源码的体积,提升加载速度,所以代码压缩是一个非常重要的步骤,所以今天向大家介绍uglifyJS的原理以及压缩规则。

AST(抽象语法树)

要想了解JS的压缩原理,需要首先了解AST。因为压缩的第一步就是把代码转成AST。

抽象语法树:AST(Abstract Syntax Tree),就是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

比如:

image.png

可以看出AST是源代码根据其语法结构,省略一些细节(比如:括号没有生成节点),抽象成树形表达。抽象语法树在计算机科学中有很多应用,比如编译器、IDE、压缩代码、格式化代码等。

压缩原理

压缩代码分为三个步骤

  1. 将code转换成AST
  2. 将AST进行优化,生成一个更小的AST
  3. 将新生成的AST再转化成code

规则

首先我们先安装uglifyJSnpm isntall uglify-js。然后在根目录新建一个文件demo.js,在里面写测试代码。然后在命令行运行 uglifyjs demo.js --mangle-props keep_quoted -c -m 进行压缩。(这里使用的uglify-js的版本是3.14.3)

1.表达式压缩

1.1 表达式预计算

将可预先计算的表达式替换成其计算结果,同时要比较原来表达式以及生成后的结果的大小,取短的。

// --------------压缩前-------------
var expr1 = 1 + 1;
var expr2 = 1 / 3;

// --------------压缩后-------------
var expr1=2,expr2=1/3;

这里之所以只计算expr1而不计算expr2,是因为由于计算出来的值0.3333333333333比1/3要长,所以不预计算。

1.2 优化true跟false

正常情况下会把:
true变成!0,节省2个字符
false变成!1,节省3个字符
有人会疑问:这里为什么不直接把true变成1,false变成0呢?因为这样会把一个布尔类型变成数字类型参与某些运算导致运行时混乱。
但是有没有什么情况比较特殊,可以把true变成1,false变成0呢?答案是有的:就是再参与==以及!=运算时。

// --------------压缩前-------------
var expr1 = true;
var expr2 = false;
true == A;
false == A;

// --------------压缩后-------------
var expr1=!0,expr2=!1;A,A;

1.3 根据&& ||短路的特性压缩表达式

// --------------压缩前-------------
true && A();
false && A();
true || A();
false || A();

// --------------压缩后-------------
A(),A();  // 效果显著

2.运算符缩短

缩短赋值表达式,对于a = a + b这样的赋值表达式,可以缩短成 a += b

这里说起来可能有点绕,但是想一下也很容易理解这条规则的详细判断:

  1. 必须是=号赋值语句
  2. =号左侧只能是变量,不能为表达式等
  3. =号右侧必须为二元操作表达式,并且符号是为数组['+', '-', '/', '*', '%', '>>', '<<', '>>>', '|', '^', '&']中的元素
  4. =号右侧的二元表达式的第一个操作数必须跟=号左侧的变量一致
// --------------压缩前-------------
a = a + b;
c = c >>> d;
a = b + c;

// --------------压缩后-------------
a+=b,c>>>=d,a=b+c;

3.去除没用的声明/引用

3.1 去除重复的指示性字符串

// --------------压缩前-------------
function A(){
  "use strict";
  function B(){
    "use strict";
  }
}

// --------------压缩后-------------
function A(){}

3.2 去除没有使用的函数参数

// --------------压缩前-------------
function A(a, b, c){
  b++;
}

// --------------压缩后-------------
function A(a, b){
  b++;
}

3.3 去除函数表达式冗余的函数名

对于一个函数表达式,如果其函数体没有引用自身名字递归调用,那么这个函数名可以去除,使之变为匿名函数。

// --------------压缩前-------------
(function A(){
  A();
})();
(function B(){
  c++;
})();

// --------------压缩后-------------
(function A(){
  A();
})();
(function(){
  c++;
})();

4. while压缩

去除根本不会执行的while循环:while(false){}

while(true)变成for(;;),可以缩短4个字符

// --------------压缩前-------------
while(false){
  A();
  B();
}
while(true){
  C();
  D();
}

// --------------压缩后-------------
//while(false)被压缩忽略掉
for(;;){
  C();
  D();
}

5.条件表达式

条件表达式使用了三元运算符?:,例如:cond ? yes() : no()

5.1 如果cond前边有非运算,那么考虑把非去掉,然后调转yes()跟no()的位置

// --------------压缩前-------------
!cond ? yes() : no();

// --------------压缩后-------------
(cond?no:yes)();

5.2 如果cond是一个常数值,那么可以直接缩短为yes()或者no();

// --------------压缩前-------------
true ? yes() : no();
false ? yes() : no();

// --------------压缩后-------------
yes(),no();

6.语句块压缩

6.1 连续的表达式语句可以合并成一个逗号表达式

// --------------压缩前-------------
function A(){
  B();
  C();
  d = 1;
}

// --------------压缩后-------------
function A(){B(),C(),d=1}

6.2 多个var声明可以压缩成一个var声明

6.3 return之后的非变量声明以及非函数声明的语句可以去除

// --------------压缩前-------------
function A(){
  return false;
  var a = 1;
  function B(){}
  expr += 1;
  a = 3;
}

// --------------压缩后-------------
function A(){return!1}

6.4 合并块末尾的return语句及其前边的多条表达式语句。

其实这条规则看起来并不会使最后生成的代码缩小。

// --------------压缩前-------------
function A(){
  B();
  C();
  return D();
}

// --------------压缩后-------------
function A(){
  return B(), C(), D();
}

7.IF分支优化

7.1 去除没用的if/else分支

如果if的条件是可预计算得到的常数结果,那么就可以忽略掉没用的if/else分支

// --------------压缩前-------------
if (true){
  A();
}else{
  B();
}
if (false){
  C();
}else{
  D();
}

// --------------压缩后-------------
A(),D();

7.2 去除空的if/else分支

如果是if分支是空的话,把条件取非,else分支反转成if分支即可

// --------------压缩前-------------
if (A){
  B();
}else{
}
if (C){
}else{
  D();
}

// --------------压缩后-------------
if (A){
  B();
}
if (!C){
  D();
}

7.3 变成表达式

// --------------压缩前-------------
if (!c){
  A();
}else{
  B();
}

// --------------压缩后-------------
(c?B:A)();

7.4 如果if块里边只有一个if语句,并且else块为空,那么可以合并这两个if

// --------------压缩前-------------
if (A){
  if (B){
    C();
  }
}else{
}

// --------------压缩后-------------
if (A && B){
  C();
}

7.5 如果if最后一个语句是跳出控制语句,那么可以把else块的内容提到else外边,然后去掉else

// --------------压缩前-------------
if (A){
  B();
  return;
}else{
  C();
}

// --------------压缩后-------------
if (A){
  B();
  return;
}
C();

7.6 如果if/else里边都只有一句return语句,则可以合并这两句return

// --------------压缩前-------------
function name() {
  if (A){
    return B();
  }else{
    return C();
  }
}

// --------------压缩后-------------
function name(){return(A?B:C)()}

7.7 如果if跟else里边都只有一句表达式语句,则可以化成条件表达式,然后走规则5.1跟5.2进一步压缩

7.8 如果if/else其中一个块为空,另一个块只有一条语句,则可以化成||或者&&的表达式。

// --------------压缩前-------------
if (A){
  B();
}else{
}
if (C){
}else{
  D();
}

// --------------压缩后-------------
A&&B(),C||D();