Closure Compiler 預期其 JavaScript 輸入內容必須符合幾項限制。您要求編譯器執行的最佳化層級越高,編譯器對輸入 JavaScript 設有的限制就越多。
本文說明各個最佳化等級的主要限制。另請參閱這個 維基頁面,瞭解編譯器執行的其他假設。
所有最佳化等級的相關限制
編譯器會針對其處理的所有 JavaScript 針對以下所有最佳化等級設定下列兩項限制:
編譯器只能辨識 ECMAScript。
幾乎所有支援 JavaScript 的版本都是 ECMAScript 5。不過,編譯器也支援 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()
的字串引數,所以不會重新命名這個引數中的任何符號。函式或參數名稱的字串表示:
編譯器會重新命名函式和函式參數,但不會變更程式碼中任何依名稱參照函式或參數的字串。因此,請避免將函式或參數名稱用做程式碼中的字串。例如,原型程式庫函式
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 }
編譯器無法理解
for
迴圈中呼叫initX()
和initY()
,因此會移除這兩種方法。請注意,如果您將函式做為參數傳遞,編譯器「可以」尋找對該參數的呼叫。舉例來說,編譯器在進階模式中編譯下列程式碼時,不會移除
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.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 經判定沒有副作用,因此已將其移除。