Closure 编译器要求其 JavaScript 输入符合一些限制。您要求编译器执行的优化级别越高,编译器对输入 JavaScript 的限制就越多。
本文档介绍了每个优化级别的主要限制。 另请参阅此维基页面,了解编译器做出的其他假设。
所有优化级别的限制
对于所有优化级别,编译器都会对处理的所有 JavaScript 施加以下两项限制:
编译器仅识别 ECMAScript。
ECMAScript 5 是几乎在所有地方都受支持的 JavaScript 版本。 不过,编译器还支持 ECMAScript 6 中的许多功能。编译器仅支持官方语言功能。
符合相应 ECMAScript 语言规范的浏览器专用功能可与编译器正常搭配使用。 例如,ActiveX 对象是使用合法的 JavaScript 语法创建的,因此创建 ActiveX 对象的代码可与编译器搭配使用。
编译器维护人员会积极支持新的语言版本及其功能。项目可以使用
--language_in
标志指定其预期的 ECMAScript 语言版本。编译器不会保留注释。
所有编译器优化级别都会移除注释,因此依赖于特殊格式注释的代码无法与编译器搭配使用。
例如,由于编译器不会保留注释,因此您无法直接使用 JScript 的“条件注释”。不过,您可以通过将条件注释封装在
eval()
表达式中来解决此限制。编译器可以处理以下代码,而不会生成错误:x = eval("/*@cc_on 2+@*/ 0");
注意:您可以使用 @preserve 注释在编译器输出的顶部添加开源许可和其他重要文本。
针对 SIMPLE_OPTIMIZATIONS 的限制
简单优化级别会重命名函数形参、局部变量和本地定义的函数,以减小代码大小。不过,某些 JavaScript 结构可能会中断此重命名过程。
使用 SIMPLE_OPTIMIZATIONS
时,请避免以下结构和做法:
with
:使用
with
时,编译器无法区分同名的局部变量和对象属性,因此会重命名该名称的所有实例。此外,
with
语句会使代码更难被人读懂。with
语句会更改名称解析的正常规则,甚至会让编写代码的程序员难以确定名称所指的对象。eval()
:编译器不会解析
eval()
的字符串实参,因此不会重命名此实参中的任何符号。函数或形参名称的字符串表示形式:
编译器会重命名函数和函数形参,但不会更改代码中按名称引用函数或形参的任何字符串。因此,您应避免在代码中将函数或形参名称表示为字符串。例如,Prototype 库函数
argumentNames()
使用Function.toString()
来检索函数参数的名称。不过,虽然argumentNames()
可能会诱使您在代码中使用实参名称,但简单模式编译会破坏这种引用。
针对 ADVANCED_OPTIMIZATIONS 的限制
ADVANCED_OPTIMIZATIONS
编译级别执行与 SIMPLE_OPTIMIZATIONS
相同的转换,还会添加属性、变量和函数的全局重命名、无效代码消除和属性扁平化。这些新传递对输入 JavaScript 施加了额外的限制。一般来说,使用 JavaScript 的动态功能会妨碍对代码进行正确的静态分析。
重命名全局变量、函数和属性的影响:
ADVANCED_OPTIMIZATIONS
的全局重命名使得以下做法变得危险:
未声明的外部引用:
为了正确重命名全局变量、函数和属性,编译器必须了解对这些全局变量的所有引用。您必须告知编译器在正在编译的代码之外定义的符号。高级编译和外部声明介绍了如何声明外部符号。
在外部代码中使用未导出的内部名称:
已编译的代码必须导出未编译的代码所引用的任何符号。高级编译和外部声明介绍了如何导出符号。
使用字符串名称引用对象属性:
编译器在高级模式下会重命名属性,但绝不会重命名字符串。
var x = { renamed_property: 1 }; var y = x.renamed_property; // This is OK. // 'renamed_property' below doesn't exist on x after renaming, so the // following evaluates to false. if ( 'renamed_property' in x ) {}; // BAD // The following also fails: x['renamed_property']; // BAD
如果您需要引用带有带引号的字符串的属性,请始终使用带引号的字符串:
var x = { 'unrenamed_property': 1 }; x['unrenamed_property']; // This is OK. if ( 'unrenamed_property' in x ) {}; // This is OK
将变量作为全局对象的属性进行引用:
编译器会独立重命名属性和变量。例如,即使以下两个对
foo
的引用是等效的,编译器也会以不同的方式处理它们:var foo = {}; window.foo; // BAD
此代码可能会编译为:
var a = {}; window.b;
如果您需要将变量作为全局对象的属性进行引用,请始终以这种方式引用:
window.foo = {} window.foo;
Implications of dead code elimination
The ADVANCED_OPTIMIZATIONS
compilation level removes code
that is never executed. This elimination of dead code makes the
following practices dangerous:
Calling functions from outside of compiled code:
When you compile functions without compiling the code that calls those functions, the Compiler assumes that the functions are never called and removes them. To avoid unwanted code removal, either:
- compile all the JavaScript for your application together, or
- export compiled functions.
Advanced Compilation and Externs describes both of these approaches in greater detail.
Retrieving functions through iteration over constructor or prototype properties:
To determine whether a function is dead code, the Compiler has to find all the calls to that function. By iterating over the properties of a constructor or its prototype you can find and call methods, but the Compiler can't identify the specific functions called in this manner.
For example, the following code causes unintended code removal:
function Coordinate() { } Coordinate.prototype.initX = function() { this.x = 0; } Coordinate.prototype.initY = function() { this.y = 0; } var coord = new Coordinate(); for (method in Coordinate.prototype) { Coordinate.prototype[method].call(coord); // BAD }
编译器不了解
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");
转换为以下代码:
var foo$bar = function (a) { alert(a) }; foo$bar("hello");
这种属性扁平化可让后续的重命名传递更高效地重命名。编译器可以将 foo$bar
替换为单个字符,例如。
但属性扁平化也会使以下做法变得危险:
在构造函数和原型方法之外使用
this
:属性扁平化可能会改变函数中关键字
this
的含义。例如:var foo = {}; foo.bar = function (a) { this.bad = a; }; // BAD foo.bar("hello");
变为:
var foo$bar = function (a) { this.bad = a; }; foo$bar("hello");
转换之前,
foo.bar
中的this
指的是foo
。转换后,this
指的是全局this
。在这种情况下,编译器会生成以下警告:"WARNING - dangerous use of this in static method foo.bar"
为防止属性扁平化破坏对
this
的引用,请仅在构造函数和原型方法中使用this
。当您使用new
关键字调用构造函数时,或者在作为prototype
属性的函数中,this
的含义是明确的。在不知道静态方法所调用的类的情况下使用静态方法:
例如,如果您有:
编译器会折叠这两个class A { static create() { return new A(); }}; class B { static create() { return new B(); }}; let cls = someCondition ? A : B; cls.create();
create
方法(在从 ES6 转译到 ES5 之后),因此cls.create()
调用会失败。您可以使用@nocollapse
注释来避免这种情况:class A { /** @nocollapse */ static create() { return new A(); } } class B { /** @nocollapse */ static create() { return new A(); } }
在不知道超类的情况下在静态方法中使用 super:
以下代码是安全的,因为编译器知道
super.sayHi()
指的是Parent.sayHi()
:class Parent { static sayHi() { alert('Parent says hi'); } } class Child extends Parent { static sayHi() { super.sayHi(); } } Child.sayHi();
不过,即使
myMixin(Parent).sayHi
等于Parent.sayHi
(未编译),属性扁平化也会破坏以下代码:class Parent { static sayHi() { alert('Parent says hi'); } } class Child extends myMixin(Parent) { static sayHi() { super.sayHi(); } } Child.sayHi();
使用
/** @nocollapse */
注释避免此中断。使用 Object.defineProperties 或 ES6 getter/setter:
编译器不太了解这些构造。ES6 getter 和 setter 通过转译转换为 Object.defineProperties(...)。目前,编译器无法静态分析此构造,并假设属性访问和设置没有副作用。这可能会带来危险的后果。例如:
class C { static get someProperty() { console.log("hello getters!"); } } var unused = C.someProperty;
编译为:
C = function() {}; Object.defineProperties(C, {a: // Note someProperty is also renamed to 'a'. {configurable:!0, enumerable:!0, get:function() { console.log("hello world"); return 1; }}});
C.someProperty 被确定为没有副作用,因此被移除。