prototype.jsを読み解く
第5回 Prototypeライブラリ(1290~1608行目)
遠いリポジトリに便利な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)
prototype.jsを読み解く
- 第10回 Prototypeライブラリ(2846~3276行目)
- 第9回 Prototypeライブラリ(2621~2845行目)
- 第8回 Prototypeライブラリ(2277~2620行目)
- 第7回 Prototypeライブラリ(2052~2276行目)
- 第6回 Prototypeライブラリ(1609~2051行目)
- 第5回 Prototypeライブラリ(1290~1608行目)
- 第4回 Prototypeライブラリ(932~1289行目)
- 第3回 Prototypeライブラリ(640~931行目)
- 第2回 Prototypeライブラリ(198~639行目)
- 第1回 Prototypeライブラリ(1~197行目)


