瞭解 Closure 編譯器施加的限制

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 的全域重新命名會導致下列做法產生危險:

  • 未宣告的外部參照:

    如要正確重新命名全域變數、函式和屬性,編譯器必須瞭解這些全域變數的所有參照。您必須向編譯器說明在編譯程式碼以外定義的符號。進階編譯和 Externs 說明如何宣告外部符號。

  • 在外部程式碼中使用未匯出的內部名稱:

    編譯後的程式碼必須匯出未編譯程式碼參照的任何符號。進階編譯和 Externs 一文說明如何匯出符號。

  • 使用字串名稱參照物件屬性:

    編譯器會在「進階」模式中重新命名屬性,但絕不會重新命名字串。

      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 擷取器/設定器:

    編譯器不太瞭解這些建構項目。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 判斷為沒有副作用,因此已移除。