Kanasan.JS CodeReading #32008年02月08日 22時17分

Kanasan.JSPrototype.js CodeReading #3 (参加者のブログ記事一覧) に行ってきた。今回は告知が開催間際だったせいか人数はやや少なめだったけど、内容的にはこれまでと変わらぬ濃さ。範囲としては Prototype.js 1.6.0.2 の 1352 行目から 1650 行目付近まで。

コードリーディング

無線ネットワークが提供されているはずが私のマシンでは利用できず。LAN ケーブルをお借りして有線で接続。それにしても私がこれまでに参加した Kanasan.JS でネットワーク関係の不備に陥ること 4 回中 4 回。何か呪いでもかけられているのかと疑いたくなる。

例外処理の有無

Ajax.Response#getStatusText などは try 文による例外処理を行っているのに、Ajax.Response#getResponseHeader および Ajax.Response#getAllResponseHeaders に例外処理がないのはなぜかという話題 (例外処理の範囲はできるだけ小さくしたほうがいいだろうに)。例外が起きたときの処理を場合によって変えやすくするためではといった意見が。後から見直すと、XMLHttpRequest オブジェクトの同名のメソッドをそのまま呼び出すときだけ例外処理がないような気がする。XMLHttpRequest オブジェクトの処理を、例外も含めて透過的に扱えるようにするためか?

ちなみに、JavaScript では try 文が複数の catch 節を持てないという話が出たが、JavaScript 1.5 以降では可能だし、シンタックスは違うものの ECMAScript 4 でも可能になる予定だ。

// JavaScript 1.5 conditional catch clauses
try {
  [].length = -1;
} catch (e if e instanceof TypeError) {
  print("invalid type");
} catch (e if e instanceof RangeError) {
  print("invalid index");
}
// => invalid index
// ECMAScript 4 multiple catch clauses
try {
  [].length = -1;
} catch (e : TypeError) {
  print("invalid type");
} catch (e : RangeError) {
  print("invalid index");
}
// => invalid index

Ajax.Response#_getHeaderJSON

X-JSON ヘッダの値を変数 json に代入し、その後 json = decodeURIComponent(escape(json)) としている。これは、変数 json の値が UTF-8 バイト列だった場合、それを UTF-16 文字列に直す働きがある。まず escape 関数によって 0x80 - 0xFF の範囲の文字が %XX という文字列に置き換えられ、次に decodeURIComponent 関数によって %XX%XX%XX といった UTF-8 の URI エスケープ表現が元の文字に置き換えられる。

// 「日本語」のバイト表現は:
// UTF-8:    E6 97 A5 E6 9C AC E8 AA 9E
// UTF-16BE: 65 E5 67 2C 8A 9E
var str = String.fromCharCode(0xE6, 0x97, 0xA5,
                              0xE6, 0x9C, 0xAC,
                              0xE8, 0xAA, 0x9E);
str.length; // => 9
str.charCodeAt(0).toString(16); // => e6
str = escape(str); // => %E6%97%A5%E6%9C%AC%E8%AA%9E
str = decodeURIComponent(str); // => 日本語
str.length; // => 3
str.charCodeAt(0).toString(16); // => 65e5

RFC 2616 によれば、RFC 2047 に従った形で符号化されない限り、HTTP ヘッダフィールドの値の文字符号化方式は ISO-8859-1 であるとみなされるようなので、UTF-8 オクテット列をそのまま送られたのなら、このような処理を挟む必要があるのだろう。実際のブラウザの実装がどうなっているかは知らないが。

Prototype.js では、プライベートなプロパティ、メソッドの名前はアンダースコア (_) から始まるようにしている。これは JavaScript の言語仕様によるものではないが、このようなコーディング規約を採用するところは多い (ぜんぜん見ないという声も)。

Ajax.Response#_getResponseJSON

HTTP ヘッダのフィールド名は大文字小文字を区別しない (RFC 2616 4.2)。XMLHttpRequest オブジェクトの getResponseHeader メソッドの引数として "Content-Type" を渡しても "Content-type" を渡しても、XMLHttpRequest オブジェクトが内部で同様に扱ってくれるはず。

if 文の条件節のインデントが不ぞろいで読みにくいという話から演算子の優先順位の話へ。論理和 (||) よりも論理積 (&&) のほうが優先順位が高い。この簡単な覚え方として、「和 (= 加算、+)」よりも「積 (= 乗算、*)」のほうが優先順位が高いのと同じというものがある。ビット和 (|) とビット積 (&) に関しても同様。最初、論理和演算子と論理積演算子の優先順位は同じと勘違いしていたのだが、そういう言語って何かあったっけ?

Node

Firefox や Opera、Safari 3 以降では、DOM 3 Core ECMAScript Language Binding に従って、Node コンストラクタ関数を提供している。Node コンストラクタ関数は Node インターフェースで定義された定数をプロパティとして持つ。

Node.ELEMENT_NODE; // => 1
Node.DOCUMENT_NODE; // => 3
document.nodeType == Node.DOCUMENT_NODE; // => true

Prototype.js では、IE でもこれらの定数が利用できるよう、Node コンストラクタ関数が存在しない場合は、新規オブジェクトを作成して Window オブジェクトの Node プロパティ (グローバル変数 Node) に設定している。

なお、Node コンストラクタ関数とはいっても、関数として呼び出したり、new 演算子とともにコンストラクタとして用いることはできない。ただし、Firefox や Opera、Safari 3 以降では、Node.prototype の値に変更を加えることで、Node インターフェースを実装するオブジェクトのプロパティ、メソッドを変更することができる。

Node.prototype.foo = 42;
Node.prototype.bar = function () { return this.nodeName; };
document.foo; // => 42
document.bar(); // => #document
// Document インターフェースは Node インターフェースを
// 継承しているので、document オブジェクトは
// Node インターフェースを実装しているといえる。

Element

Node と同じく、ブラウザによっては DOM Core の Element インターフェースに対して Element コンストラクタ関数 (ただし関数呼び出し、コンストラクタ呼び出しはできない) を提供している。しかし、Prototype.js では、Element をコンストラクタとして扱えるようにするため、Window オブジェクトの Element プロパティ (グローバル変数 Element) の値を書き換えている。その際、元の Element コンストラクタ関数のプロパティをそのまま扱えるようにするため、元の Element コンストラクタ関数を一時変数 element に退避している。

// Prototype.js 1.6.0.2 ll. 1546-1561
(function() {
  // ここで元の Element コンストラクタ関数の値を退避。
  var element = this.Element;

  // Element プロパティの値に関数を設定することで、
  // Element コンストラクタとして使えるようにする。
  this.Element = function(tagName, attributes) {
    ...
    // ここで、Element.cache の Element は
    // Element プロパティに新しく設定された無名関数。
    // グローバル変数 Element とはグローバルオブジェクト
    // (ブラウザ上では Window オブジェクト) の
    // Element プロパティのことであり、この関数内の
    // コードが実行される時点で Element プロパティの
    // 値は既に無名関数に書き換えられているため。
    var cache = Element.cache;
    ...
  };
  // 元の Element コンストラクタ関数のプロパティを、
  // 新たな Element コンストラクタ上でも使えるようにする。
  Object.extend(this.Element, element || { });
}).call(window);

全体を関数式で囲んでいるのは一時変数 element をグローバルに公開しないためか。単に (function() { ... })() とするのと変わらないはずなのに、なぜわざわざ (function() { ... }).call(window) と call メソッドを使っているのかは不明。

IE では要素の name 属性の値を動的に変更することができないので、IE で name 属性を持つ要素を作成するときは場合分けをして、document.createElement メソッドの引数に、name 属性を含めた開始タグを渡すようにしている。createElement メソッドの引数として要素名だけでなく開始タグも受け付けるというのは、もちろん IE の独自拡張仕様。

要素を作成する際、キャッシュを用意して cloneNode メソッドを呼び出すことで新しく要素のコピーを作成している。多くのブラウザでは、毎回 createElement メソッドを使って新しく要素を作るよりもパフォーマンスがよいようだ。

要素作成にかかる時間 (単位: ms)
IE 6.0 SP2 Opera 9.25 Firefox 2.0.0.12 Firefox 3.0b4pre
createElement 1125 1344 3140 1701
createElement (document を変数に保持) 640 969 3062 1522
cloneNode 500 750 2343 1194

Element.Methods

まだ読み進めてないが、Element.Methods で定義されたメソッド x は、Element.x(element, args) と対象の要素を引数にとって呼び出すことも、element.x(args) と対象の要素のメソッドとして呼び出すこともできるようになるのだろう。

Element.Methods.update

Object.toHTML メソッドは、引数が toHTML メソッドを持っていればその返り値を、そうでなければ引数を文字列化した値を返す。ただし、引数が null または undefined のときは空文字列を返す。よって、element.update() とすると element.innerHTML に空文字列が設定され、結果として element の内容がすべて削除されることになる。

innerHTML に script 要素を含んだ文字列を設定してもそのスクリプトは実行されない。そこで、文字列から script 要素の内容を抜き出してスクリプトとして評価している。この処理には defer メソッドが適用され、update メソッドの終了後にスクリプトの評価が行われるので、処理時間の長いスクリプトが含まれていても update メソッドの実行が長引くことはない。

Element.Methods.replace

update メソッドが対象の要素の内容を置き換えるのに対し、replace メソッドは対象の要素そのものを置き換える。

DOM 2 Range (IE 7 は非対応) および Mozilla 独自拡張の createContextualFragment メソッド (SafariおよびOpera 9以降でも対応) を使っているのに、なぜ IE でも動くのかと思ったら、2445 行目以降で要素オブジェクトが outerHTML プロパティを持つ場合 (IE、Opera、Safari が該当) はメソッドの内容を書き換えていた。

Element.Methods.insert

引数に { before: beforeContent, bottom: bottomContent } といったオブジェクトを渡すことで、複数の箇所に内容を挿入できる。そうでなければ bottom の位置 (要素の元の内容の終端) に挿入される。

Element._getContentFromAnonymousElement

Element.insert の内部などで使われているヘルパメソッド。HTML のソースコードを文字列として受け取り、それを DOM ノードに変換して配列として返す。引数は tagName、html の 2 つでいずれも文字列型。html は変換する HTML のソースコードを表し、tagName は html を内容として持つ要素名 (親要素名) をあらわす。たとえば、p 要素の内容の終端に <em>Hello</em>, world! という内容を挿入したいとき、

// 変数 p はある p 要素を指す。
Element.insert(p, "<em>Hello</em>, world!");
// p.insert({ bottom: "<em>Hello</em>, world!" }) としても同じ。

と insert メソッドを呼び出したとすると、その内部で、

Element._getContentFromAnonymousElement("P", "<em>Hello</em>, world!")

と _getContentFromAnonymousElement が呼び出される。_getContentFromAnonymousElement の内部では、新たに div 要素を作成し、その innerHTML プロパティに引数 html の値 (ここでは "<em>Hello</em>, world!") を設定した後、その childNodes プロパティを配列に変換して返す。よって、この場合は、em 要素ノードとテキストノード (表す文字列は ", world!") との 2 要素を収めた配列が返ってくる。

しかし、このままでは、たとえば表の行にセルを追加したいときなどに対応できない。このとき、

Element._getContentFromAnonymousElement("TR", "<th>Header</th><td>Cell</td>")

と呼び出されるが、div 要素の直接の子要素として th、td 要素をとることはできないからだ。そこで親要素名 "TR" が用いられることになる。特定の親要素名に対しては、前後にタグを補完した値が div 要素の innerHTML プロパティに設定される。この場合は、

div.innerHTML = "<table><tbody><tr>" +
                "<th>header</th><td>Cell</td>" +
                "</tr></tbody></table>";

といった具合だ。その後、tr 要素の childNodes プロパティを配列に変換して返すことになる。つまり、ここでの返り値は th 要素ノードと td 要素ノードを収めた配列だ。このとき、ローカル変数 div は、最終的に div 要素ではなく tr 要素を指すようになるので注意が必要。

懇親会、その他

IE では table 要素の直下に tr 要素を追加しても表示されないとか、HTML 4 では tbody 要素の開始タグと終了タグは省略可能だけど tbody 要素自体は table 要素の子どもに必ず存在するとか、PHP 4 と VB 6 はしぶとく生き残り続けるんじゃないのとか、PHP 4 == Perl 4 とかいった話題が出ていたような気がする。getElementsByTagName メソッドの引数に "*" を指定して全要素が取れるのはIE 5.5以降と言ったけど、帰ってから試したら IE 6 以降みたいだ。

そんなこんなでコードリーディング以外にもいろいろと興味深い話が聞け、得るものの多い一日だった。Kanasan さんはじめ参加者の皆さんお疲れ様 & ありがとう。

コメント

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※投稿には管理者が設定した質問に答える必要があります。

名前:
メールアドレス:
URL:
次の質問に答えてください:
「ハイパーテキストマークアップ言語」をアルファベット4文字でいうと?

コメント:

トラックバック

このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2008/02/08/2611668/tb