Closure Compilerを使う!

Compilerが求めるコーディングルール

最終更新:

aias-closurecompiler

- view
管理者のみ編集可

トップページ >

Compilerが求めるコーディングルール

Closure Compilerは自身への入力となるJavaScriptがいくつかのコーディングルールに適合していることを想定しています。そしてユーザが高いレベルの最適化を求めるほど、Compilerは入力されるJavaScriptに対しより多くのルールを課すことになります。このページではそれぞれのコンパイルレベルにおけるルールについて説明します。

目次:

全てのコンパイルレベルに適用されるルール

全てのコンパイルレベルにおいて、Closure Compilerは処理を行うJavaScriptに対し以下の2つのルールを課します。

  • Compilerは Ecmascript 262 revision 3 だけを正しく認識する

    Ecmascript revision 3 はJavascript 1.5とJScript 5.5の基礎になっており、"JavaScript"という言葉が使われる場合は通常このバージョンのJavaScriptを意味します。CompilerはJScript独自の言語仕様やJavaScript1.5以降のバージョンのJavaScriptをサポートしません。

    ブラウザによる拡張機能は、それがEcmascriptの言語仕様に適合するものであれば、Compilerと共に正常に動作します。例えばActiveXオブジェクトは従来のJavaScriptの構文によって作成されるので、Compilerはそのコードを問題なく扱うことができます。しかし一方、従来のJavaScriptの構文から離れたブラウザ拡張はClosure Compilerのエラーを引き起こします。例えばFirefoxのJavaScriptエンジンは const キーワードをサポートしますが、このキーワードはEcmascriptの言語仕様には含まれていないため、Compilerもそれをサポートしません。この制約により、コードが全ての主要なブラウザで動作するかどうかをチェックするするためにCompilerを利用することもできます。

  • Compilerはコメントを保存しない

    全てのコンパイルレベルにおいてコメントは削除されるため、特殊なフォーマットのコメントに依存しているコードはCompilerのもとでは正常に動作しません。例えば(Compilerがコメントを保存しないために)JScriptの「条件付きコメント」は使用できません。ただしこの制約は条件付きコメントを eval() 式で囲むことで回避できます。Compilerは下のコードをエラーを発生させることなく処理します:

    x = eval("/*@cc_on 2+@*/ 0");

    • 注意: @preserve アノテーションを使用して、Compilerの出力結果の先頭にオープンソースライセンスやその他の重要なテキストを含めることができます。詳しくはこちらを参照してください。

SIMPLE_OPTIMIZATIONSでのルール

SIMPLE_OPTIMIZATIONS レベルでは、コードサイズを減らすために関数パラメータ、ローカル変数、ローカルで定義された関数の名前を短く変更します。しかしJavaScriptのいくつかの言語構造は、この変更処理を失敗させることがあります。

SIMPLE_OPTIMIZATIONS を適用する場合、以下の言語構造の使用やコードの書き方は避けてください:

  • with

    with を使用するとCompilerはローカル変数とオブジェクトプロパティを区別できなくなり、プロパティと同名の変数が先に存在した場合はそれに合わせてプロパティ名を変更してしまいます。そもそも with 命令は人間にとってもコードを読みにくいものにします。 with 命令は名前解決のための通常のルールを変更し、名前が参照するものの識別をそのコードを書いたプログラマにすら困難にしてしまうことがあるため、利用すべきではありません。

  • eval()

    Compilerは eval() の引数文字列をパースしないので、この引数の中に含まれるいかなる名前も変更されることはありません。このことはリネームされたシンボルとの間で名前の不整合が発生する危険性があることを示しています。

  • 関数やパラメータ名の文字列表現

    Compilerは関数と関数パラメータの名前を変更するものの、コードの中でそれらを表す文字列までは変更しません。従って、コード内で関数名やパラメータ名を文字列として表現することは避けるべきです。例えばPrototype.jsライブラリの argumentNames() 関数は関数パラメータの名前を検索するために Function.toString() を使っています。 argumentNames() はコード内で引数名を利用したいと思わせるかもしれませんが、最適化処理の過程でこの種の参照は破壊されてしまいます。

ADVANCED_OPTIMIZATIONSでのルール

ADVANCED_OPTIMIZATIONS レベルのコンパイルでは、 SIMPLE_OPTIMIZATIONS と同じ変換処理に加え、プロパティ、変数、関数に対するグローバルスコープでのリネーム、未使用コードの除去、プロパティの平坦化が実行されます。これらの新しい処理は入力されるJavaScriptに対し更に多くの制約を課すことになります。また、 SIMPLE_OPTIMIZATIONS でのルールは ADVANCED_OPTIMIZATIONS にも適用されます。

以下では、 ADVANCED_OPTIMIZATIONS でコンパイルされるコードについて注意すべき点を列挙します。

一貫した方法でプロパティにアクセスする

コンパイルレベルに関係なく、Closure Compilerは文字列リテラルを決して変更しません。これは ADVANCED_OPTIMIZATIONS レベルにおいて、プロパティ名を文字列で指定してアクセスしているかどうかによってプロパティの扱い方が違ってくることを意味します。もしプロパティの参照に文字列とドットシンタックスによるものが混在していると、Closure Compilerはそれらの一部だけをリネームします。その結果、おそらくそのコードは正しく動作しなくなるでしょう。

例として以下のコードを取り上げます:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}
var flowerNote = {};
flowerNote.myTitle = 'Flowers';

alert(flowerNote.myTitle);
displayNoteTitle(flowerNote);

このソースコードの最後の2つの文は、実際には全く同じことを行っています。 ADVANCED_OPTIMIZATIONS レベルでコードを圧縮するとこうなります:

var a={};a.a='Flowers';alert(a.a);alert(a.myTitle);

圧縮されたコードの最後の文はエラーを引き起こします。 myTitle プロパティへの直接の参照は a にリネームされ、一方 displayNoteTitle() 関数内のクォートされた myTitle による参照はリネームされませんでした。その結果、最後の文では既に存在しない myTitle プロパティが参照されています。

この問題の解決策はとてもシンプルです。可能な限りクォートされた文字列ではなくドットシンタックスによるプロパティ名を使用し、文字列を使うのは Closure Compilerにプロパティ名を変更されたくない場合だけにします。例えばプロパティをエクスポートする際には、文字列が使用される必要があります。しかしそのプロパティがコンパイルされたコードの中だけで使われるなら、ドットシンタックスを使用してください。

もしどうしてもプロパティをクォートされた文字列によって参照する必要があるのであれば、常にその方法を使ってください:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}
var flowerNote = {};
flowerNote['myTitle'] = 'Flowers';

alert(flowerNote['myTitle']);
displayNoteTitle(flowerNote);

変数とグローバルオブジェクトのプロパティの不一致

Compilerはプロパティと変数をそれぞれ独立してリネームするので、それが原因でプロパティ名の不一致が発生する可能性があります。例えばCompilerは下の foo への2つの参照を、それらが実際には同一であっても、別のものとして扱います:

var foo = {};
window.foo; // BAD

このコードは次のようにコンパイルされます:

var a = {};
window.b;

もし変数をグローバルオブジェクトのプロパティとして参照したいのであれば、常にその方式で参照してください:

window.foo = {};
window.foo;

thisはコンストラクタとプロトタイプメソッドだけで使う

名前短縮の前段階として、 ADVANCED_OPTIMIZATIONS レベルのCompilerはオブジェクトプロパティに関するコードを1つにまとめる処理を行います。

var foo = {};
foo.bar = function (a) { alert(a) };
foo.bar("hello");

例えば上のコードは、次のように変換されます:

var foo$bar = function (a) { alert(a) };
foo$bar("hello");

このようなプロパティの平坦化はその後のリネーム処理をより効果的にします。例えば foo$bar はCompilerによって1文字の名前に置き換えられます。

しかしプロパティの平坦化は、関数内の 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 を参照しています。このようなケースでは、Compilerは下に示す警告を出力します:

WARNING - dangerous use of this in static method foo.bar

プロパティの平坦化が this の参照を破壊するのを防ぐには、 this をコンストラクタまたはプロトタイプメソッドの中だけで使うようにしてください。コンストラクタを new キーワードを使って呼び出すとき、または this がプロトタイプのプロパティである関数内にあるときには、 this の意味は常に明確であるといえます。

  • この問題についてはこちらの説明も参照してください。

Compilerが使用箇所を見つけられないコードは削除される

もし下の関数だけを ADVANCED_OPTIMIZATIONS でコンパイルした場合、Closure Compilerは空データを出力します:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}

Compilerに渡されたJavaScriptの中で関数が一度も呼び出されていないので、Closure Compilerはこのコードを不要とみなしてしまうのです。この振る舞いは多くのケースで望ましいものです。例えばあなたが自分のコードと大きなライブラリを一緒にコンパイルする場合、Closure Compilerはそのライブラリの中の関数が実際に使用されているかどうかを判断し、使用されていないものを取り除いてくれます。

ある関数(メソッドも)が使用されているとみなされるためには、Compilerが理解できる形式で呼び出しが行われている必要があります。JavaScriptではコンストラクタやそのプロトタイプのプロパティ名を反復処理の中で取得し、メソッドを実行できます。しかしCompilerはこの方法で呼ばれる特定の関数を識別することはできません。例えば次のコードは、意図しないコード削除の原因となります:

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
}

Compilerは for ループの中で initX() initY() が呼ばれていることが分からず、両方のメソッドを削除します。

関数への参照がパラメータとして渡される場合、Compilerはそのパラメータへの呼び出しを見つけることができます。例えば次のコードが ADVANCED_OPTIMIZATIONS レベルでコンパイルされるとき、Compilerは 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.

残しておきたいコードをClosure Compilerが削除している場合、これを防ぐには2つの方法があります:

解決策1: 関数呼び出しを行っているコードを、Closure Compilerが処理するコードの中へ移動させる

もしあなたが自分のコードの一部だけをClosure Compilerでコンパイルした場合、望ましくないコードの削除に遭遇する可能性があります。例えば、関数定義のみを含むライブラリのファイルと、ライブラリをインクルードしその中の関数を呼び出すコードを含むHTMLファイルがあるとします。このとき、もしライブラリファイルだけを ADVANCED_OPTIMIZATIONS でコンパイルすると、Closure Compilerはライブラリ内の関数を全て削除してしまいます。

この問題の最もシンプルな解決策は、関数とそれらを呼び出しているプログラムを一緒にコンパイルすることです。下に示すプログラムをコンパイルしたとき、Closure Compilerは displayNoteTitle() 関数を削除しません:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}

displayNoteTitle({'myTitle': 'Flowers'});

このケースでClosure Compilerが displayNoteTitle() 関数を削除しないのは、それがコード内で呼び出されているためです。

言い換えると、Closure Compilerに渡すコード内にそのプログラムの「エントリポイント」を含まれていれば、望ましくないコードの削除を防ぐことができるわけです。プログラムの「エントリポイント」とは、プログラムの実行が開始される場所を指します。例えば前のセクションの"flower note"プログラムであれば最後の3行はJavaScriptがブラウザに読み込まれた直後に実行されるコードですので、ここがこのプログラムのエントリポイントということになります。コードを残すべきかどうかを決定するため、Closure Compilerはエントリポイントを出発点としてプログラムの処理フローのトレースを開始します。

解決策2: 残しておきたいシンボルをエクスポートする

ではもしあなたが再利用可能なライブラリを作成するため関数定義だけをコンパイルしたいと考えた場合、Closure Compilerによる関数の削除をどうすれば防げるでしょうか?

この場合の最善の解決策は、下の例に示すように関数をエクスポートすることです。エクスポートとは、シンボルの参照をグローバルオブジェクトのプロパティにプロパティ名を文字列指定して設定する処理のことです:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}
// Store the function in a global property referenced by a string:
window['displayNoteTitle'] = displayNoteTitle;

ADVANCED_OPTIMIZATIONS レベルのコンパイルによって圧縮されたコードは、次のようになります:

function a(b){alert(b.myTitle)}window.displayNoteTitle=a;

圧縮されたコードが alert 文を実行している部分を関数として残している点に注意してください。この関数はかつての displayNoteTitle() a() にリネームされたものです。しかし a() を参照するグローバルオブジェクトのプロパティの名称に古い関数名が使われているため、依然としてこの関数を古い名前で呼び出すことも可能になっています。例えばこのコードの外部にあるプログラムから以下のような呼び出しがあったとしても、やはり正しくalertを表示するはずです:

displayNoteTitle({'myTitle': 'Flowers'});

上のコードが動作するのは、ちょうど window.alert プロパティの値が alert() 関数であるのと同様、 window.displayNoteTitle プロパティの値が関数であるためです。

さらに、Closure Compilerが関数定義を削除しなかった( a() にリネームして残した)のは、 displayNoteTitle() 関数が実行中の処理の中で使用されていると見なしたからです。以下の代入文が、その使用箇所にあたります:

window['displayNoteTitle'] = displayNoteTitle;

コンストラクタやプロトタイプメンバも、上と同様の手法でエクスポートできます。以下に例を示します:

MyClass = function(name) {
  this.myName = name;
};
MyClass.prototype.myMethod = function() {
  alert(this.myName);
};
window['MyClass'] = MyClass; // <-- Constructor
MyClass.prototype['myMethod'] = MyClass.prototype.myMethod;

もし逐一エクスポートのための文を書くことが面倒に感じられるなら、エクスポート用の関数を使用するのも良いでしょう。そのような関数の例として、Closure Library内の goog.exportSymbol() goog.exportProperty() を参照してみてください。

アプリケーションは全体を一度にコンパイルする

もしあなたが自分のアプリケーションを2つのコードモジュール(JSファイル)に分割したとすると、おそらくそれらのモジュールを別々にコンパイルしたいと考えるでしょう。しかしそれらのモジュールが連携して動作している場合、あなたはコンパイル時に後述するような様々な問題に直面するものと思われます。そして仮にコンパイルに成功したとしても、2つの出力結果は互いに関連性を失っていることでしょう。

例として、データ取得とデータ表示の2つの部分に分割されたアプリケーションを想定します。データを取得するコードは以下のとおりです:

function getData() {
   // In an actual project, this data would be retrieved from the server.
  return {title: 'Flower Care', text: 'Flowers need water.'};
}

データを表示するコードは以下のとおりです:

var displayElement = document.getElementById('display');
function displayData(parent, data) {
  var textElement = document.createTextNode(data.text);
  parent.appendChild(textElement);
}
displayData(displayElement, getData());

これら2つのコードを別々にコンパイルした場合、いくつかの問題が発生します。第1に、Closure Compilerは getData() を削除します。(その理由についてはCompilerが使用箇所を見つけられないコードは削除されるを参照してください)第2に、データ表示のコードを処理する際、Closure Compilerは以下のエラーを出力します:

input:6: ERROR - variable getData is undefined
displayData(displayElement, getData());

データ表示のコードのコンパイル時に getData() 関数にアクセスできないため、Closure Compilerは getData を未定義として扱います。

適切なコンパイルを確実に行うには、アプリケーションに含まれる全てのコードを1回の処理で一緒にコンパイルさせるべきです。Closure Compilerには複数のJavaScriptファイルやJavaScriptコード文字列を入力として指定できるため、ライブラリのコードとそれ以外のコードを単一のコンパイル処理リクエストに同時に渡すことが可能です。

コンパイルされたコードと外部コードの間の参照関係を維持するには

ADVANCED_OPTIMIZATIONS レベルでのシンボルのリネームによって、Closure Compilerが処理したコードとそうでないコードの間では互いに対話ができなくなります。コンパイル処理がソースコード内に定義された関数をリネームした後に外部のコードがその関数を呼び出したとしても、それらは変更前の関数名を参照しているため全て失敗するでしょう。同様にコンパイルされたコードからの参照に用いられていた外部コードのシンボル名もまた、Closure Compilerによって変更されていると思われます。

注意してほしいのは、コンパイルされたコードから外部コードへの参照を正しく維持することと、外部コードからコンパイルされたコードへの参照を正しく維持することは、実際には別の問題だということです。これらにはそれぞれに異なった解決策があり、Closure Compilerを最大限に活用するには状況に応じ適切な解決策をとることが重要になります。

外部コードからコンパイル済みコードへの呼び出しについての解決策:エクスポート

もしあなたがライブラリとして再利用しようと考えているJavaScriptコードを持っているとして、外部のコンパイルされていないコードからライブラリ内の関数を呼び出せなくなるのなら、Closure Compilerによるコードの短縮を使いたいとは思わないでしょう。

この状況に対する解決策は、残しておきたいシンボルをエクスポートするで示した望まないコードを削除への対策と同じです。要素のエクスポートは要素の削除を防ぐだけでなく、それらを外部コードから利用可能な状態にすることにもなります。

コンパイル済みコードから外部コードへの呼び出しについての解決策:extern

あなたが"OpenSocial API"や"Google Maps API"のようなサードパーティのJavaScriptライブラリを利用している場合、コード内で使用されている外部ライブラリ定義のシンボル名がClosure Compilerによってリネームされないようにしなければなりません。例えばOpenSocialライブラリの opensocial.newDataRequest() 関数を呼び出しをClosure Compilerが a.b() に変換してしまうのは、まったく望ましいことではありません。あなたのコードは外部ライブラリで使われているとおりの名前を使う必要があるからです。

Closure Compilerは、外部コードが定義する名前を宣言しそれをリネームさせなくする機能を提供します。この機能は「extern」と呼ばれます。Closure Compilerはextern宣言されたシンボルがコンパイルされたJavaScriptが実行される環境内に存在するものとみなして処理を行います。externについてはこちらで詳しく説明します。


目安箱バナー