prototype.jsを読み解く

第5回 Prototypeライブラリ(1290~1608行目)

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

遠いリポジトリに便利なsvk

第五回目です。

Subversionのlogやdiff,annotateサブコマンドを駆使するといろいろな情報が取れることは前回紹介しました。

ただ,Railsのような海外にある,また人気があって負荷が高いことも多いリポジトリに対して,頻繁にそれらのサブコマンドを使っても,結果が出てくるまで遅いことが多々あり快適に解析が進められません。

そんな時にはsvk を使う手があります。

svkのmirrorサブコマンドを使うと,ローカルのディスク上に,Subversionリポジトリの特定部分以下だけをまるごとミラーすることができます。今回のような状況では,http://svn.rubyonrails.org/rails/spinoffs/prototype/だけでもローカルにミラーしておくと,作業が格段に快適になります。

% svk mirror http://svn.rubyonrails.org/rails/spinoffs/prototype //prototype
Committed revision 3764.
% svk sync //prototype
Retrieving log information from 1 to 7693
Committed revision 3765 from revision 3362.
... (ここで途中で失敗する場合は再度 svk sync)
% svk checkout //prototype prototype
...
% cd prototype/tags/rel_1-5-1-1/src
% svk log dom.js | less

という手順で利用できます。

では,今回はElementへの拡張からです。Elementまわりは膨大なので,次回も含めて2分割でお送りします。

Element への拡張

1290: if (!window.Element) var Element = {};
1291: 

まず,必要に応じて空のElementオブジェクトをプレースホルダとして用意します。

1292: Element.extend = function(element) {
1293:   var F = Prototype.BrowserFeatures;
1294:   if (!element || !element.tagName || element.nodeType == 3 ||
1295:    element._extended || F.SpecificElementExtensions || element == window)
1296:     return element;
1297: 
1298:   var methods = {}, tagName = element.tagName, cache = Element.extend.cache,
1299:    T = Element.Methods.ByTag;
1300: 
1301:   // extend methods for all tags (Safari doesn't need this)
1302:   if (!F.ElementExtensions) {
1303:     Object.extend(methods, Element.Methods),
1304:     Object.extend(methods, Element.Methods.Simulated);
1305:   }
1306: 
1307:   // extend methods for specific tags
1308:   if (T[tagName]) Object.extend(methods, T[tagName]);
1309: 
1310:   for (var property in methods) {
1311:     var value = methods[property];
1312:     if (typeof value == 'function' && !(property in element))
1313:       element[property] = cache.findOrStore(value);
1314:   }
1315: 
1316:   element._extended = Prototype.emptyFunction;
1317:   return element;
1318: };
1319: 

まずはElement.extend()関数です。これは既存の要素に,Prototypeライブラリ独自のメソッド群を付け加えるものです。

昔はElement以下の追加メソッドは,Element.visible(targetElement)のように呼び出すようになっていましたが,現在では各要素自体のメソッドとしてtargetElement.visible()のように呼び出すことができるようになっています。

できるだけ効率的に,かつ副作用を抑えつつ便利にするために,このElementに対するメソッド追加はかなりややこしいことになっています。

1294行目からは,メソッドを付け加える必要があるかどうかを判断しています。以下の条件のどれかに該当するとreturn elementで何もせずに返ります。

条件文 付け加える必要がない理由
!element そもそも引数elementが無効,null,undefinedなど
!element.tagName tagNameプロパティが無いので要素ではない
element.nodeType == 3 テキストノード(3)は対象外
element._extended すでにこの関数により拡張済み
F.SpecificElementExtensions コード最後部のElement.addMethods()の呼び出しにより,HTMLElement.prototype以下にメソッドを追加してあるので,個々の要素インスタンスに対して追加する必要は無し
element == window windowは対象外。それ以前にtagNameのチェックで除外されているはず

1298行目では,ここで使う変数や,ショートカット関数などを定義しています。

1301行目からは,このブラウザで拡張するべき関数オブジェクトをmethodsオブジェクトに詰め込んでいます。ここでmethodsのプロパティ名が要素に拡張されるメソッド名で,プロパティ値が関数オブジェクトになります。

グローバルなHTMLElementが定義されている場合,F.ElementExtensionsが真となります。この値はIEでは偽ですがFirefox,Safari,Operaでは真になります。真の場合はHTMLElementに定義されているメソッドが使えるのでここで拡張する必要がなくなります。

Element.Methods.ByTagには,タグ毎に拡張するべき関数が入っているので,今回渡された要素のタグ名に対して該当するものがあればそれもmethodsオブジェクトに加えます。

あとは,methodsに入っている値を列挙して,プロパティ値がちゃんと関数オブジェクトで,かつまだelementには該当する名前のプロパティがセットされていない場合に,Element.extend.cache.findOrStore()関数を使って拡張しています。

最後に,element._extendedに「拡張済み」の印を入れて,elementを返します。

1320: Element.extend.cache = {
1321:   findOrStore: function(value) {
1322:     return this[value] = this[value] || function() {
1323:       return value.apply(null, [this].concat($A(arguments)));
1324:     }
1325:   }
1326: };
1327: 

1320行目からはElement.extend.cacheです。findOrStore()関数だけがプロパティとして定義されています。

findOrStore()関数は,先ほどのElement.extend()で,関数オブジェクトを渡して呼び出されています。1313行目において,cache.findOrStore()の形で呼び出されているので,この関数におけるthis はcacheすなわちElement.extend.cacheとなります。

return this[value] = this[value] || function(){...} という形は,Element.extend.cache オブジェクトのプロパティ名(といっても文字列ではなく関数オブジェクトがキーになっていますが)にあればそれを返し再度代入,無ければ関数を定義して代入,そしてその代入された値を返す,というものです。これにより,同じ無名関数がいくつも定義されないようにしています。

無名関数の中では,valueとして渡された関数オブジェクトに対してapply()を呼び出しています。例えば1329行目からのElement.Methods.visibleがvalueとして渡されているとすると,apply()の第一引数がnullなので,visible()の中でのthisはグローバルオブジェクトとなります。第二引数に[this].concat($A(arguments))を渡していますが,ここのthisは無名関数のコンテキストでのthisなので,通常はElementインスタンスになります。例えばIDが't1'という要素があれば,$('t1').visible()として呼び出すのでvisible の左側すなわち $('t1') が this となります。 arguments もこの無名関数のコンテキストなので,$('t1').visible(1,2,3) と書けば arguments に [1,2,3] が入ります。これらを concat() で繋いで apply() に渡しているので,visible() 関数の定義では,最初の引数として Element インスタンスを受け取る形になります。

  • ECMA-262 第三版 - 15.3.4.3 Function.prototype.apply (thisArg, argArray)

著者プロフィール

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

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

コメント

コメントの記入