最近在看JQuery作者John Resig 写的Pro JavaScript Techniques,虽说书是06年的,但是其中很多内容拿到现在的JS开发中来也依然适用。
其中说到源代码压缩方面,jsMin只是简单的去除注释和空白字符,JS大牛Dean Edwards写的Packer,压缩率很高,但是压缩完以后的代码不好阅读,看到这里,我很想知道Packer压缩完以后的代码到底成什么样了,于是稍微使用了一下Packer。
以下是压缩前的一段源代码:
//注释:很简单的一段代码,利用原型链实现了简单的继承
var A = function() {};
A.prototype.key = 1;
var B = function() {};
B.prototype = new A();
var b = new B();
console.log(b.key); // 1
Packer官方网站(http://dean.edwards.name/packer/)中提供了两种压缩方式,一 种是Shrink variables(常规压缩~~),另一种是Base62 encode(比较变态的压缩~~)。利用Shrink variables进行压缩,得到的结果是:
var A=function(){};A.prototype.key=1;var B=function(){};B.prototype=new A();var b=new B();console.log(b.key);
看上去是简单的清理了一下注释、空格等内容。
利用Base62 encode进行压缩,得到的结果看起来有点费劲:
eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!”.replace(/^/,String)){while(c–)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return’\\w+’};c=1};while(c–)if(k[c])p=p.replace(new RegExp(‘\\b’+e(c)+’\\b’,'g’),k[c]);return p}(’0 2=4(){};2.5.6=1;0 3=4(){};3.5=7 2();0 8=7 3();a.9(8.6);’,11,11,’var||A|B|function|prototype|key|new|b|log|console’.split(‘|’),0,{}))
确实不太好阅读!为了搞清楚压缩以后的这一段代码到底是干什么的,我把代码格式化了一下,再加上了部分注释,并没有对功能性代码进行任何调整,得到的代码是:
eval(
function(p,a,c,k,e,r){
// 替换了e,此方法用于获取高位进制以后的字符,例如:Number(10).toString(11) = ‘a’;
e=function(c){return c.toString(a)};
// 如果浏览器正则表达式不靠谱,则不能依赖正则表达式,只能老老实实一个一个替换了
if(!”.replace(/^/,String)){
while(c–) // 遍历常量字符串数组元素
//把下标与字符串的对应存在r里,k已经使用完毕
r[e(c)]=k[c] || e(c);
// k被替换,用来返回e对应的字符串,k[0]为function,用于replace,作为第二个参数
k=[function(e){return r[e]}];
// e是正则表达式,用于字符串替换
e=function(){return’\\w+’};
// 因为是replace正则表达式,所以c=1,replace一次就可以了
c=1
}; // end of if
while(c–)
if(k[c])
p=p.replace(new RegExp(‘\\b’+e(c)+’\\b’,'g’),k[c]); // recover the real statement
// 还原了原来的代码,等待eval
return p
} // end of function
//function部分结束,arguments部分开始
(
// p : statement,这个语句与压缩之前的代码结构上很像
// var->0, A->2, function ->4, prototype->5, key->6, B->3, a->console, log->9
// 这里面的每一个字母都要作为索引,从第四个参数的内容中查找具体的字符串
’0 2=4(){};2.5.6=1;0 3=4(){};3.5=7 2();0 8=7 3();a.9(8.6);’
// a : k数组的长度,用于进行进制转换和寻址
,11
// c : 数组长度,跟上一个参数重复,可能是为了满足function(p,a,c,k,e,r)的参数个数
,11
// k : 字面量字符串数组
,’var||A|B|function|prototype|key|new|b|log|console’.split(‘|’)
// e : 没用,传进去就被替换了
,0
// r : 保存替换后的值与原字符串之间的映射关系
,{}
) // end of arguments
) // end of eval
返回的结果可以分为function部分和arguments部分,这样看就清楚多了。
实际上,Packer做的工作就是混淆代码,替换单词,在代码真正需要执行的时候,再还原到原来的代码,最后执行eval。
重复了几次压缩,可以发现代码压缩完以后,function部分的内容结构基本一致,不一样的是后面传入参数arguments的部分,比如,另外一个例子:
源代码:
var doc = document.documentElement;
var div = doc.getElementsByTagName(‘div’)[0];
if(div) {
div.addEventListener(‘onclick’, function(){}, false);
}
压缩完成以后:
eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!”.replace(/^/,String)){while(c–)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return’\\w+’};c=1};while(c– )if(k[c])p=p.replace(new RegExp(‘\\b’+e(c)+’\\b’,'g’),k[c]);return p} (
’2 3=7.4;2 1=3.6(\’1\’)[0];8(1){1.9(\’a\’,b(){},5)}’,
12,
12,
‘|div|var|doc|documentElement|false|getElementsByTagName|document|
if|addEventListener|onclick|function’.split(‘|’),
0,
{})
)
function部分的结构是一样的,不同的是传入的参数。
在传入了一个内容比较多的JS源码之后,得到的压缩结果的function部分如下:
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?”:e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))} ;if(!”.replace(/^/,String)){while(c–)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return’\\w+’};c=1};while(c–)if(k[c])p=p.replace(new RegExp(‘\\b’+e(c)+’\\b’,'g’),k[c]);return p} (arguments here…))
可以看出,除了高位进制的算法不同了之外,其他内容都是一样的。高位进制的算法可以参考这篇文章
最后,我们利用Packer压缩一个真实存在的JS文件,并且将YUI压缩工具加入到对比的行列中来。对于objectjs的主文件object.js和URL解析模块urlparse.js代码的压缩结果对比如下:
Packer (Shrink variables) |
Packer (Base62 encode) |
YUI Compressor Online |
|
object.js |
17766/37726=0.471 |
12754/37726=0.338 |
21503/37726 = 0.57 |
urlparse.js |
827/1833=0.4511 |
1154/1833=0.6296 |
831/1833=0.4534 |
可以看出,Packer的Base62 encode模式在文件较大时能够给出更高的压缩率,而且可以很好的混淆代码;Shrink variables模式在小文件的压缩方面能够提供比较好的压缩率,对大文件的压缩率不高,而且在代码的保密性方面可能要差一些。
不过,Packer的eval带来的性能损耗,应该是Packer无法抹去的痛~~~
http://fed.renren.com/archives/350