Closure覚書

ClosureでAdvanced Optimizationsに対応する場合にいろいろ調べたこと覚書。

モジュールのローカル変数の定義

モジュールのローカル変数の定義は

/**
 * @private
 */
var local_variable = "hoge";

単純にこれで できる。ここで言うモジュールは Closure Compilerのmoduleではなく自分のjsファイル単位です。さーせん。

ただし、このままだとGlobalが汚れる。aとかbとかglobal変数ができる。

モジュールで即時関数を使って関数スコープにする事も当然できるが、モジュール毎に即時関数が作成されて僅かだが出力ファイルが大きくなる。

Closure Compilerのoutput wrapで全体を即時関数にして隠蔽する事で即時関数の定義を減らす事が出来る。

モジュール毎に即時関数で関数スコープとしなかった場合、たまたま他のモジュールで同じローカル変数を定義していると名前が衝突して、warning_levelがverboseだとエラーになる。

warning_levelがdefaultだとエラーどころか警告もされない。でも、変数は共有されるため、衝突しておかしくなる可能性がある。怖い。

とりあえず名前にprefixをつけて衝突回避が一番簡単。だけど、自分の性格上不安。@privateの付け忘れとか絶対やるよなぁ、、、。

そこで名前空間を別にしてやればいいんだよなと、private namespaceを使う方法を考えてみた。

/**
 * @private
 * @namespace
 */
var modulename = {};
modulename.local_variable = "hoge";

この場合、modulename.local_variableに対して、別モジュールの外からはアクセスできない(modulenameのアクセス制限による)。

他のモジュールで同じネームスペースが定義されると警告されるが、モジュール名が違えば衝突の可能性は低い。

private namespaceはcompile結果を眺めていると削除されて実際には作成されない。

先ほどの例で言えば modulename$local_variableという名前に一度置き換えが発生し、その後renameにより短い変数名に置き換わってる。

公開する関数の定義

荒っぽいけどクラスの定義が以下みたいだとする。

function Clazz() {
}

Clazz.prototype = {
    say: function() { console.log('hello'); }
};

window['namespace']['Clazz'] = Clazz;

利用する側のコードは以下。

var a = new namespace.Clazz();
a.say();

これはsayという関数が削除されてしまって失敗する。

文字列なら書き換えとかされないんだっけ?と思って以下にしてみると、これもダメ

function Clazz() {
}

Clazz.prototype = {
    'say': function() { console.log('hello'); }
};

window['namespace']['Clazz'] = Clazz;

続いて @exposeアノテーション

function Clazz() {
}

Clazz.prototype = {
    /** @expose */
    say: function() { console.log('hello'); }
};

window['namespace']['Clazz'] = Clazz;

実はsayの前で @exposeしてもsay関数は削除される。

次の例だとどうなるか。

function Clazz() {
}

/** @expose */
Clazz.prototype = {
    say: function() { console.log('hello'); }
};

window['namespace']['Clazz'] = Clazz;

この例の場合、prototypeに対して say関数は残るが、renameされてしまうため、外部からの呼び出しはできない

やっと動作する例は以下

function Clazz() {
}

/** @expose */
Clazz.prototype = {
    /** @expose */
    say: function() { console.log('hello'); }
};

window['namespace']['Clazz'] = Clazz;

でも、@exposeが二つもあって嫌。仕方がないのでこう書く。

function Clazz() {
}

/** @expose */
Clazz.prototyp.say = function() { console.log('hello'); }

window['namespace']['Clazz'] = Clazz;

↑は OKだけど関数が増えると記述が面倒。なので、$.extendみたいな関数を使って拡張する事を考えた。

function Clazz() {
}

$.extend(Clazz.prototype, {
    /** @expose */
    say: function() { console.log('hello'); }
});

window['namespace']['Clazz'] = Clazz;

これでまともに動いてるっぽいけど、Documentを見る限りはこの場合は@lends アノテーションが必要。@thisアノテーションが無くても@thisの型を推測するのに必要なのかな。

function Clazz() {
}

$.extend(Clazz.prototype, /** @lends {Clazz.prototype} */ {
    /** @expose */
    say: function() { console.log('hello'); }
});

window['namespace']['Clazz'] = Clazz;

これでOK。あたりまえだけど以下のような記述は許されないんですが、、、

function Clazz() {
}

Clazz.prototype = /** @lends {Clazz.prototype} */ {
    say: function() { console.log('hello'); }
};

window['namespace']['Clazz'] = Clazz;

このとき発生するエラーが Clazz.prototypeが未宣言ですなので変な感じ。まぁいいか。

インターフェースの@expose

インターフェース側で@exposeしておけば@implements側では関数毎に@exposeしなくてもOKだった。

/** @interface */
function Interface() {}
$.extend(Interface.prototype, /** @lends {Interface} */ {
    /** @expose */
    say: function() {},
    /** @expose */
    goodbye: function() {}
});

/**
 * @constructor
 * @implements {Interface}
 */
function Clazz() {
}
$.extend(Clazz.prototype, /** @lends {Clazz} */ {
    say: function() {},
    goodbye: function() {}
});

window['namespace']['Clazz'] = Clazz;

これでちゃんとsay/goodbyeともにリネーム/削除される事なく公開された。

ただ、ドキュメンテーションの意味も兼ねて、@overrideをつけてインターフェースを実装している(@implementってタグは無かった、、、)事を宣言したほうが良いかなと。

とりあえず今日はこんなところ、、、。