Jan 24, 2008

Firefox 3でコンテキストオブジェクトを明示して、日本語などを含むスクリプトを実行するための方法

UxUのことで悩み中。

テストケースを実行するとき、コンテキストオブジェクトを指定してスクリプトを実行しないといけないんだけど、色々問題があって悩ましい。

brazilさんが紹介されているとおり、ビルトイン関数のevalに第2引数を渡すか、Objectクラスのメソッドであるevalを使うと、コンテキストオブジェクトを指定してスクリプトを実行することができる。ただ、この両者は若干動作が異なる。

例えばこんなコードを考えよう。

var context = {};
context.eval('var val1 = true; this.val2 = false');
alert(context.val1); // => true
alert(context.val2); // => undefined

Objectクラスのメソッドだと、実行したスクリプトの中で変数として宣言した物がそのままそのオブジェクトのプロパティとなり、外部から後で参照することができる。でも、

var context = {};
eval('var val1 = true; this.val2 = false', context);
alert(context.val1); // => undefined
alert(context.val2); // => undefined

ビルトイン関数の方だと、そうはならない。また、どちらにしてもthisを明示した場合は外部からは参照できない。

言い換えると、「コンテキストオブジェクトを明示して実行したスクリプトの中でvarで宣言された値を外部から取得するには、Objectクラスのevalメソッドを使わないといけない」ということ。

ところが、Firefox 3ではObjectクラスのevalメソッドが存在しない。どうやらどこかの時点で削除されてしまったようだ。

元のMozLabではどうしていたかというと、evalを使う代わりにmozISubScriptLoaderを使っていた。

var context = {};
var loader = Components.classes['@mozilla.org/moz/jssubscript-loader;1']
          .getService(Components.interfaces.mozIJSSubScriptLoader);
loader.loadSubScript(
  'data:application/x-javascript,'+encodeURIComponent('var val1 = true; this.val2 = false'),
  context
);
alert(context.val1); // => true
alert(context.val2); // => false

こちらは前二者のどっちとも結果が異なり、varで宣言した物もthisを明示した物も両方ともコンテキストオブジェクトのプロパティとして外部からアクセスできるようになる。

ところが。mozIJSSubScriptLoaderを使う方法だと、スクリプトの中に日本語などの非ASCII文字があると化けてしまう。

var script = 'alert("日本語")'; // Unicode

eval(script); // => "日本語"
({}).eval(script); // => "日本語"
loader.loadSubScript('data:application/x-javascript,'+encodeURIComponent(script), {}); // => 文字化けした文字列

encodeURIComponentによってUnicodeの文字列がUTF-8のバイト列に変換されてしまうのでこうなる、のか? とにかく、これではテストケースの説明を日本語で書けなくて(僕が)困る。UxUではこの処理の直前でUTF-8なりShift_JISなりで書かれたテストケースを読み込んで内部コードのUnicodeに変換しているのだけれども、せっかく変換したのに最後の最後で化けてしまうんじゃあしょうがない。

ということでああでもないこうでもないと試していて、以下のような方法に辿り着いた。

var script = 'alert("日本語")'; // Unicode
script = 'eval('+script.toSource().replace(/^\(new String\(|\)\)$/g, '')+')';
loader.loadSubScript('data:application/x-javascript,'+encodeURIComponent(script), {}); // => "日本語"

encodeURIComponentに放り込む前に、一旦全体を文字列リテラルとして評価可能な文に変換して(この時点で日本語などの非ASCII文字は「\uXXXX」のようなUnicodeエスケープに変換される)、encodeURIComponentを通過した後でevalで元に戻す、というトンネル抜けのようなやり方。これによって、日本語で書かれた説明もそのまま利用できるようになった。

でも、これにもまだ問題がある。この方法で実行したスクリプトの中でエラーが起こると、MozUnitテストランナーのUI上でソースを表示してもeval("(元のテストケースのスクリプト)")という1行だけのソースになってしまって、エラー箇所がさっぱりわからない。これ、どうにかならんもんだろうか……

追記。エラーが発生した行の番号自体はこれでも正しく取れてるようなので、とりあえずやっつけ仕事で、MozUnitテストランナーのUI上でソースを表示する時にソースを元の文字列に復元するという方向で手を打とうと思う。

さらに追記。よく考えたら、わざわざ自分でソースを復元しなくても、変換前後で行数その他は変わってないんだから、ソース表示の時に元のファイルの方を読み込ませるようにしたらいいんだな……

さらにさらに追記。mozIJSSubScriptLoaderの仕様変更によってこの方法も使えなくなりました。現在何かいい手は無いか考え中。

さらにさらにさらに追記。上記内容と同じような結果になる代替案を考えてみた。

さらにさらにさらにさらに追記。evalの機能についてFirefox 3.1でまた変更があったようだ

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能