クロージャ コンパイラが課す制限を理解する

Closure コンパイラは、JavaScript の入力がいくつかの制限に適合すると想定します。コンパイラに実行を求める最適化のレベルが高いほど、入力 JavaScript に対するコンパイラの制限が多くなります。

このドキュメントでは、最適化のレベルごとに、主な制限事項について説明します。 コンパイラに関する追加の前提条件については、こちらの Wiki ページもご覧ください。

すべての最適化レベルの制限

コンパイラは、すべての最適化レベルについて、処理するすべての JavaScript に次の 2 つの制限を設定します。

  • コンパイラは 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() の文字列引数を解析しないため、この引数内のシンボルの名前を変更しません。

  • 関数またはパラメータ名の文字列表現:

    コンパイラは、関数と関数パラメータの名前を変更しますが、関数またはパラメータを名前で参照するコード内の文字列は変更しません。そのため、コード内で関数名やパラメータ名を文字列として表すことは避けてください。たとえば、プロトタイプ ライブラリ関数 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
    
  • 変数をグローバル オブジェクトのプロパティとして参照します。

    コンパイラは、プロパティと変数の名前を個別に変更します。たとえば、次の 2 つの参照は同等ですが、Compiler では 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");

このプロパティをフラット化すると、後で名前を変更するパスをより効率的に変更できるようになります。たとえば、Compiler は foo$bar を 1 文字に置き換えることができます。

ただし、プロパティを平坦化すると、以下の行為も危険になります。

  • コンストラクタとプロトタイプ メソッドの外部で 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 内の thisfoo を参照します。変換後、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();
    
    の場合、コンパイラは(ES6 から ES5 にトランスパイルした後)両方の create メソッドを折りたたみます。そのため、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 のゲッターとセッターは、トランスパイルによって 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 は、副作用がないと判断されたため、削除されました。