关于google closure compiler的使用

2012年7月2日

closure compiler介绍

closure compiler于yui compress其实并不是同类产品,closure compiler不仅仅可以进行优化、压缩代码,正如它的名字一样,它是一个编译器,还可以对代码进行语法、语义、语用上的检查。

编译级别

closure compiler的编译级别包括三个级别:

  1. WHITESPACE_ONLY 删除注释、空白
  2. SIMPLE_OPTIMIZATIONS 重命名函数的参数、局部变量和局部函数定义
  3. ADVANCED_OPTIMIZATIONS 重命名全局的属性、变量、函数,无用的代码删除,内联函数

每个等级的功能都包括比它低的等级的功能。

选择closure compiler高级模式

可以看到,SIMPLE_OPTIMIZATIONS级别的closure compiler和yui compress无异,没做什么真正的编译工作,所以说,使用了高级模式才能对我们目前的代码有进一步的压缩。

了解closure compiler高级模式的一些限制条件

避免使用with和eval()方法

with

对于局部变量名和对象的属性名相同的情况,编译器不能区分,所以它会将它们都重命名成同一个名字,如下就会导致编译后的结果和预想的不一样。

var href = 12;
with( location ){
    alert( href );
}

编译成

with( location ){
    alert( 12 );
}

此外,使用with语句会使你的代码更难读,也会影响性能。

eval()

编译器不会去解析字符串,所以eval中的javascript语句中的标识符就都不会被重命名。

重命名全局的变量、函数和属性名的影响

使用字符串引用对象属性

编译器会重命名属性,但是从不会重命名字符串

var x = { renamed_property: 1 };
var y = x.renamed_property; // This is OK.

// 如下在重命名之后,x中不存在'renamed_property'属性 , 
// 所以如下结果判断会是false:
if ( 'renamed_property' in x ) {}; // BAD

// 如下也会出错:
x['renamed_property']; // BAD

如果你需要使用双引号的字符串引用一个属性,就需要统一一直使用双引号:

var x = { 'unrenamed_property': 1 };
x['unrenamed_property'];  // This is OK.
if ( 'unrenamed_property' in x ) {};   // This is OK

使用window全局对象引用一个属性

编译器重命名属性和变量是相互独立的。如下例子,编译器会对认为如下的两个foo是不同的,实际上它们是相等的。

var foo = {};
window.foo; // BAD

这个代码会编译成:

var a = {};
window.b;

如果你需要使用全局对象引用一个变量,就需要统一都使用全局对象引用:

window.foo = {}
window.foo;

删除无用代码的一些影响

编译器会删除那些从来没有被调用到的函数。删除掉无用代码会出现如下一些风险:

在外部代码中调用编译文件中的函数

当编译的时候,那些在外部调用函数的代码没有一起进行编译的时候,编译器会认为这些函数从不会被引用到,所以会删除它们。为了避免有用的代码被删除,可以从如下2点来做:

  1. 将你的所有javascript文件放在一起进行编译。
  2. 告诉编译器那些外部声明的函数。

通过遍历构造函数或prototype的属性检索函数

为了判断哪个函数是无用的函数,编译器必须找到所有引用这个函数的对象。当遍历构造器的属性、或者它的prototype的属性时,你可以找到并调用该函数,但是编译器不能识别这种特殊的调用方法。
如下例子将会导致一些有用的代码被删除:

function Coordinate() {
}
Coordinate.prototype.initX = function() {
    this.x = 0;
}
Coordinate.prototype.initY = function() {
    this.y = 0;
}
var coord = new Coordinate();
for (var method in Coordinate.prototype) {
    Coordinate.prototype[method].call(coord); // BAD
}

编译后的结果为:

function a() { } 
var b = new a, c; 
for(c in a.prototype) { 
    a.prototype[c].call(b);
}

编译器不知道initX()和initY()方法在for循环中被调用到,所以这两个方法都被删除了。

需要注意的是当你把一个方法当参数传递的时候,编译器可以发现这个方法被引用了。如下例子在编译的时候就不会删除getHello()函数。

function alertF(f) {
    alert(f());
}
function getHello() {
    return 'hello';
}
// The Compiler figures out that this call to alertF also calls getHello().
alertF(getHello); // This is OK.

内联函数的一些影响

为了使命名更简短,编译器会简化对象属性的调用层级。如下例子:

var foo = {};
foo.bar = function (a) { alert(a) };
foo.bar("hello");

会编译成

alert("hello");

但是内联函数也会带来一些风险:

在构造函数和prototype方法外使用this

如下例子,属性内联会改变函数中this的指向对象:

var foo = {};
foo.bar = function (a) { this.bad = a; }; // BAD
foo.bar("hello");

会编译成

var a = function (a) { this.bad = a; };
a("hello");

在编译前foo.bar中的this是指向foo对象。但是编译后,this指向的是全局对象。为了防止这种情况发生,编译时会报错:

"WARNING - dangerous use of this in static method foo.bar"

为了防止内联函数破坏this的指向对象,只能在构造函数和prototype方法中使用this对象。这样当你使用一个new关键字调用一个构造函数的、或者在一个prototype的函数里面使用this的时候,能确保它的引用是正确的。

实际压缩结果分析

自己拿了个js代码文件来进行测试,closure compiler压缩完的代码大小与yui compress压缩完的代码大小相比,优势还是很明显的,有20%的提升空间。但是经过gzip压缩后的最终代码大小只有5.8%的提升空间。

分析原因可能是和gzip本身的算法有关,对于一些重复率较高的的代码,gzip压缩的效果会明显。经过closure compiler压缩完的代码由于很多变量都已混淆,所以再用gzip压缩,效果没有用yui compress压缩完的代码好。

参考

https://developers.google.com/closure/compiler/docs/api-tutorial3
https://developers.google.com/closure/compiler/docs/limitations