E4Xのプロパティアクセスと比較演算子

今度はみんなが大好きなE4Xの不思議に迫るよ!

実はこれ、Firefox Hacks Rebootedにも少し書いた内容だけど良いよね。

最初に書いておくと、「E4Xすばらしい! これからどんどん使っていこう!」という内容ではない。残念ながら。むしろ、捨てましょう、という内容になってしまっている。StrictモードではBANされたし、もう良いよね...(かなり自虐的)

  1. 普通のオブジェクトの内部関数[[Get]]について
  2. E4Xの内部関数[[Get]]について
  3. 同値演算子(===)と等値演算子(==) について
  4. まとめ的なもの

E4Xでない普通のオブジェクトの内部関数[[Get]]

E4Xに入る前に普通の[[Get]]から。

var o = {
  p: "FOO"
}

o.pとすると、当然"FOO"が返ってくるわけだけど、oオブジェクトのプロパティpを得るのに内部的に[[Get]]という関数を呼び出している。

ECMA-262の仕様では以下の様なステップを踏む。

オブジェクトoの内部関数[[Get]]がプロパティ名pを伴って呼び出されたとき、以下のステップが取られる

  1. プロパティ名pで内部関数[[GetProperty]]を呼び、結果をdescとする
  2. もし、descundefinedなら、undefinedを返す
  3. もし、descDataDescriptorなら、desc.[[Value]]を返す
  4. そうでない場合、descAccessDescriptorでなければならず、desc.[[Get]]をgetterに入れる
  5. もし、getterundefinedなら、undefinedを返す
  6. オブジェクトo経由でgetterを呼び([[Call]])、結果を返す

E4X内部関数[[Get]]

さて、E4Xでは、通常の[[Get]]を書き換えている。

var o = <o name="XML element">
  <p>FOO</p>
</o>;

どのようなプロセスになるかというと

XMLオブジェクトoの内部関数[[Get]]がプロパティ名pを伴って呼びだれたとき、以下のステップが取られる(ちょい端折って書くよ)

  1. もし、ToString(ToUnit32(p)) == p ならば、
    • a. list = ToXMLList(x) とする
    • b. 引数pとともにlistの[[Get]]を呼び出した結果を返す
  2. n = ToXMLName(p) とする
  3. list を、list.[[TargetObject]] = x および、list.[[TargetProperty]] = n である新しいXMLListとする
  4. もし、n が属性名であるならば
    • xの属性をループで回して、aに入れる
      • nが名前 "*" である、または nの名前がaの名前と同じならば、listaを加える
    • listを返す
  5. k = 0 から x.[[Length]] - 1 までxのループで回す
    • nが "*" または、x[k]が要素でx[k]の名前がnの名前と同じならば、listに追加する
  6. listを返す

もっと端折ると

  1. もし、プロパティ名pが配列のプロパティのような数値なら、その要素を返す
  2. 新たなXMLListlistを作成して
  • pが属性名なら、適合する属性をlistに入れて返す
  • pが要素名なら、適合する要素をlistに入れて返す

ここで面白いことが分かる。既にstrong要素で囲っているけど、新たなXMLListを作っている箇所がある。

コードで書くと

o.p[0];  // 新たなXMLListは作らない
o.p;     // 新たなXMLListを作って返す
o.@name; // 新たなXMLListを作って返す

ということ。

undefined? なにそれ?

挙動をきちんと見てみると、テキトウなプロパティ名にアクセスしても空のXMLListが返ることに気付くだろう。
逆に添数値だと、通常の[[Get]]が呼ばれるので undefined が返る

o.*[100];   // undefined
o._;        // undefined じゃない!
typeof o._; // "xml"

となる。面白いね。

ついでに、今度は要素の追加について

E4XではXMLオブジェクトを普通のリテラル値に使うような演算子が使える
ここでは +演算子を使ってXMLオブジェクトの子要素を追加してみる。

var xml = <root>
 <p>hoge</p>
</root>;

xml += <p>foo</p>;
// <root>
//   <p>hoge</p>
// </root>
// <p>foo</p>

おおっと失敗。これでは子要素に追加とならない。

xml.* += <p>foo</p>;

とやる必要があった。これは結局のところ、

xml.* = xml.* + <p>foo</p>;

としているわけだが、これは最悪な方法である。

  1. 右辺値の計算
    • xmlの子要素を全て取る + <p>foo</p> を追加
  2. 代入
    • xmlの子要素全てを、1の右辺値に置き換える

なかなかに熱いね。2回もプロパティアクセスして全子要素を新たなXMLListにして返している。"*"によるアクセスは全ての子要素を取ってくるのでなかなかにファットな(いやfxxkな)挙動だ。できれば使わない方が良い。

うん、最悪。もう少し工夫をしてみよう。というか普通のメソッドを使用してみよう。

xml.appendChild(<p>foo</p>);

うん、普通だ。だが、しかーし。このappendChildにも罠が!

appendChildは以下の様なステップを踏む。

  1. childrenに引数 "*" をもってxmlに対して[[Get]]を呼び出し結果を入れる
  2. childrenの最後に<p>foo</p>を追加する
  3. xmlを返す

おお、ここでもxml.*が使われてしまっているではないか! 1回に減ったとはいえ...。

もっと工夫してみよう。

o._ += <p>foo</p>;

何ぞこれ?
これは、

o._ = o._ + <p>foo</p>

と同じだった。

  1. 右辺値
    • o._のプロパティ_は存在しないがundefinedではなく空のXMLListを返す
    • それに、<p>foo</p>を追加する
    • つまり、<p>foo</p>となる
  2. 代入
    • o._のプロパティ_は存在しないが空のXMLListである
    • そこに、1の結果を入れる

XMLオブジェクトの存在しないプロパティへの代入については、説明は省略するが、内部関数[[Append]]が呼ばれてxmlの子要素の最後に追加される動きとなる。これなら全子要素を取ってくるような動きもない。実際にこれらのパフォーマンスを比較してみると、これが一番速いはず。しかし...存在しないプロパティに代入とか、なんというバッドノウハウ感...orz

脱線したけど、代入だからと言って油断は禁物という話でした。


同値演算子(===)と等値演算子(==)

さて、JavaScriptにおいて、ご存知、比較演算子は2つある。

===
同値演算子
==
等値演算子

この演算子たちの挙動を説明するよ。

同値演算子(===)

同値演算子は、オブジェクトに対して実行すると、同じリファレンス(?)であるかで判断されるよね?
===の同値演算子E4XXMLオブジェクトにやってみよう。

var o = <o name="XML element">
  <p>FOO</p>
</o>;

o === o;             // 当然ながら true
o.p[0] === o.p[0];   // 当然、 true
o.p === o.p;         // 当然...じゃなかった! false です、お兄さん!
o.@name === o.@name; // こちらも false !

となる。ビックリだね。

理由は「E4X内部関数[[Get]]」の項で説明になるはず。

等値演算子(==)

次、等値演算子

通常のオブジェクトでは、オブジェクト同士の等値演算は同値演算と同じく、リファレンス(?)であるかで判断されるよね? ということは、E4XXMLオブジェクトでも結果は同値演算と同じになるはず。

と思いきや、そうはならない。(同じだったら、こんなエントリは書かないので展開は読めていると思うけどw)

var o = <o name="XML element">
  <p>FOO</p>
</o>;

o == o;              // 当然ながら true
o.p[0] == o.p[0];    // 当然、 true
o.p == o.p;          // false ...ではなく、今度は true かよ!
o.@name == o.@name;  // こっちも true

み〜んな true。なんでやねんっ!

はい、実は、E4Xの等値演算で使われる内部関数[[Equals]]は書き換えられている。

細かいステップは...書くのが疲れたからいいや、簡単に。

  1. 双方の要素名や属性、名前空間、を見て同じ値でないなら、falseを返す
  2. 1を子要素に対して再帰的に行う

ということで、

o.p == <p>FOO</p>; // true

となる。

そんなわけで、再帰的に比較していくので等値演算は大きなXMLオブジェクトに対して行うと大変なコストが掛かる。


まとめ、の様なもの

E4Xのプロパティアクセスから比較演算子を解説してみたわけだが、結論。

  • プロパティアクセスは深くなるほど大変コストがかかる
    • 子要素の追加にもご注意あれ
    • 添数値でなら遅くはないだろうが、そんなことやってたらE4Xは使い難い代物になってしまう
  • 同値演算子(===)は罠があるので、使える代物じゃない
  • 使える等値演算子(==)は再帰的に見ていくので大きなXMLオブジェクトでは大変コストがかかる。要注意

パフォーマンスを重視するならE4Xは捨てましょう。