陈德馨

JavaScript代码压缩细节

926人参与 |分类: 站长百科|时间:2016年07月07日 22:23

对于Javascript来说,提高网络下载的性能最直接的方法就是把JS文件体积减小。为了方便理解和对比,本文会给出压缩前后代码作为参考,但压缩后的代码仍会换行,变量名字不做混淆处理,同时一个压缩规则的例子会尽量不混其它压缩策略进去。

1、表达式的压缩

规则1.1 表达式预计算

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

压缩前

var expr1 = 1 + 1; 
var expr2 = 1 / 3;

压缩后

var expr1 = 2;
var expr2 = 1 / 3;//由于计算出来的值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;
var expr2 = !1;
1 == A;
0 == A;

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

压缩前

true && A();
false && A();
true || A();
false || A();

压缩后

A();//返回&&第二个操作数的值
!1;//返回&&第一个操作数的值
!0;//返回||第一个操作数的值
A();//返回&&第二个操作数的值

2、运算符缩短

规则2.1 对于二元操作符===以及!==,其两个操作数都是string类型或者都是布尔类型的,可以缩短成==以及!=

留意:这里typeof A得到的结果是string类型,b instanceof B得到的结果是布尔类型。

压缩前

"object" === typeof A;
true !== b instanceof B;

压缩后

"object" == typeof A;
true != b instanceof B;

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

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

必须是=号赋值语句;

=号左侧只能是变量,不能为表达式等;

=号右侧必须为二元操作表达式,并且符号是为数组[‘+’, ‘-‘, ‘/’, ‘*’, ‘%’, ‘>>’, ‘<<‘, ‘>>>’, ‘|’, ‘^’, ‘&’]中的元素;

=号右侧的二元表达式的第一个操作数必须跟=号左侧的变量一致。

压缩前

a = a + b;
c = c >>> d;
a = b + c;

压缩后

a += b;
c >>>= d;
a = b + c;

规则2.3 操作符非!的压缩

对a>=b取非可以得到a<b,对a&&b取非可以得到!a||!b。如果转换后的结果能得到更短的代码,那就将这个取非的表达式换成转换后的表达式。

压缩前

!(a>=b)
!!!a

压缩后

a<b
!a

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

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

对于嵌套的作用域使用了同样的指示性字符串,其实子作用域的是可以去除的。

压缩前

function A(){
  "use strict";
  function B(){
    "use strict";
  }
}

压缩后

function A(){
  "use strict";
  function B(){
  }
}

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

参数c在函数A里边没有使用,所以直接去除c参数的声明。也许你会好奇为什么参数a也没有使用却不去掉,如果去掉参数a,就会改变了b所在的参数位置。

例如:调用A(1,2)时候,本来b应该是2的,如果去除参数a,这个时候b就会变成1,这样会引起错误。因此UglifyJS在去除函数参数的时候都是从后往前扫描变量在函数里边的引用。

压缩前

function A(a, b, c){
   b ++;
}

压缩后

function A(a, b){
   b++;
}

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

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

压缩前

(function A(){
  A();
})();
(function B(){
  c++;
})();

压缩后

(function A(){
  A();
})();
(function(){
  c++;
})();

规则3.4 去除没用的块

如果块里边没有语句或者只有一条语句,那么这个块可以去掉{ }。

压缩前

while (f){ { A(); } }
while (f){ { } }
if (A){ B(); }

压缩后

while (f) A();
while (f) ;
if (A) B();

规则3.5 去除没有使用的break;

switch最后一个case/default分支块的最后一个语句如果是“break;”的话,可以忽略,如果break后边带有标签则不能去除。

压缩前

switch(A){
  case 1: break;
  case 2: break;
}
label: switch(A){
  case 1: break;
  case 2: break label;
}

压缩后

switch(A){
  case 1: break;
  case 2://这里最后的break;可以忽略
}
label: switch(A){
  case 1: break;
  case 2: break label;//带标签的break不可以忽略
}

规则3.6 去除没有引用的label

压缩前

label1:var a = 1;
label2:while (true){
  break label2;
}

压缩后

var a = 1;
label2:while (true){
  break label2;//label2被引用 不可以去掉
}

规则3.7 去除没作用的toString函数调用

这个规则在某些条件可能会有不安全的问题产生,因此UglifyJS只有在你明确调用时带上–unsafe的参数才会做这个压缩。

压缩前

(typeof A).toString();
("A" + "B").toString(); 
var expr = "str".toString();

压缩后

(typeof A); 
"AB";
var expr = "str";

4、while压缩

规则4.1 去除根本不会执行的while循环、将循环巧妙变化节省字符

压缩前

while(false){ 
  A(); 
  B(); 
} 
while(true){ 
  C(); 
  D(); 
}

压缩后

//while(false)被压缩忽略掉 
for(;;){ //while(true)转换成for(;;)节省4个字符 
  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、语句块压缩

函数体、with都会生成一个语句块,下边规则是针对语句块的压缩优化。

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

>>>留意:这里要表达式语句才可以。

压缩前

function A(){ 
  B(); 
  C(); 
  d = 1; 
}

压缩后

function A(){ 
  B(), C(), d = 1; 
}

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

压缩前

function A(){ 
  var a; 
  var b;
  var c; 
}

压缩后

function A(){ 
  var a, b, c; 
}

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

在块里边return之后的语句是不会被执行到的,所以可以被去除。但是Javascript中在块里边无论什么地方声明都可以(声明会被提升),因此return之后的声明是不能去掉的。 当然这里不仅仅是return之后的语句可以去除,还有throw、break、continue之后的语句也适用于这条规则。

压缩前

function A(){ 
  return false; 
  var a = 1; 
  function B(){ } 
  expr += 1; a = 3; 
}

压缩后

function A(){ 
  function B(){ } 
  return false; 
  var a = 1; 
}

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

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

合并前

function A(){ 
  B(); 
  C(); 
  return D(); 
}

合并后

function A(){ 
  return B(), C(), D(); 
}

7、IF分支优化

接下来开始复杂丰富多彩的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/else分支,看看生成代码是否更短

尝试对if条件取非,如果能得到更短的代码,那就反转if/else分支。

压缩前

if (!c){ 
  A(); 
}else{ 
  B(); 
}

压缩后

if (c){ 
  B(); 
}else{ 
  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

压缩前

if (A){ 
  return B(); 
}else{ 
  return C(); 
}

压缩后

return A ? B() : C();

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

即把适合的if语句转化为三目条件表达式,具体请参考规则5.1与5.2。

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

压缩前

if (A){ 
  B(); 
}else{ 
} 
if (C){ 
}else{ 
  D(); 
}

压缩后

A && B();
C || D();

源码注释

更详细的源码分析可点击下载查看本文作者对UglifyJS源码的注释。源码注释下载

来源:陈德馨博客(微信/QQ号:35435164),转载请保留出处和链接!

地址:

必填

选填

选填

◎已有 2 人评论,请发表您的观点。

  
    
1楼在线阅读   2016-07-09 07:35:10
赞,感谢博主分享
回复
2楼卢松松博客   2016-07-10 00:05:38
看了你的文章,收获挺大的,迷茫这么久,终于知道怎么做了
回复