Kanasan.JS レポート2007年11月04日 14時23分

関西での JavaScript 勉強会、Kanasan.JS に行ってきた。内容は Prototype.js のコードリーディング (バージョン 1.5.1.1 を使用)。「家に帰ってレポートを書き、主催者のブログにトラックバックを送るまでが勉強会です」と念を押されたので、まもなく Kanasan さんの記事経由で参加者各々の様子をうかがえるようになると思うけど、以下あとから思ったことも加えて私の場合をつらつらと。

JavaScript 概説 & コードリーディング

プロトタイプ

やはり皆さん一番引っかかるのはここみたい。おそらく、プロトタイプと名のつくものが 2 つあるのが混乱の原因だと思う。

prototype プロパティ
関数 (Function オブジェクト) のみが持つ。その関数をコンストラクタとして使ったときに用いられる。
[[Prototype]] 内部プロパティ
すべてのオブジェクトが持つ。プロパティ / メソッドを参照するときは、自分自身、自分の [[Prototype]] 内部プロパティに指定されているオブジェクト、さらにその [[Prototype]] 内部プロパティに指定されているオブジェクト、……といった具合に、そのプロパティ / メソッドが見つかるまでプロトタイプチェーンをたどることになる。内部プロパティなので普通に JavaScript コードを書いているときに参照はできないが、__proto__ プロパティとして公開している実装 (Firefox で使われている SpiderMonkey、Safari で使われている JavaScriptCore など) もある。

prototype プロパティと [[Prototype]] 内部プロパティの関係については、私も以前 JavaScript の new 演算子の意味で少し触れたほか多くの解説記事があるし、これを押さえておくと JavaScript の理解がぐっと進むと思う。Prototype.js を読む上でも、今追加しているのがいわゆるクラスメソッドなのかインスタンスメソッドなのかといったことがわかりやすくなるだろうし。

レキシカルスコープとダイナミックスコープ

JavaScript はレキシカルスコープを採用。関数内で定義されていない変数が実際にどの変数を指しているか、静的に (コンパイル時に) 決定できるのがレキシカルスコープ、実行時にならないとわからないのがダイナミックスコープ。

Perl でいうと、my で定義された変数 (Private Variables via my() の項) はレキシカルスコープを持ち、local で定義された変数 (Temporary Values via local() の項) はダイナミックスコープを持つ (個人的には my と local どう違う? - futomi's CGI Cafe の例がわかりやすかった)。Common Lisp はレキシカルスコープだが、Emacs Lisp は (レキシカルスコープにすると重くなるから) ダイナミックスコープらしい。

Opera オブジェクト

Prototype.js ではブラウザが Opera かどうかの判別に window.opera の有無を用いている。Opera では Opera オブジェクトwindow.opera プロパティの値。Opera オブジェクトは、"9.23" といったバージョンを文字列で返す version メソッドなどを持つ。

Prototype.K

名前の由来が謎。K コンビネータ説は motemen さんより。やってることは恒等関数だから個人的には id とするのが一番しっくり来るんだけど、それだと document.getElementById と紛らわしいか。

Number.prototype.times

Ruby の Integer#times から。ただし、5.times(func) とやると 5. までが数値リテラルとみなされるので構文エラーになる。(5).times(func)5..times(func)5 .times(func) などなら大丈夫。

String.prototype.gsub

Ruby の String#gsub から? 機能的には組み込みの String.prototype.replace とそう変わらないと思うけど、Ruby とインターフェースを合わせる、独自のテンプレート書式を使う、ブラウザのバグを避けるといった目的でわざわざ定義しているのではないか。

暗黙の型変換

数値型への変換では、null0undefinedNaN として扱われる。null * 20var x = null; x--; とすれば、x が数値として評価され、最終的に x の値は -1 になる。比較演算子における型変換規則等価演算子における型変換規則は非常にややこしいので、できるだけ暗黙の型変換が起こらないようにしたほうがいいかも。

inject 症候群

簡単なループで書けるところも、わざわざ Enumerable.inject (Ruby の Enumerable#inject から。JavaScript 1.8 の Array.prototype.reduce) を使っているのではという疑惑が発生。そのほうがわかりやすいという声が挙がるも、変態の意見と一蹴される。

String.prototype.succ

単に Unicode のコードポイントが次の文字を求めるだけ。Ruby の String#succ のように "9""10" にしたり (Perl でいうマジカルインクリメント) はしない。

String.prototype.underscore

"::""/" にするなど、名前以上のことをやりすぎ。

雑感

顔を付き合わせてひとつのコードについて話し合うというのは初めてだったけど、すごく楽しかった。何より良かったのは、Ruby をはじめとしてほかの言語に堪能な方の見地を聞けたこと。普段と違った視点から JavaScript を眺めることができ、新鮮な気分を味わった。次回以降もぜひ参加したいと思う。主催してくださった Kanasan さん氏久さんはじめ参加各氏に感謝。

それから、お菓子が出るっていうのは非常に大きい。やっぱり人間甘いものがないと生きていけないから。アルフォートとかもう最高。これがあるとないとでは能率が 70% くらい違うと思う。会場前のカレーハウス ガラムの旬の野菜カレーも野菜が山盛りでおいしかった。

ちなみに、Prototype.js からは Ruby の影響が随所にうかがえ、Ruby.js の流れを汲んでいるんだろうなと勝手に思っていたのだけれど、Prototype.js 1.0.0 のソースコードを見る限りでは直接の関係はなかったみたいだ。

Safari 3 の XPath のバグ2007年11月10日 13時52分

Mac OS X v10.5 Leopard 上では一足早く正式版が公開された Safari 3。JavaScript でのゲッタとセッタのサポートや DOM 3 XPath の実装など、新機能が山盛りです。しかし、Safari 3.0.4 の DOM 3 XPath 実装にはいくつかバグがあるので、それらをここで取り上げてみようと思います。

逆方向軸に関するコンテキスト位置

XPath 1.0 では、式を角括弧で囲んだ述語を用いることで、ノードセットから結果を絞り込んでいくことが可能です。例えば、child::*[position() mod 2 = 0] なら、子要素のうち偶数番目のものだけを選び出すといった具合です。

ここで、position 関数により得られるコンテキスト位置は、元のノードセットを文書順で並べ替えたとき、現在処理中のノードが何番目になるかを示しています。ただし、ancestor、ancestor-or-self、preceding、preceding-sibling の各軸は逆方向軸と呼ばれ、これらの軸に対する述語内では、ノードセットを文書順の逆順で並べ替えたときの順番からコンテキスト位置が得られます。つまり、

<a>
  <b>
    <c/>
  </b>
</a>

という XML 文書があったとき、/descendant::c/ancestor::*[1] は、a 要素と b 要素を文書順の逆順で並べ替えたときの最初のもの、すなわち b 要素を指します。

ところが、Safari では、逆方向軸に対する述語中のコンテキスト位置の算出方法が間違っており、ancestor、ancestor-or-self、preceding-sibling 軸に対する述語中でも文書順でコンテキスト位置を算出してしまいます。preceding 軸に対しても、文書順ではないですが間違った順序でコンテキスト位置が算出されます。すなわち、上の例では a 要素を返してしまうということです。

このバグを回避し、ひとつの XPath 式で Safari でもほかのブラウザと同じ結果を得られるようにするためには、ノードセットをあらかじめ文書順に並べ替えておくといった方法が考えられます。(/descendant::c/ancestor::*)[last()] のように、述語の対象をロケーションステップから基本式へと変えることで、基本式で得られたノードセットを文書順に並べ替えてから述語が適用されるようになります。順番が逆になるので、1 番目としていたノードは、last 関数を使い最後のものと指定することになります。

ただし、このアプローチは常にうまくいくわけではありません。例えば、以下の XML 文書を考えてみてください。

<a>
  <b xml:id="first"><c/></b>
  <b xml:id="second"><c/></b>
  <b xml:id="third"><c/></b>
</a>

ここで、/descendant::c/ancestor::*[1] は、各 c 要素の最初の祖先要素を集めたもの、すなわち first、second、third の 3 要素からなるノードセットになります。しかし、(/descendant::c/ancestor::*)[last()] とすると、各 c 要素の祖先要素を集めたものの中で最後のもの、すなわち third のみからなるノードセットになってしまいます。

id 関数に指定する文字列

id 関数を使えば特定の id を持つ要素を一気に取得できますが、Safari では要素をうまく取得できないことがあります。例えば、文書中に a、b という id を持つ要素があったとして、id('a') としても空のノードセットしか返ってきません。id('a b') とすると、本来ならば a、b の 2 要素を含むノードセットが返るはずが、a のみが含まれるノードセットが返ってきます。

この問題は、文字列の最後に空白文字を付け足すことで解決します。つまり、id('a ')id('a b ') とすれば、期待通りの結果が得られます。また、引数がノードセットのときには、このような問題は発生しません。

なお、id 関数に関しては、Opera でも id 関数のみからなる XPath 式がエラーになることがあるといった問題が存在するようです。

substring-after 関数

substring-after(str1, str2) は、str1str2 を含んでいれば str2 が見つかった位置に str2 の長さを足した位置以降の文字列を、含んでいなければ空文字列を返します。しかし、Safari では、str2 が見つかった位置を無視して、常に str1 のうち str2 の長さ分の位置以降の文字列を返してしまいます。substring-after('abcde', 'd') が、本来ならば 'e' となるところを、Safari は 'bcde' としてしまうといった具合です。

これを避けるためには、substring-after 関数を、substring(str1, string-length(substring-before(str1, str2)) + string-length(str2) + 1, contains(str1, str2) * string-length(str1)) のように書き換えてやる必要があります。あるいは、バグをもってバグを制すというのなら、substring(substring-after(str1, str2), string-length(substring-before(str1, str2)) + 1) のように、もう少し短くも書けます。

補記

これらのバグのうち、逆方向軸に関するコンテキスト位置と substring-after 関数とについては、amachang さんが作成した XPath 機能テストにより明らかになりました。また、いずれのバグも既に WebKit Bugzilla に登録され、修正済みとなっています (Bug 15380Bug 15436Bug 15437)。最新の WebKit Nightly Builds を使えば問題は起こらないはずですし、Safari においても、Mac OS X のアップデートなどに伴って修正されるかもしれません。

なお、これらはいずれも DOM 3 XPath のインターフェースを用いて XPath を扱ったときの問題であり、XSLT 中に書かれた XPath では発生しません。これは、WebKit が DOM 3 XPath に関する処理を自前で行っているのに対し、XSLT の処理は libxslt に任せているからだと思われます。

オレオレ言語の MIME タイプ2007年11月23日 09時15分

OreScript時代の幕開け - yukobaの日記」を筆頭に、JavaScript によるプログラミング言語の実装が流行っているようです。オレオレ言語はその名のとおり各人が好きに作るものですが、それらの言語を識別する MIME メディアタイプ (以下 MIME タイプ) に関してはちょっと違います。他人のつけた MIME タイプと競合してうまく動かないといったことのないよう、意識して設計しなくてはいけません。

MIME タイプの仕様

MIME に関連する規格は多数ありますが、MIME タイプに関して重要なのは RFC 2046 Multipurpose Internet Mail Extensions (MIME) Part Two: Media TypesRFC 4288 Media Type Specifications and Registration Procedures です。それによれば、MIME タイプはトップレベルタイプとサブタイプからなり、トップレベルタイプは text、image、application などあらかじめ定められた中から選択します。両者はスラッシュで区切られ (トップレベルタイプ/サブタイプ)IANA に登録され管理されています。トップレベルタイプ、サブタイプともに大文字小文字を区別せず、最大 127 文字まで使用できます。

ここで、オレオレ言語のトップレベルタイプとして text が指定されがちなのですが、本来 text とは特別なソフトウェアや前提知識なしでも読めるリソースに対して用いられるものであり、プログラミング言語にはふさわしくありません。ソースコードは基本的に機械が解釈するものなので、application タイプを使ったほうがいいでしょう。

サブタイプに関しては、ツリーと呼ばれるさらに細かい区分があります。ツリーはドットで区切られ、サブタイプ中にドットが含まれない場合は通常、標準ツリーに属するものとみなされます。標準ツリーのサブタイプの登録には標準規格の発行と IESG による承認が必要であり、オレオレ言語には荷が重いでしょう。

ほかにもツリーには、vnd. から始まるベンダツリーと prs. から始まる私的ツリーがあります。いずれも IANA に直接登録を申請することができますが、事前に ietf-types メーリングリストでレビューを受けることが推奨されています。

さらに、サブタイプが x. または x- で始まっていた場合は、実験的なツリーに属するものとみなされます。これらのサブタイプは登録なしで使える (むしろ登録してはいけないことになっている) ので、現状オレオレ言語に最も適しているといえるでしょう。

IE の MIME タイプの取り扱い

IE が script 要素を処理するとき、まずは type 属性を見ます。type 属性の値が text/script-name だった場合、IE は Windows レジストリの HKEY_CLASSES_ROOT\script-name を探し、そこに Active Script エンジン (ActiveX スクリプティングインターフェースを実装するスクリプトエンジン) が登録されていれば、そのエンジンを用いてソースコードを実行します。application/script-name だった場合は何も実行しません。

ですから、IE 以外のブラウザでは JavaScript でオレオレ言語を実装し、IE に対しては Active Script を利用して、より高速なスクリプトエンジンを提供しようとした場合、MIME タイプのトップレベルタイプを text にしたほうが都合がいいということもあります。

そのような場合でも、MIME タイプを登録するのなら、RFC 4329 Scripting Media Types に倣ってトップレベルタイプが text のものと application のもの 2 つを登録し、text のほうを非推奨としたほうがいいでしょう。

まとめ

  • オレオレ言語の MIME タイプは application/x-script-name という形がお勧め。
  • IANA に登録するなら、私的ツリーかベンダツリーが簡単。
  • Active Script エンジンを提供するなら、text タイプの使用もやむをえない。ただし、IANA に登録するなら application タイプも忘れずに。

個人的には、サブタイプに作者名を含めれば衝突を回避するのに十分ではないかと思っています。実例を挙げると、「ブラウザで S 式をつかいたい>< - 女子高生ぷろぐらまーなお☆のブログ」の application/x-naoscheme や、reStructuredText に用いられる text/prs.fallenstein.rst などです。より堅固性を求めてドメイン名を使うのなら、application/x.org.example.script-name といった感じになるでしょうか (この例だと何だかウィンドウシステムっぽく見えなくもないですが)。