prototype.jsを読み解く

第6回 Prototypeライブラリ(1609~2051行目)

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

1825: if (!Prototype.BrowserFeatures.ElementExtensions &&
1826:  document.createElement('div').__proto__) {
1827:   window.HTMLElement = {};
1828:   window.HTMLElement.prototype = document.createElement('div').__proto__;
1829:   Prototype.BrowserFeatures.ElementExtensions = true;
1830: }
1831: 

1825行目からは,window.HTMLElementプロパティが存在しているかどうかを見て,無いようなら作成しています。

div要素の __proto__ としてプロトタイプオブジェクトが存在するようなら,そこに様々なHTMLElementインターフェイスが実装されているはずなので,それをwindow.HTMLElementとして流用します。

1832: Element.hasAttribute = function(element, attribute) {
1833:   if (element.hasAttribute) return element.hasAttribute(attribute);
1834:   return Element.Methods.Simulated.hasAttribute(element, attribute);
1835: };
1836: 

クロスブラウザで使えるhasAttribute()関数です。

Element化した要素は,メソッドとしてhasAttribute()を持っていますが,それ以外の要素にも使えるようにグローバルにElement.hasAttribute()を用意しています。

1837: Element.addMethods = function(methods) {
1838:   var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
1839: 
1840:   if (!methods) {
1841:     Object.extend(Form, Form.Methods);
1842:     Object.extend(Form.Element, Form.Element.Methods);
1843:     Object.extend(Element.Methods.ByTag, {
1844:       "FORM":     Object.clone(Form.Methods),
1845:       "INPUT":    Object.clone(Form.Element.Methods),
1846:       "SELECT":   Object.clone(Form.Element.Methods),
1847:       "TEXTAREA": Object.clone(Form.Element.Methods)
1848:     });
1849:   }
1850: 
1851:   if (arguments.length == 2) {
1852:     var tagName = methods;
1853:     methods = arguments[1];
1854:   }
1855: 
1856:   if (!tagName) Object.extend(Element.Methods, methods || {});
1857:   else {
1858:     if (tagName.constructor == Array) tagName.each(extend);
1859:     else extend(tagName);
1860:   }
1861: 
1862:   function extend(tagName) {
1863:     tagName = tagName.toUpperCase();
1864:     if (!Element.Methods.ByTag[tagName])
1865:       Element.Methods.ByTag[tagName] = {};
1866:     Object.extend(Element.Methods.ByTag[tagName], methods);
1867:   }
1868: 
1869:   function copy(methods, destination, onlyIfAbsent) {
1870:     onlyIfAbsent = onlyIfAbsent || false;
1871:     var cache = Element.extend.cache;
1872:     for (var property in methods) {
1873:       var value = methods[property];
1874:       if (!onlyIfAbsent || !(property in destination))
1875:         destination[property] = cache.findOrStore(value);
1876:     }
1877:   }
1878: 
1879:   function findDOMClass(tagName) {
1880:     var klass;
1881:     var trans = {
1882:       "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
1883:       "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
1884:       "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
1885:       "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
1886:       "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
1887:       "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
1888:       "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
1889:       "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
1890:       "FrameSet", "IFRAME": "IFrame"
1891:     };
1892:     if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
1893:     if (window[klass]) return window[klass];
1894:     klass = 'HTML' + tagName + 'Element';
1895:     if (window[klass]) return window[klass];
1896:     klass = 'HTML' + tagName.capitalize() + 'Element';
1897:     if (window[klass]) return window[klass];
1898: 
1899:     window[klass] = {};
1900:     window[klass].prototype = document.createElement(tagName).__proto__;
1901:     return window[klass];
1902:   }
1903: 
1904:   if (F.ElementExtensions) {
1905:     copy(Element.Methods, HTMLElement.prototype);
1906:     copy(Element.Methods.Simulated, HTMLElement.prototype, true);
1907:   }
1908: 
1909:   if (F.SpecificElementExtensions) {
1910:     for (var tag in Element.Methods.ByTag) {
1911:       var klass = findDOMClass(tag);
1912:       if (typeof klass == "undefined") continue;
1913:       copy(T[tag], klass.prototype);
1914:     }
1915:   }
1916: 
1917:   Object.extend(Element, Element.Methods);
1918:   delete Element.ByTag;
1919: };
1920: 

ちょっと大きめなElement.addMethods()関数です。

$()でElement化される要素に自動的に追加されるメソッドに,自分で作ったものを加えることができます。また,このElement.addMethods()はprototype.js読み込みの最後で引数なしで呼び出され,デフォルトの追加メソッドのセットアップも行います。

まずは関数内で短く記述するために,Prototype.BrowserFeaturesオブジェクトとElement.Methods.ByTagオブジェクトをそれぞれF,Tという変数に代入しておきます。

次に,引数がなにも指定されていなかった場合は,Form, Form.Elementに対してそれぞれ指定されたメソッドを追加します。さらに,Element.Methods.ByTagに<form>, <input>, <select>, <textarea>に対する追加メソッドも加えておきます。

addMethods()は,引数ひとつの場合はすべてのタグを対象とし,引数が二つの場合は特定のタグだけを対象とします。1851行目からにおいて,引数が二つあったときに指定されたタグ名を保存しています。

1856行目で,タグ名が指定されていなければ,Object.extend()を使って,拡張メソッドが格納されているElement.Methodsオブジェクトに指定されたmethodsをマージします。

もしタグ名が指定されているなら,この関数内ローカルの関数extend()を呼び出します。タグ名が配列の場合はそれぞれに対してextend()を呼び出します。

1862行目からは,関数内の関数定義としてextend()が定義されています。タグ名を大文字に揃え,Element.Methods.ByTagがタグ名をキーとするハッシュということになっているので存在していなければ空で初期化し,Object.extend()を使ってタグ名とメソッド定義を含むハッシュ,のペアとして格納しておきます。この時methods変数は,外側のaddMethods()関数内のローカル変数ですが,関数内で定義された関数の中からでも外側の変数を参照できるので,特に引数として渡さなくてもエラーとはなりません。

1869行目からも関数内の関数copy()です。これは1905行目以降で使います。

Element.extend()でも使ったElement.extend.cacheを使い,メモリを無駄に使わないようにしつつ,methodsをdestinationにコピーします。onlyIfAbsentフラグですでにメソッドがあった時に上書きするかしないかを指定しています。

次も関数内関数でfindDOMClass()です。

Firefox,WebKitではwindow.HTMLDivElementなどにHTML要素のコンストラクタとなる関数が入っています。IEではこれらが存在しないので,その場合は(完全に等価なものではありませんが)作成してそれを返します。

DOMで定義されているHTML Elementインターフェイスには,要素名と一致しない名前が多々存在するので,その対応表となるハッシュをtransとして用意しています。

その対応表に存在するタグで,window直下にその名前のプロパティが存在すればそれを返します。次は大文字となっているタグ名を使って"HTMLDIVElement"などの文字列を作り,それがwindowのプロパティに存在するかどうかを確かめます。

それでもダメならタグ名の部分の先頭だけ大文字にした形"HTMLDivElement"等で試します。

それでも見つからなければ,window直下にその名前のオブジェクトを作ってしまい,要素を一旦作成してそのプロトタイプを拾って代入しておきます。一般にHTMLElementなどはコンストラクタとなるべき関数オブジェクトが適切かと思われるので,ここは {} という空オブジェクトではなく function(){} のような空関数オブジェクトの方が良さそうです。

ということで,このfindDOMClass()は,タグ名を渡されると,⁠適切なprototypeプロパティを持つ)そのコンストラクタを返す,という関数になります。

1904行目からまたaddMethods()のメインの処理に戻ります。実行中のブラウザにwindow.HTMLElementプロパティが存在していたなら,そのHTMLElementのprototypeに,Element.Methodsをコピーしておきます。Element.Methods.Simulatedの方は,HTMLElementに関数が存在しなければ追加しています。

1909行目からは,SpecificElementExtensionsが真の時に実行されます。このフラグは <div>, <form> のプロトタイプが異なるときに真になります。ということは,HTMLDivElement, HTMLFormElementでそれぞれ異なるプロトタイプオブジェクトを持つ,ということなので,様々な要素型ごとに独自のメソッド群を実装している,と推測できます。

Element.Methods.ByTagで指定されたタグそれぞれについて,先ほど定義したfindDOMClass()でDOM HTMLインターフェイスを取得し,もし存在すればそのprototypeにElement.Methods.ByTagで指定された関数オブジェクトをコピーします。

後は,再度ElementをElement.Methodsで拡張しなおして終了です。1823行目で既にElementに追加していますが,この関数内でElement.Methodsが変更されている場合があるので再度拡張しています。

また,1910行目の Element.Methods.ByTagは,せっかくTに代入してあるのでそれを使う方が短く記述できます。

最後の delete Element.ByTagは,今のところ参照されていない変数のようですが,なぜかdeleteしています。Ticket #7888で指摘された問題に対して,Changeset 6561で変更された部分ですが,これらを見てもやはり理由はわかりません。

1921: var Toggle = { display: Element.toggle };
1922: 
1923: /*--------------------------------------------------------------------------*/
1924: 

Toggleオブジェクトを定義していますが,今のところ公式ドキュメントにも載っていませんし,他から参照されているわけでもないようです。

著者プロフィール

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

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