http://d.hatena.ne.jp/m-hiyama/20051202/1133487974 :
簡単にJSAN化(JSANでロード可能にする)できると思ってやってみたのですが、なぜかプリントダイアログが表示されてしまいます。不可解!
ということでしたが、「簡単にできると思った」のは正しくて、JSTのJSAN化はものすごく簡単です。プリントダイアログが出たのは:
プリントダイアログの件は、僕がアホタレでした。Rhino向けに手を加えたJSAN.jsを読んでいて、トレース用のprint関数が動いてしまっただけ(←大バカ)。
まずは“いかに簡単にJSAN化できるか”を示し、後半で“僕が陥った落とし穴”(printの件はあまりにバカだから論外)について報告します。ただし、余裕と気力に欠けるので中途半端な調査結果で、いまだに納得できない点があります。
内容:
●TrimPath JSTをJSAN化する手順
template.jsの先頭に、TrimPath Template. Release 1.0.38.
と書いてあるので、モジュール名は TrimPath.Template とすることにして:
- JSANリポジトリ(include pathに含まれるディレクトリ)に、TrimPath/ というディレクトリを作成。
- template.js を TrimPath/Template.js という名でコピー。
- 最初の行
var TrimPath;
を削除して、代わりにif (!window.TrimPath) window.TrimPath = {};
を挿入。
これだけで、JSAN.require("TrimPath.Template");
でロードできます(JSAN.useしてもJSAN.requireと同じ)。が、JSANのモジュール管理の都合から、上の行の直後に次のコードも加えておいてください。
TrimPath.Template = {
VERSION:"1.0.38"
};
●たった1行が動かない
JSAN化は実質1行の変更で済むので、ほとんど間違えようもないようですが、その1行の書き方で動かなかったり動いたりします。以下で、NGは動かなかったことを示し、括弧内の「関数」はTrimPath.processDOMTemplate is not a function
というエラー、「オブジェクト」はTrimPath is not defined
というエラーが生じることを意味します。(テストはFirefox 1.0.7のみ。)
if (!TrimPath) var TrimPath = {};
-- NG(関数)if (!TrimPath) TrimPath = {};
-- NG(オブジェクト)if (!TrimPath) window.TrimPath = {};
-- NG(オブジェクト)if (!window.TrimPath) var TrimPath = {};
-- NG(関数)if (!window.TrimPath) TrimPath = {};
-- OKif (!window.TrimPath) window.TrimPath = {};
-- OKif (typeof(TrimPath) == 'undefined') var TrimPath = {};
-- NG(関数)if (typeof(TrimPath) == 'undefined' ) TrimPath = {};
-- OKif (typeof(TrimPath) == 'undefined') window.TrimPath = {};
-- OK
●現象論と対症療法
現象だけを観察すると、if条件のなかで !TrimPath と書くのはダメなようです。Rhinoでも、この書き方は許されません。修飾されてない裸の未定義変数へのアクセスはエラーとなります。意味的には同じでも、明示的にwindow(大域オブジェクト)のプロパティとして書いたwindow.TrimPathなら許されます。また、typeofに未定義変数を入れるのは問題ないので:
- 大域変数Fooの有無を確認するには、
!window.Foo
またはtypeof(Foo) == 'undefined'
を使う。
という処世訓(?)が導かれます。
次に、varを使うと失敗するので、理由はともかくも、次の処方に従うべきだと言えそうです。
- 大域変数Fooを生成するには、varを付けずに、
Foo = 値;
またはwindow.Foo = 値;
とする。
Rhinoのstrictモードでは、Foo = 値;
は許されないので、これを考慮するなら、window.Foo = 値;
がいいということになります。
厳密に言えば、windowが大域オブジェクトを指しているのはブラウザ環境固有のことなので、JSANが提供するJSAN.globalScopeを使うのが望ましいのですが、すべてのコードをJSAN.globalScopeに対して相対的に書くのは現実的でない(たぶんミスを犯す)ので、僕は「windowを使ってもいい」と判断しています。
●eval評価時のスコープの問題
さて、大域変数生成にvarを付けるとなぜダメなのでしょうか。これは、evalによる動的コード評価(実行)に関係するようです。僕は、eval関数が呼び出される文脈に関係なく「evalに渡されたコード(テキスト)は大域スコープで評価される」と思いこんでいたのですが、これは事実と違うようです。次はRhinoの実行セッションです。
※ print事件で懲りて、Rhinoのprintの別名としてalertを使っている。
js> function ev(s) {eval(s);}
js> var code="if (!Foo) var Foo = {}; alert('Foo :' + typeof(Foo))"
js> ev(code)
Foo :object
js> typeof(Foo)
undefined
js> eval(code)
Foo :object
js> typeof Foo
object
js>
関数内でevalが呼ばれると、その関数の局所スコープによりコードが実行されています。evalに渡されるコード内から裸の変数を参照すると、それは大域変数ではなくて、関数内局所変数(それがあれば、ですが)が見えてしまいます。
js> function tt() {
var bar = true; //局所変数
eval("alert(typeof bar)");
}
js> typeof bar
undefined
js> tt()
boolean
js>
ウーン、まー便利なんだけど、間違いやすいから僕は嫌いだな、このスコープルールは。でも、ブラウザ(Firefox)でも同じ結果が出るので、evalがこういう動作をするのは事実です。
当然ながら、JSANで動的ロードされたJavaScriptコードは関数内でevalされます。よって、var Foo = {};
のようなコードで大域変数(として参照されるオブジェクト)を生成するのは不可能となります。念のため、確認すれば:
js> function makeFoo() {
eval("var Foo = {};");
}
js> Foo
js: "<stdin>", line 18: uncaught JavaScript runtime exception: ReferenceError: "Foo" is not defined.
js> makeFoo()
js> Foo
js: "<stdin>", line 20: uncaught JavaScript runtime exception: ReferenceError: "Foo" is not defined.
js>
●あーアホでした … えっ?! だがしかし
僕が最初に書いていたコードは、if (!TrimPath) var TrimPath = {};
です。if内で裸の変数参照を使い、varも使っているから、こりゃダメだ。あー、檜山はアホでした -- で話が済むならいいのですが、また別な事実があります。
なんと、上記のとんでもない(はずの)コードが、JSANアーカイブにアップロードされ公開されているモジュールでイディオムのごとく平気で使われています。手元にあるのだけ見ても:
// Debug.Logger-0.01
if(!Debug) var Debug = {};// CSS.Change-0.02
if (!CSS) var CSS = {};// HTML.Popup-0.04
if (!HTML) var HTML = {};
これらのモジュールはちゃんとテストされている(と思う)し、問題なく動いている(と思う)のですが……
こっ、これってなによ?
[追記 date="12-07"]謎は解けました。 そのうち、解説する機会もあるでしょう。[/追記]