prototype.jsを読み解く

第8回 Prototypeライブラリ(2277~2620行目)

この記事を読むのに必要な時間:およそ 15 分

2371:     // TOKEN FUNCTIONS
2372:     tagName: function(nodes, root, tagName, combinator) {
2373:       tagName = tagName.toUpperCase();
2374:       var results = [], h = Selector.handlers;
2375:       if (nodes) {
2376:         if (combinator) {
2377:           // fastlane for ordinary descendant combinators
2378:           if (combinator == "descendant") {
2379:             for (var i = 0, node; node = nodes[i]; i++)
2380:               h.concat(results, node.getElementsByTagName(tagName));
2381:             return results;
2382:           } else nodes = this[combinator](nodes);
2383:           if (tagName == "*") return nodes;
2384:         }
2385:         for (var i = 0, node; node = nodes[i]; i++)
2386:           if (node.tagName.toUpperCase() == tagName) results.push(node);
2387:         return results;
2388:       } else return root.getElementsByTagName(tagName);
2389:     },
2390: 

2371行目からはタグ,ID,クラス名などを処理するハンドラです。

まずはtagName()関数です。Selector.criteria.tagNameでmatcher関数を作る際に,CSSフィルタのパース中にtagNameを見つけると呼び出されます。これらの関数では,これまでに絞り込まれたノードの配列がnodesに,元々の検索対象のルートノードがrootに,tagName()の場合はCSSフィルタで見つかったタグ指定(もしくは"*")がtagNameに,直前に結合子(combinator)指定があればcombinatorに渡されて呼び出されます。返す値はCSSフィルタにマッチし得るノードの配列,となります。

実装では,nodesにまだ何も含まれていなければroot.getElementsByTagName()でroot以下のタグ名でマッチさせその配列を返します。

そうでなければ,combinatorが指定されていればそれに合わせてnodesを絞り込みます。"descendant"以外のcombinatorの場合はSelector.handlers以下の関数でフィルタ処理を行います。

tagNameが'*'なら,残っているnodesすべてをそのまま返し,そうでなければtagNameにマッチするものだけを集めて返します。

2391:     id: function(nodes, root, id, combinator) {
2392:       var targetNode = $(id), h = Selector.handlers;
2393:       if (!nodes && root == document) return targetNode ? [targetNode] : [];
2394:       if (nodes) {
2395:         if (combinator) {
2396:           if (combinator == 'child') {
2397:             for (var i = 0, node; node = nodes[i]; i++)
2398:               if (targetNode.parentNode == node) return [targetNode];
2399:           } else if (combinator == 'descendant') {
2400:             for (var i = 0, node; node = nodes[i]; i++)
2401:               if (Element.descendantOf(targetNode, node)) return [targetNode];
2402:           } else if (combinator == 'adjacent') {
2403:             for (var i = 0, node; node = nodes[i]; i++)
2404:               if (Selector.handlers.previousElementSibling(targetNode) == node)
2405:                 return [targetNode];
2406:           } else nodes = h[combinator](nodes);
2407:         }
2408:         for (var i = 0, node; node = nodes[i]; i++)
2409:           if (node == targetNode) return [targetNode];
2410:         return [];
2411:       }
2412:       return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
2413:     },
2414: 

2391行目からはid()ハンドラです。

まだnodesが空で,rootがDOMのトップであるdocumentなら,単純に$(id)したもの(もしくは空配列)を返します。

nodesが空で,rootがdocument以外の場合,$(id)が見つかって,それが検索開始要素のrootの内側にあるようならそれを配列で返します(2412行目の [targetNode] 部分⁠⁠。$(id)が見つからないか,見つかってもrootの外側だった場合には空配列を返します(同 [] 部分⁠⁠。

nodesがすでにある場合(2395行目以降⁠⁠,まずcombinatorが渡されているようならそれに従いnodesを絞り込みます。

combinatorが'child'の場合,nodes配列を調べて$(id).parentNodeと一致するものが見つかれば$(id)を配列で返します。

combinatorが'descendant'の場合は,nodes配列を調べて$(id)がその子孫ならば$(id)を配列で返します。

combinatorが'adjacent'なら,nodes配列の各々に対して,$(id)の直前の兄弟と一致するかどうかを調べ,そうなら$(id)を配列で返します。

combinatorがそれ以外の場合(といっても残りは'laterSibling'だけですが⁠⁠,Selector.handlers以下の関数(すなわち2352行目のSelector.handlers.laterSibling())を使っています。

2408行目には (1) combinatorが指定されていないか,(2) combinatorが'laterSibling'であるか,(3) それ以外のcombinatorで該当要素が無かった場合にのみ到達します。

(1)の場合は"div#id"のような指定なので,現在残っているnodesから該当IDを持つ要素を探して見つかれば返します。

(2)の場合は"div ~ #id"という形で,絞り込まれた結果が2406行目でnodesに入っているのでこの中から該当IDを探します。

(3)はcombinator 指定がある場合なので,直前の要素配列nodesから該当IDを探すのは間違っているように見えます。(3)の場合は空配列を返すべきでしょう。

CSSセレクタのID指定では,対象となる要素が見つかるか見つからないか,しか無いので,この関数の返り値は空配列か,該当要素が一つ入った配列か,のどちらかしかありません。

2415:     className: function(nodes, root, className, combinator) {
2416:       if (nodes && combinator) nodes = this[combinator](nodes);
2417:       return Selector.handlers.byClassName(nodes, root, className);
2418:     },
2419: 

2415行目からはCSSセレクタのクラス名指定です。

combinatorが指定されている場合はSelector.handlers.descendant()などを使って絞込み,その後byClassName()関数に渡してその結果を返します。

2420:     byClassName: function(nodes, root, className) {
2421:       if (!nodes) nodes = Selector.handlers.descendant([root]);
2422:       var needle = ' ' + className + ' ';
2423:       for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
2424:         nodeClassName = node.className;
2425:         if (nodeClassName.length == 0) continue;
2426:         if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
2427:           results.push(node);
2428:       }
2429:       return results;
2430:     },
2431: 

className()から呼び出されるbyClassName()です。

まずnodesがセットされていなければdescendant()を使って引数で渡されたroot要素以下の要素すべてをnodesに集めます。

その後,各ノードに対して,指定されたクラス名が含まれているかを確認しますが,その際の確認方法として,Element.ClassNamesは使わずに,文字列の包含関係だけで行っています。単純に含まれているかどうかをチェックすると,"A"というクラスを確認しようとすると"AB"もマッチしてしまうため,検索する際は" A "という風に前後にスペースをつけます。そうするとclassNameプロパティ文字列の先頭・末尾にスペースが無い場合があるので,こちらにも前後にスペースを付け" A AB C "のような形にします。この状態で " A AB C ".include(" A ") という式でクラス名含まれているかどうかを確認しています。

2432:     attrPresence: function(nodes, root, attr) {
2433:       var results = [];
2434:       for (var i = 0, node; node = nodes[i]; i++)
2435:         if (Element.hasAttribute(node, attr)) results.push(node);
2436:       return results;
2437:     },
2438: 

2432行目からはattrPresence()ハンドラです。CSSセレクタでは "[attr]" という表記になります。

実装は,各ノードに対して,渡された属性名が存在するかをElement.hasAttribute()で調べ,存在するノードだけを配列で返します。

2439:     attr: function(nodes, root, attr, value, operator) {
2440:       if (!nodes) nodes = root.getElementsByTagName("*");
2441:       var handler = Selector.operators[operator], results = [];
2442:       for (var i = 0, node; node = nodes[i]; i++) {
2443:         var nodeValue = Element.readAttribute(node, attr);
2444:         if (nodeValue === null) continue;
2445:         if (handler(nodeValue, value)) results.push(node);
2446:       }
2447:       return results;
2448:     },
2449: 

2439行目からはattr()ハンドラです。2274行目の正規表現が複雑ですが,[attr="value"] や [attr!="value"] などの文字列にマッチして,属性名,属性値,間の演算子がハンドラに渡されます。

まず,nodesが空の場合はrootから下の要素すべてをnodesに格納しておきます。

渡された演算子によって,処理を変えるために2581行目からのSelector.operatorsから関数オブジェクトを取得します。

あとはすべてのノードに対して,渡された属性が存在すれば値を関数オブジェクトに渡します。これらのhandler関数からはマッチするかどうかが真偽値で返されるので,それに応じて返り値用のresultsに溜めます。最後にresultsを返して終了です。

著者プロフィール

栗山淳(くりやまじゅん)

S2ファクトリー株式会社株式会社イメージソース所属。
本業はWeb制作会社の裏方。得意分野はFreeBSDやPerlのはずだが,必要に迫られるとHTML/CSSやJavaScriptも書く。