2012年11月26日月曜日

JSXでグローバル変数を扱う際に気をつけること

ここで言うグローバル変数

一口にグローバル変数と言っても言語や環境によって意味が異なりますが,ここではWebブラウザ上でJavaScriptを扱う場合のグローバル変数を指すこととします.もう少し具体的に言えば,JavaScriptに於いてvarを付けずに利用した変数で,内部的にはwindow.変数名です.そして,JSXでグローバル変数を扱う場合には「js.jsx」をインポートした上で「var foo = js.global["foo"] as number;」のようにして使います(使用するグローバル変数がfooで,その型がnumberの場合).なので,グローバル変数fooを取得してコンソールに表示するプログラムは以下のようになります.

気をつけること 〜上書き代入してはいけません〜

上記のように簡単に値を取得できるので,ついつい代入したくなりますよね?ダメです.js.globalは代入には対応していません.正確に言えば,代入された瞬間からその変数はjs.globalではなく代入された値を保持します.以下のソースの7行目でfooに100を代入しています.8行目のようにfooを確認すると100と表示されるので一見上手く行っているように見えますが,9行目のようにjs.global[ "foo" ]を確認すると1が表示されます.

せっかくなので 〜クラス変数だと切り分けが面倒〜

実は上記が全てです.ですが,せっかくなので悩んだ軌跡を紹介します.クラス変数(static変数)と組み合わせるとデバッグが面倒になります.もちろん上記原則をきちんと分かっていれば良かったのですが,分かっていなかったばっかりに難儀しました.

元々やりたかったことは,グローバル変数を介してJSX以外で書かれたプログラムと連携することです.「もしかするとグローバル変数が初期化されていないかもしれないのでコンストラクタでnullかどうか判断して,nullだったら初期化しよう」としたのが間違いでした.以下のソースをご覧ください.3行目でクラス変数mapにグローバル変数globalを割り当てています(もちろん,本物のソースはglobalなんて変数名は使っていませんよ).と言う訳で,問題となったのは6行目です.見事に代入しちゃってます.おかげで,このJSXファイル内ではまともに動作するのに,他のJSとは連携してくれないという中途半端な状態になってしまいました.

試しに,以下のソースをコンパイルしてWebブラウザ上で実行して,コンソールからglobalの内容を表示させようとすると「ReferenceError: global is not defined」と表示されてしまいます.

普通やらないよね 〜安易にlogを使わない〜

通常ならばChromeのデバッガなどを使うところですが,そのときたまたま--enable-source-mapオプションを付けずに開発していたので,logを入れまくってデバッグしていました.その結果,動作しないことに変わりないけど,logの有無によって挙動が異なるようになりました.コンパイルされたJSファイルとにらめっこしながら,「これグローバル変数にアクセスしてないよな」などと考えながら,「logの有無による挙動の差」のほうが気になりだしました.既にプログラムは「js.global[ "global" ] = {}: Map.<string>;」を使って初期化すれば上手くいくことが分かっていました.

ますます???となってきたので思考をまとめるために「js.globalに書き込むとうまくいく」,「タイミングによって挙動が変わるのは$__jsx_lazy_initが原因か?」とtweetしたところ,@__gfx__様から「js.globalに書き戻すことが必要」と「lazy initが悪さしているかも」との教えを授かりました(tweetの内容は短縮しています).

一緒にこちらのURLを授かったので参照したところ『最後の $__jsx_lazy_init の部分が曲者で、static 変数に対してリテラル以外で初期化すると $__jsx_lazy_init によって初期化されるようです。名前から想像がつくと思いますが、これによってこの変数が使われるときに初めて初期化が行われます。』と書かれていました.なるほど,logを入れまくると挙動が変わるわけです.上のソースで言うと,5行目と6行目の間に「log glb.map;」を入れた時だけ挙動が変化しました.

ちょっと疑問なのは,if文の条件式の中で参照していると思うのですが,そうではなさそうです.単に私の検証が足りないだけかな?

結論

色々書きましたが,「js.globalを代入した変数に対してさらに代入しない(配列に追加するのは大丈夫)」「代入したければjs.globalに直接代入する」「普通は問題にならないけど,デバッグ時のクラス変数の初期化のタイミングには気をつける」「デバッガを使う」「@__gfx__様の最初のtweetですでに答えが出ていたので素直に聞く」などを心がけましょう.

ちなみに上記のソースコードは以下のようにすればきちんと動作します.※これが最適な書き方かどうかについては責任を負いません.

最後に

@__gfx__様,助言をいただきありがとうございました.

このエントリーをはてなブックマークに追加