E4Xのプロパティアクセスと比較演算子
今度はみんな僕が大好きなE4Xの不思議に迫るよ!
実はこれ、Firefox Hacks Rebootedにも少し書いた内容だけど良いよね。
最初に書いておくと、「E4Xすばらしい! これからどんどん使っていこう!」という内容ではない。残念ながら。むしろ、捨てましょう、という内容になってしまっている。StrictモードではBANされたし、もう良いよね...(かなり自虐的)
E4Xでない普通のオブジェクトの内部関数[[Get]]
E4Xに入る前に普通の[[Get]]から。
var o = { p: "FOO" }
o.p
とすると、当然"FOO"が返ってくるわけだけど、oオブジェクトのプロパティpを得るのに内部的に[[Get]]という関数を呼び出している。
ECMA-262の仕様では以下の様なステップを踏む。
オブジェクトoの内部関数[[Get]]がプロパティ名pを伴って呼び出されたとき、以下のステップが取られる
- プロパティ名pで内部関数[[GetProperty]]を呼び、結果をdescとする
- もし、descがundefinedなら、undefinedを返す
- もし、descがDataDescriptorなら、desc.[[Value]]を返す
- そうでない場合、descはAccessDescriptorでなければならず、desc.[[Get]]をgetterに入れる
- もし、getterがundefinedなら、undefinedを返す
- オブジェクトo経由でgetterを呼び([[Call]])、結果を返す
E4X内部関数[[Get]]
さて、E4Xでは、通常の[[Get]]を書き換えている。
var o = <o name="XML element"> <p>FOO</p> </o>;
どのようなプロセスになるかというと
XMLオブジェクトoの内部関数[[Get]]がプロパティ名pを伴って呼びだれたとき、以下のステップが取られる(ちょい端折って書くよ)
- もし、ToString(ToUnit32(p)) == p ならば、
- a. list = ToXMLList(x) とする
- b. 引数pとともにlistの[[Get]]を呼び出した結果を返す
- n = ToXMLName(p) とする
- list を、list.[[TargetObject]] = x および、list.[[TargetProperty]] = n である新しいXMLListとする
- もし、n が属性名であるならば
- xの属性をループで回して、aに入れる
- nが名前 "*" である、または nの名前がaの名前と同じならば、listにaを加える
- listを返す
- k = 0 から x.[[Length]] - 1 までxのループで回す
- nが "*" または、x[k]が要素でx[k]の名前がnの名前と同じならば、listに追加する
- listを返す
もっと端折ると
- もし、プロパティ名pが配列のプロパティのような数値なら、その要素を返す
- 新たなXMLList、listを作成して
- 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>;
としているわけだが、これは最悪な方法である。
なかなかに熱いね。2回もプロパティアクセスして全子要素を新たなXMLListにして返している。"*"によるアクセスは全ての子要素を取ってくるのでなかなかにファットな(いやfxxkな)挙動だ。できれば使わない方が良い。
うん、最悪。もう少し工夫をしてみよう。というか普通のメソッドを使用してみよう。
xml.appendChild(<p>foo</p>);
うん、普通だ。だが、しかーし。このappendChildにも罠が!
appendChildは以下の様なステップを踏む。
おお、ここでもxml.*
が使われてしまっているではないか! 1回に減ったとはいえ...。
もっと工夫してみよう。
o._ += <p>foo</p>;
何ぞこれ?
これは、
o._ = o._ + <p>foo</p>
と同じだった。
- 右辺値
o._
のプロパティ_は存在しないがundefinedではなく空のXMLListを返す- それに、
<p>foo</p>
を追加する - つまり、
<p>foo</p>
となる
- 代入
o._
のプロパティ_は存在しないが空のXMLListである- そこに、1の結果を入れる
XMLオブジェクトの存在しないプロパティへの代入については、説明は省略するが、内部関数[[Append]]が呼ばれてxmlの子要素の最後に追加される動きとなる。これなら全子要素を取ってくるような動きもない。実際にこれらのパフォーマンスを比較してみると、これが一番速いはず。しかし...存在しないプロパティに代入とか、なんというバッドノウハウ感...orz
脱線したけど、代入だからと言って油断は禁物という話でした。
同値演算子(===)と等値演算子(==)
さて、JavaScriptにおいて、ご存知、比較演算子は2つある。
この演算子たちの挙動を説明するよ。
同値演算子(===)
同値演算子は、オブジェクトに対して実行すると、同じリファレンス(?)であるかで判断されるよね?
===の同値演算子をE4XのXMLオブジェクトにやってみよう。
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]]」の項で説明になるはず。
等値演算子(==)
次、等値演算子。
通常のオブジェクトでは、オブジェクト同士の等値演算は同値演算と同じく、リファレンス(?)であるかで判断されるよね? ということは、E4XのXMLオブジェクトでも結果は同値演算と同じになるはず。
と思いきや、そうはならない。(同じだったら、こんなエントリは書かないので展開は読めていると思うけど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]]は書き換えられている。
細かいステップは...書くのが疲れたからいいや、簡単に。
ということで、
o.p == <p>FOO</p>; // true
となる。
そんなわけで、再帰的に比較していくので等値演算は大きなXMLオブジェクトに対して行うと大変なコストが掛かる。