Array に Hash を被せる(AOH)
配列を返す関数は、素の配列を返すよりも、first や last などの付加情報も一緒に返すといいんじゃないかな。 というお話です。
たとえば FakeArray → Array
NodeList や arguments は FakeArray(Array Like Object) と呼ばれる擬似配列です。
FakeArray には join() や push() などの便利なメソッドがないため、FakeArray を一旦 Array に変換する(↓)toArray のような関数が、どうしても必要になります。
# Array.prototype.slice.apply(fakeArray) を使うケースは説明しないよ
素の Arrayを返す toArray 関数
function toArray(fakeArray) { // @param Array/FakeArray: // @return Array: var rv = [], i = 0, iz = fakeArray.length; for (; i < iz; ++i) { rv[i] = fakeArray[i]; } return rv; } function getLastArgument(/* var_args */) { var ary = toArray(arguments); return ary[ary.length - 1]; }
それに対し
Array + { first, last } を返す toArray 関数
では、
function toArray(fakeArray) { // @param Array/FakeArray: // @return Array+Hash: Array + { first, last } var rv = [], i = 0, iz = fakeArray.length; for (; i < iz; ++i) { rv[i] = fakeArray[i]; } rv.first = rv[0]; rv.last = rv[rv.length - 1]; return rv; } function getLastArgument(/* var_args */) { return toArray(arguments).last; }
え? last はあってもいいけど、first は別に要らない? まぁ toArray 関数だけ見ると、そう見えるかもね。
では次に、
NodeList(Siblings) → Array
Nodeと同じ階層のELEMENT_NODE(Siblings)を列挙する関数 enumSibling() を考えてみましょう。
兄弟を列挙し NodeArray を返す enumSibling 関数
普通に考えると、
function enumSibling(node) { // @param Node: needle // @return NodeArray: [node, ...] var rv = [], n = node.parentNode.firstChild; for (; n; n = n.nextSibling) { if (n.nodeType === 1) { // 1: ELEMENT_NODE rv.push(n); } } return rv; }
こうなりますが、
兄弟を列挙し NodeArray + { first, prev, next, last, index } を返す enumSibling 関数
こうするとどうでしょう。
prev には node の一つ上の兄が、next には node の一つ下の弟が、index には node の位置が入ります。
function enumSibling(node) { // @param Node: needle // @return NodeArray+Hash: [node, ...] + { first, prev, next, last, index } // first - Node/null: firstElementSibling // prev - Node/null: previousElementSibling // next - Node/null: nextElementSibling // last - Node/null: lastElementSibling // index - Number: position index or -1 (node is TEXT_NODE) var rv = [], i = 0, first = null, last = null, index = -1, n = node.parentNode.firstChild; for (; n; n = n.nextSibling) { if (n.nodeType === 1) { // 1: ELEMENT_NODE rv.push(last = n); first || (first = n); n === node && (index = i); // found index ++i; } } rv.first = first; rv.prev = rv[index - 1] || null; rv.next = rv[index + 1] || null; rv.last = last; rv.index = index; return rv; }
配列に first や last などのプロパティを追加しているため、この関数のコストは微増します(ほぼ誤差の範囲で)、しかし、node の前後の要素や兄弟の中における位置も同時に取得することができるため、enumSibling() を必要とするようなロジック(DOM Traversal系)がもっとシンプルに書け、トータルで見るといい感じに速くなります。
たとえば、これが
// <ul> // <li id="a"> // <a /> ← node いまここ // </li> // <li id="b"> // <a /> ← これが欲しい // </li> // </ul> Array.prototype.indexOf || Array.prototype.indexOf = function(search, fromIndex) { // 代替実装 var iz = this.length, i = fromIndex || 0; i = (i < 0) ? i + iz : i; for (; i < iz; ++i) { if (i in this && this[i] === search) { return i; } } return -1; }; var parent = node.parentNode; // <li id="a"><text><a /></text></li> var liNodeArray = enumSibling(parent); // [<li id="a">, <li id="b">] // IE には Array.indexOf が実装されていないため、 // Array.indexOf を for() で代替する → 遅い var index = liNodeArray.indexOf(parent); // 0 var nextParent = liNodeArray[index + 1]; // <li id="b"><text><a /></text></li> // nextParentの最初の子要素をenumSibling()で取得する // 暗黙のTEXT_NODEが挟まるブラウザや、 // firstElementChildが使えないブラウザもあるので var result = enumSibling(nextParent.firstChild)[0];
enumSibling() が NodeArray + Hash を返すなら、もっとも簡単に書ける
このケースでは、Array.indexOf が不要になります。
// <ul> // <li id="a"> // <a /> ← node いまここ // </li> // <li id="b"> // <a /> ← これが欲しい // </li> // </ul> var parent = node.parentNode; // <li id="a"><text><a /></text></li> var firstChild = enumSibling(parent).next.firstChild; // <text><a /></text> var result = enumSibling(firstChild).first; // <a />
なんなら、ワンラインでも
var result = enumSibling(enumSibling(node.parentNode).next.firstChild).first;
杞憂はご無用
Array に first や last などの Hash 的なプロパティをかぶせると、for (i in Array) で first や last が出てきてしまいますが、Array に対して for in でループしちゃうのは、初心者 or わかってやってる上級者と相場は決まってるのでそんな心配はご無用かと。
# for (i in Array) 使ったら お仕置きだべぇ〜
Hash を被せると便利
この Array に Hash を被せることを、個人的には AOH と呼んでます(名前が無いと説明が大変なので)。
uupaa.js では toArray() に slice 機能を追加した版が uu.array() として、 enumSibling() は uu.node.array() として実装されています。
おまけ
暗黙の TEXT_NODE や COMMENT_NODE 等が DOM ツリー上に散在していると、IE のレンダラがアホなバグを発動させたり、DOM Traversal が遅くなるなどの余計なコストがかかります。
そこで uupaa.js では、そのような無駄を省くため、それらを一掃する uu.node.normalize() を提供しています。
uu.node.normalize() を DOMContentLoaded や window.onload の後に一度呼び出すだけで、先程のコードはさらに短くなります。
uu.ready("dom:1", function(uu) { // DOMContentLoaded Mid priority // DOMContentLoaded 後の処理 uu.node.normalize(); // 暗黙のテキストノードやコメントノードを一括除去 }, function(uu) { // ここも window.onload 後の処理 var node = uu.query("ul>li>a")[0]; // <ul> // <li id="a"> // <a /> ← node いまここ // </li> // <li id="b"> // <a /> ← これが欲しい // </li> // </ul> // 暗黙の TEXT_NODE が挟まっているかもしれないことを前提にしたコード // var result = enumSibling(enumSibling(node.parentNode).next.firstChild).first; // 暗黙の TEXT_NODE を除去しているため、firstChild でそのままアクセス可能 var result = uu.node.array(node.parentNode).next.firstChild; });
uu.node.normalize() は、様々なメリットをもたらします。
ちょっとした裏技って奴ですね。