Closure Compiler は、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 の制限事項
最適化レベル「Simple」では、コードサイズを削減するために、関数パラメータ、ローカル変数、ローカルで定義された関数の名前が変更されます。ただし、一部の 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
グローバル オブジェクトのプロパティとしての変数:
コンパイラはプロパティと変数を個別に名前変更します。たとえば、コンパイラは、次の 2 つの
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");
このコードを Babel で変換すると次のようになります。
var foo$bar = function (a) { alert(a) }; foo$bar("hello");
このプロパティのフラット化により、後続の名前変更パスでより効率的に名前を変更できます。コンパイラは、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
内の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 のゲッターとセッターは、トランスパイルによって 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 に副作用がないと判断されたため、削除されました。