search phpbb-phpbb-FC2BLOG-Info-Edit Template-Post-Edit-Upload-LogOut
jQuery解読作業を進めるにつれ、Javascriptの基礎が如何に分かっていないか、ほぼ毎日のように痛感させられます。その意味ではjQuery解読は無謀なチャレンジであった訳ですが、それでも誤謬を犯すマイナスを埋め合わせて遙かに余りあるプラスがあることも毎日自覚されるので、恥を忍んで引き続きjQuery解読を進めるつもりでいます。
しかし、基礎の基礎が余りに分かってない自分に嫌気が指してきてしまい、匙を投げ出すような醜態は演じたくありません。そこで、自戒を込めて敢えて誤解していたこと、理解していなかったこと、不十分な理解に留まっていたことなどについて、つらつらと記述していきたい、と思います。
「this」──Javascriptを始めたばかりの素人は、直ぐに「thisって何?」と躓きます。しかも度々、あちこちで登場するので、どうも役割がいくつかあるらしいと気がつくと共に、混乱は深まります。
半可通のままでは駄目なことを自覚させられ、やむを得ず学習を深めると
などが次第に分かってきます。
この「this」が jQuery.js では多用されています。「これでもか!」というくらいあちこちに登場します。従って this をきちんと理解しないと、jQuery は殆ど理解出来ません。しかしそれは決して難解なことではありません。this の定義さえしっかり踏まえれば容易なことです。
jQuery.js コード内において、第一に、インスタンスメソッド内の、つまりプロトタイプオブジェクトのプロパティ内における this は、上の3番目の定義からインスタンスを指し示すことになります。第二に、クラスメソッド内の this は上の 2番目の定義からクラスを指し示します。
jQuery.jsにおけるクラスとは、そう機能するように定義された グローバル変数 jQuery(及びそのコピーである 変数 $ ) に他なりません。
第三に、そして興味深いことは、ユーザーによる jQuery() 実行指示時においても、this を効果的に使用することが出来ることです。例えば、jQuery for JavaScript programmers から引用すれば、
//引用ブロックのテキストコンテンツを順に alert する
jQuery('blockquote').each(function(el) { alert(jQuery(this).text()) });
//マウスオーバー時とマウスアウト時の背景色をそれぞれ変更する
jQuery('a').hover(
function() {
jQuery(this).css('background-color', 'orange');
},
function() {
jQuery(this).css('background-color', 'white');
}
);
そしてどの様な場合においても、「this とはそれが指し示すオブジェクトへの参照である」ことをきちんと踏まえていれば、コードを解読する場合や記述する場合においては、this をそれが指し示すところのオブジェクトそのものであると「思いこむ」ことが手っ取り早い、現実的なthisの利用方法であると思います。
それにしても上のコードにおける jQuery(this) ってどういうこと?───と一瞬思いました。これについてはこのエントリイの末尾で触れたいと思います。
それにしても、ユーザーが入力した jQuery(・・・) に返されるインスタンスオブジェクトは、具体的にはどんな内容のものなのでしょうか? それを知ることは決して意味のないことではありません。
なお、FireBugを使用すれば使いながら(on the fly)インスタンスオブジェクトの内容を見ることが出来ますから、敢えて表示することもない───と言うことも出来ます。しかし、ここでは敢えて学習のためにも、何はともあれインスタンスオブジェクトを表示してみたいと思い、それを行ってみました。
その方法は例えば、強引ではありますが、jQuery.js がインクルードされているサイト(勿論このエントリイでも構わない)を開いてから、FireBug を起動し、そのコンソールに次のコードを入力すれば、当該サイトの末尾に当該サイトのリンクタグ一覧を取得したばかりの、jQueryインスタンスオブジェクトのプロパティ一覧が追加されます。
また下のボタンをクリックすると下の関数を実行し、この頁の末尾に一覧を追加します。
(function($){ var obj =$("a"), tmp=""; for (var i in obj) tmp += "<div>" +i +" : "+ obj[i]+"</div>"; $(document.body).append(tmp); //bodyに tmp 要素を追加する })(jQuery);
指定した要素(上の例ではリンク要素)を取得したインスタンスオブジェクトのプロパティ一覧を見れば、当該要素が確かに1つの配列にその要素として格納されていることが分かります。上の例で言えば、それは「 0 : http://・0・・改行、1 : http://・1・・改行、・・・・」のように表示されますから、 [<a href="・0・・">, <a href="・1・・">, ・・・] のように格納されていることが分かります。勿論その他に 60 を越える数の、プロトタイプを利用して設定されたインスタンスメソッドやインスタンスプロパティが存在していることが、改めて分かります。
インスタンスオブジェクトの内容が分かったところで、次にそこから指定した要素ノードを抽出する方法を考えてみます。
それはたやすいことです。対象は配列の要素であり、init()メソッドを読めば、指定した要素オブジェクトは、連想配列ではなく単純な配列として格納されていることが分かりますので、上の例で言えば $("a")[0]、あるいは $('a')['2'] とすれば目的を達します。実際 FireBug のコンソールに $("a")[0] を入力して実行すると、当該サイトの最初のリンク要素が <a href="・・・"> と表示されます。
序でに触れれば、プロトタイプを利用して作成されたインスタンスメソッドも連想配列を利用して自在に表示出来ます。
$("a")["index"]、$()["init"]、・・・・
この件については、jQueryに学ぶJavascriptの基礎(4) インスタンスオブジェクトからの値の取得について──jQuery解読(20) で再考しました。よろしければご覧ください。
次に、jQuery()のインスタンスメソッドである meth()において this をどのように使うのか、その方法を考えてみます。
メソッド内の this はその定義から、そのメソッドが実行されているオブジェクト、つまり jQuery(・・) を指し示すことになります。ところで、jQuery(・・) はinit()メソッドによって、「 指定した要素ノードを要素とする配列を、そのプロパティの一部に内包しているインスタンスオブジェクト 」を返値として受け取っています。つまり meth()内の this はこのインスタンスオブジェクトを指し示すことになります。
いきなりですが、jQuery(InsObj) を指示すると、init()メソッドによる初期化を経て、当該の InsObj が返値として返されます。init()を読めばそうなることは直ぐに分かりますが、次のコードを実行すればそれが視覚化されます。
※ InsObj はインスタントオブジェクトを意味します。
(function($){
var InsObj = $(); //インスタンスオブジェクトを取得、引数はなくても問題なし
InsObj = $(InsObj); //それを引数として $() を実行し結果を InsObj に代入
for (var i in InsObj)
document.write ( i +" : "+ InsObj[i] + "<br />");
document.close();
})(jQuery);
改めて jQuery for JavaScript programmers から引用したコードを見てみます。(このエントリイのずっと上に表示)
jQuery('blockquote') または jQuery('a') 関数 が実行を終了した時点では、このオブジェクトは返値であるインスタンスオブジェクトを受け取っています。
そして直上の jQuery(insObj) の解明を踏まえれば、ずっと上の例におけるそれぞれの jQuery(this) はその返値として jQuery('blockquote') または jQuery('a') が受け取っているインスタンスオブジェクトを受け取っています。だからこそ jQuery のインスタンスメソッドである each() や hover() メソッド内において jQuery(this) を利用してjQueryインスタンスメソッドである text() や css() が使用出来る訳です。
jQuery解読作業を進めるにつれ、Javascriptの基礎が如何に分かっていないか、ほぼ毎日のように痛感させられます。その意味ではjQuery解読は無謀なチャレンジであった訳ですが、それでも誤謬を犯すマイナスを埋め合わせて遙かに余りあるプラスがあることも毎日自覚されるので、恥を忍んで引き続きjQuery解読を進めるつもりでいます。
しかし、基礎の基礎が余りに分かってない自分に嫌気が指してきてしまい、匙を投げ出すような醜態は演じたくありません。そこで、自戒を込めて敢えて誤解していたこと、理解していなかったこと、不十分な理解に留まっていたことなどについて、つらつらと記述していきたい、と思います。
ここでは return 値の受け取り先について、『Javascript第5版 日本語訳』における記述の曖昧さを指摘すると共に、jQuery().init()メソッドにおけるreturnチェーンについてまとめたいと思います。
四則計算のような簡単な関数の場合、return で返される値は余りに明白です。return a*b ならば a に b を乗じた計算結果が返値になります。
またreturn Boolean の場合には true か false が返されます。そして同時に或る関数から true や false が返された場合のJavascriptの振る舞いについても、状況に応じた一定の決まり事があるようです。(status行に関するコードやタグ属性として記述されたJavascript文の場合等)
一般に、返値は数値、文字列、論理値及びオブジェクトのいずれでもあり得ます。クロージャーの場合のように関数自身を返値とすることさえある訳ですから、返値はあっさり言えばJavascriptで扱えるものならば「何でもあり」と思います。
『Javascript第5版』には「return はそれを含む「関数の呼び出し元に返値を返す」」と書いてあります。
関数がリテラル標記されている場合で右辺の関数が無名だった場合について考えてみます。
jQuery関数もこのように無名関数として定義されていますが、例えば、var funcName = function (a,b) {return a + b }; のような場合です。
この関数は funcName(2,3) のような標記によって呼び出され returnすべき値として 5 を算出します。この場合の呼出し元は言うまでもなく funcName(2,3) です。
つまり、変数 funcName に返値 5 が代入されるのではなく、 funcName は関数オブジェクトそのものであって、alert( funcName ) の出力結果は function (a,b) {return a + b } となります。
確かに var result = funcName (2,3) とすれば変数 result に 返値 5 が代入されますが、それは呼出し元に返された return 値が変数に代入されているに過ぎません。返値を受け取る受け取り先は変数result でも、funcName関数オブジェクトでもありません。
以上は余りに当たり前のことですが後述することとの関係でまず確認のために記しました。
よく知られているように、無名関数をあるオブジェクトのプロパティ値に設定する場合、つまり或るオブジェクトのメソッドにするには obj.meth = function (a,b) {return a + b }; とします。
この場合オブジェクト obj のプロパティである meth に関数オブジェクトへの参照が代入され、実行演算子()を使って obj.meth(3,4) のように指示してこの関数が実行されます。
では、この場合の返り先はどこでしょうか? オブジェクトobjでしょうか?
もちろんそれは違います。リテラル標記時の左辺が返り先でなかったのと全く同様に、この場合にも返値の受け取り先は obj.meth(3,4) です。
さて、ここから本題です。『Javascript第5版』では次のような一説があります。
オブジェクトを介して呼び出す関数のことをメソッドと呼びます。メソッドを呼び出したオブジェクトは暗黙的に引数として関数に渡されます。(p.123)
この文章を読むと、メソッドの場合の呼出し元は当該オブジェクトであり、他所で「関数の呼び出し元に返値を返す」と書いてあるのだから、メソッドの返値はそれを呼び出したオブジェクトに受け取られる、と理解してもおかしくありません。
しかし、オブジェクトはメソッドの呼び出し元ではありませんし、返値はオブジェクトに返される訳ではありません。オブジェクトを介して、オブジェクト上で、当該関数が実行されるだけです。
「オブジェクトを介して呼び出す」との表現と「メソッドを呼び出したオブジェクト」という表現は微妙に意味が異なります。介して呼び出された場合の呼出し元は、決して介されたオブジェクトではないのです。従ってメソッドを呼び出したオブジェクトという表現は適切さを欠くと言わざるを得ないでしょう。
この曖昧な表現故に、少なくとも私は、メソッドの場合には当該オブジェクトに返値が返される、と誤解させられたのですから。
「メソッドを呼び出したオブジェクト」は原典では「When a function is invoked on an object, the function is called a method, and the object on which it is invoked is passed as an implicit argument of the function.」(アンダーライン部が「メソッドを呼び出したオブジェクト」と訳された箇所)です。アンダーライン部を直訳すれば「それ(メソッド)がその(オブジェクト)上で呼び出されたオブジェクト」はとなり「メソッドを実行したオブジェクト」と表現した方が、「呼出し元」なる表現との差異を明確にするためにも適切ではないか、と思います。
呼出し元が或るオブジェクト上(あるいは「内」)の1つのプロパティである、メソッド関数を実行し、その結果が呼出し元に返されるのですから。
一方「return文が指定されていれば、関数の実行はここで中断され、式の値(もしあれば)が呼び出され呼出し元へ返されます(p.124)」は原典では「it causes the function to stop executing and to return the value of its expression (if any) to the caller.」です。callerは訳すと確かに「呼出し元」となります。
つまり call と invoke をそれぞれ異なる適切な言葉で表現すべきであって、共に「呼び出す」と訳していることが読者に混乱を招いている、と思います。
<1> 55: this[0] = tmp; 56: this.length = 1; 57: return this; <2> 65: return new jQuery( context ).find( selector ); <3> 72: return this.setArray([ selector ] );
init() メソッド内には this を直接返す場合(<1>)、新たなインスタンスを返す場合(<2>)、this.setArray() メソッドを返す場合(<3>)など様々な return 値があり大変勉強になります。
下記コードの進行過程を具に解析する作業を通じて返値を解析してみます。
var jQuery = function (a,c){
return this instanceof jQuery ? this.init(a,c) : new jQuery(a,c)
}
因みに、jQuery()は var jQuery = function(){・・・return this.init()} というリテラル形式関数ですから、先に見たように無名関数から返された return 値はこの式内では受け取れません。受け取り先はこの関数を呼び出したもの、つまりユーザーが入力した jQuery(x,y) に他なりません。
また蛇足ながら、returnチェーンこそがメソッドチェーンを実現している「 秘策 」に他なりません。
55行:this[0]にtmpが代入されます。つまりインスタンスの最初の要素がtmpとなります。
56行:次にその配列の要素数が1とされます。これによりもし要素数が2以上であった場合であっても強制的に1となります。
57行:最後にこうして加工された this、つまりインスタンスが jQuery()関数に 返され、最終的にユーザー入力 API に返されます。
実は、jQueryではインスタンスがjQuery()関数に返されるシーンは結構沢山あります。それこそがjQuery.jsの神髄とも言えるメソッドチェーンを実現するために、こうした仕様となっているのでしょう。
これは細かく説明するまでもなく、new 実行後の新たなインスタンスがjQuery()関数に返され、それがユーザー入力 API に返されることになります。
この場合には setArray() メソッドを分析すれば、Array.prototype.push.apply( this, a ) によって、取得されたエレメントノードを要素とする配列がインスタンスオブジェクトに返され、それが init() からの返値となり、結局それがユーザー入力 API に返されることになります。
以上のように、this キーワードと return が組み合わされて使用されると、一見複雑に見えます。jQueryでは生成されるインスタンスを明示的に固有の変数に代入しませんので、インスタンスオブジェクトが直接変数としてコード内に出現することはありません。、それは常に this によって指し示されるだけであり、このことがコードの理解をより一層分かりにくくしているのではないか、と痛感しています。この見えないインスタンスに散々苦労させられたのは、私自身に他なりませんから(苦笑)。
変数に代入されない return 値は一体どこに記憶されるのでしょう?
もちろん物理的にはメモリ上ですが、Javascriptにおいてどこに保存されるのでしょうか?
裏返せば変数に代入されない返値を参照する何らかの方法はあるのでしょうか?
関数が定義されるとCallオブジェクトが生成され、そのプロパティに引数やローカル変数に格納されるそうですので、このオブジェクトの中に返値も保存され、変数が代入先として用意されていれば、Callオブジェクトのプロパティとして記憶された返値が、当該変数に代入されるのでしょうか?
このCallオブジェクトのプロパティに返値が保存される、という考えは全くの推測に過ぎませんが、他に考えようがありません。
次のようにして全く変数に代入されない返値 98 を取得することが可能です。
var ret= (function() {return arguments[0] + arguments[1]; })(20,78)
また決して好ましい方法とは思いませんが、徒に複雑にしただけの以下のようなクロージャーを使っても同様に返値を取得することが出来ます。
(function () {
var r=arguments;
return function() {return r[0]+r[1]; };
})(20,78)() //これを実行すると 20 + 78 のreturn値である 98 が取得できる。
上のコードでは、まず無名関数 function () の引数に 20と78を代入して無名関数を実行し【(20,78)】、更にその返値として返される関数を実行【最後の()】して計算させ返値を求めています。
$.find() メソッド解読を始めてから数週間が経過したような気がします。それだけ長い時間を掛けてもまだその全容解読に至っていません。それは言うまでもなく私の非力故なのですが 、$.find() メソッドが jQuery において Selector と呼称されている、 css や DOM を縦横に利用したドキュメント内検索のための根幹のメソッドであって、 $.find() メソッドを解明するためには、検索条件に関連する部分のjQueryコードの全てを解読する作業を同時に行わなければならないからに他なりません。
CSS style属性の全て、id属性やclass属性の他、HTML要素の諸々の属性名称と属性値、DOM の子孫要素、兄弟要素等々、様々な条件によってHTMLドキュメント内を横断的に検索する、その検索ツールが $.find()メソッドなのですから、そう易々と全貌は見えないのかも知れません。
それでも、一人悶々と思案する過程を通じて、jQueryへの理解は格段に深まり、またJavascriptに関する基礎的な知識も、日に日に深化していることを実感しています。やはり思い切ってjQuery解読に踏み込んで良かった、と痛感するこの頃です。
解読作業ではひたすらコード進行を追いかけるのですから、jQueryをあれこれと使ってHTML文書を自由に操作することは余りありません。しかし、それでは何かおかしい、使ってこそのjQueryであり、使いつつ解読することが相互作用的に学習効果を高めるのではないか、と思い始めました。
そんな訳で、手始めに Slide Toggle Button を関連する各エントリイに設置してみました。
紙幅を要する抜粋されたコード部分について、エントリイ表示時には初期設定として隠蔽しておき、ボタン操作でコード掲載部分を表示したり、隠したりするようにしたのです。これまでならばDOMを使って直接コードを書いていたのですが、ここでは jQuery APIを意識的に使用してコードを書いてみました。
<!--表示/隠蔽用のボタンとその操作対象となる PRE タグを包含する div タグ を設け、その class 名を "togglePRE" として、ボタンと PRE タグのセットを 全て取得する。 --> <div class="togglePRE"> <button style="dicplay:block;">ボタン説明文字列</button> <pre style="display:none;">表示/隠蔽内容</pre> </div>
(function(){
1: var togglePRE = $(".togglePRE");
//当該クラス要素がない場合には何もしない。
2: if (togglePRE.length == 0) return;
//個々のクラス対象毎にイテレート
3: togglePRE.each(function(n){
//子要素の pre タグを抽出し、変数 pre に代入
4: var pre = $(this).children("pre");
//それを非表示にする。
5: pre.css("display","none")
//ボタンタグを選び、その disabled 属性を「有効」に変更し、
6: .end().children("button").attr("disabled","")
//ボタンがクリックされたら pre タグを slideToggle 操作する。
//同時にクリック時に背景色を変更/初期化するように設定
11: .click( function(){ pre.slideToggle(); } )
12: .mousedown(function(){this.style.backgroundColor='lightgreen'})
13: .mouseup(function(){this.style.backgroundColor=''});
14: });
15:})();以上のコードによりトグルボタンと表示/隠蔽要素の挙動を設定しました。言うまでもなく、上記コードの表示/隠蔽にもこのコードが適用されています。
さて、ここでは jQuery の特徴を踏まえないと間違いやすいコード作成上のポイントを自戒のために記しておきたいと思います。
或るクラス名の要素があるかどうかを調べる方法として、それが存在しない場合には $(".togglePRE") == false になると思っていました。ところが togglePRE クラスが存在しなくても、$(".togglePRE") == true となってしまいます。何故ならば $(".togglePRE") は該当要素が存在しない場合には $(document) を返すため、togglePRE クラスがなくても、$(".togglePRE") == true となってしまうのでした。
ですから、或る条件に合致する要素の存在有無をチェックするには該当要素の数をカウントさせる必要があります。つまり、$("条件").size() > 0 になるならば条件に合致した要素があったことになります。
試行錯誤の末にこのことを理解しました。
なお敢えてメソッドを使わなくても配列の length プロパティでも同様のチェックが可能であり、むしろこの方がすっきりしているかも知れません。
複数回使用するjQueryインスタンスは変数に代入して、繰り返し検索・作成しないようにしました。
エントリイ表示モードではない時、例えば検索結果を一覧で表示する場合や月別表示などの場合において、Google Addsence は3つ以上のエントリイにおいては表示されない仕様となっているようです。仕様というよりも複数のアドセンスが1つのページに表示されることを前提としたコードになっていないのでしょう。そこで3つ以上のエントリイを一覧表示すると、アドセンスのコード内でスクリプトエラーが発生してしまい、slideToggle ボタンも無効となってしまいます。
そこで非エントリイモードの時にはスライドトグルボタンを無効にしてしまって、表示対象を非表示から表示に変更して最初から表示してしまうようにしました。
むしろ、Google Addsenceを エントリイモードの時だけしか表示しないようブロック変数で制限するのが賢明というものでしょう。ですからそのように変更して、非エントリイモード時のエラー発生を排除し、それにより非エントリイモード時の特殊な対応(トグルボタンの無効化と表示対象の最初からの表示)をやめることとしました。
toggle対象要素となる pre タグには、初期値としてdisplay:none;を設定しました。そうしないとエントリイ表示モードではこの要素が一旦表示されてから、隠蔽されるからです。
pre.end().children("button") で end メソッドを使ってみました。変数 pre は var pre = $(this).children("pre") と定義したので、$(this)をゲットするために end() メソッドを使って children("pre") を解除しました。こうして $(this) を改めて取得せず、取得済みのそれを利用して $(this).children("button") を実行させています。
jQuery 1.2.1 版及び UI の登場してから約2ヶ月が経過しました。そして今、jQueryの評判はますます高まっているのではないか、と勝手に推測しています。
そんな訳で改めてjQuery関連サイトをサーフィンしてみました。確かに一部のサイトでは動きがあるようで最新版を対象として記述を更新したサイトも見つかりました。
決して推奨順に並べた訳ではありません。これまでお世話になった順でもあり、またより総合的なサイトを優先して並べたと言うことも可能です。
特筆すべきは古籏一浩氏がずっとVer1.0.3版をベースにしたリファランスを掲載していましたが(1.)、新たに Ver1.2.1 や UI を取り上げた Web 頁を作成したことでしょう。(2.)
なお、私はリファランスには興味がなく、個人的なお勧めは、1.及び2.よりも IBM のWebサイト(5.)です。また3.の開発者向けメモは日本語化の努力成果として大いに歓迎すべきでしょう。
このエントリイでは、jQuery.init()メソッド内の53行で呼び出されるjQuery().find(selector)メソッドを解読する手始めとして、jQuery().find()メソッドの最初に登場するjQuery.map()クラスメソッドの解読を行います。
225: find: function(t) {
226: var data = jQuery.map(this, function(a){ return jQuery.find(t,a); });
227: return this.pushStack( /[^+>] [^+>]/.test( t ) || t.indexOf("..") > -1 ?
228: jQuery.unique( data ) : data );
229: },
・・・・・
987: map: function(elems, fn) {
988: // If a string is passed in for the function, make a function
989: // for it (a handy shortcut)
990: if ( typeof fn == "string" )
991: fn = eval("false||function(a){return " + fn + "}");
992:
993: var result = [];
994:
995: // Go through the array, translating each of the items to their
996: // new value (or values).
997: for ( var i = 0, el = elems.length; i < el; i++ ) {
998: var val = fn(elems[i],i);
999:
1000: if ( val !== null && val != undefined ) {
1001: if ( val.constructor != Array ) val = [val];
1002: result = result.concat( val );
1003: }
1004: }
1005:
1006: return result;
1007: }全く知りませんでしたが、「map」とはプログラミング界では一般的な専門用語のようです。ある関数の引数として別の関数を受け取る関数を「高階関数(higher-order function)」と呼ぶらしい。その結果、ある配列の各要素を対象として順番にそれを操作した結果を返す関数を作ることが出来る、ということらしい。
C言語やPerlなどでは一般的なようですが、Javascriptの場合実装されてないため、each()メソッドと同様に、フレームワークで色々と工夫されている模様です。
さてその map 関数ですが、要所は998行の val = fn(elems[i],i); にあると思います。元の関数の第二引数である関数の、その第一引数に、元の関数の第一引数である配列の各要素を渡しています。後はこのvalを元の配列回数分だけ集めて新たな配列としてまとめればよい訳で、まとめ上げられたその配列は、呼出し元の関数の返値となって目的が達せられています。
まずjQuery.js 1.1.4の解説にあった例題を掲載します。
これらの例によって、$.map()を使って、単純に配列要素に加算したり、条件づけて配列要素数さえ増減させたり、自在に加工出来ることがよく分かります。
そこで$.map()の定義を踏まえて独自のサンプルを作ってみます。
$.mapの定義から、引数となる関数には2つの引数(元の配列の各要素とインクリメント値)を渡していますので、jQuery.js1.1.4のサンプルではなかった2つの引数を取る場合を作成してみました。
以上を踏まえて本題に戻ります。IEやOperaにおける id 値による DOM 操作時に、同一のname値をもつ要素を拾ってしまった場合の対処として、jQuery().find(a)が起動され、その結果$.map()メソッドがよびだされたのでした。
呼び出されたmapメソッドは具体的には次のようになっています。
つまり、配列 this の各要素に対して、jQuery.find(t,thisの要素) を適用し、その結果の配列を data に代入しています。ここに this は $.map()が jQuery().find()から呼び出されたのですから jQuery() インスタンスの返値である配列になっているはずです。
ところで上の内容を知るためには、次に進まなければなりません。 jQuery.find() メソッドを解明しなければなりません。
--------------------------------------------------------
ということで、次は愈々長大な $.find()メソッドの解読です。
直前のエントリイで jQuery( args ).find( selector, context ) について概観しました。$().find() メソッドは僅か5行しか記述がありませんが、その僅かの記述の中に、 $.map()、$.find()、$().pushStack()、$.unique() 及び $.data()メソッドと、4つの jQuery クラスメソッドと1つのインスタンスメソッドを呼び出しています。ここで最重要なメソッドは jQuery.find( selector,elem ) クラスメソッドで、find の具体的な検索処理はこのクラスメソッドが全部請け負います。
そこでこのエントリイでは、この長大な jQuery.find() クラスメソッドの解読を行います。
jQuery.find() クラスメソッドは、1445-1635 行までの約 200 行もある長大なコードです。そこでまずその構造と各種サブルーチンの役割を概観しておきます。
下の表は$().find()から呼び出される各種のサブルーチンメソッドを登場順に一覧したもので、……より右の文章はそれぞれのサブルーチンメソッドの役割を簡潔に記したものです。
├init()
│ |
│ ├$(context).find(selector)
│ │ ├$.map(this,fn(elem){})……this 配列(=$(context)のインスタンス)の
│ │ │ 各要素 elem を fn 関数で指定した内容に変換する
│ │ │└$.find(selector,elem)……elem の中から selector にマッチする要素を抜き出す
│ │ │ ├$.trim(selector)……selector の最初と最後にある空白を削除
│ │ │ ├$.data(elem)……elem に 固有 id 番号(uuid) を振る
│ │ │ ├$.trim()
│ │ │ ├$.merge(a,b)……a, b2つの配列を合体する
│ │ │ ├$.isXMLDoc(elem)……elem が XML要素かどうかを判定する
│ │ │ ├$.merge()
│ │ │ ├$.classFilter(r,m)……r 配列の中から m というclass名のelemを抽出/除外する
│ │ │ ├$.filter(t,r)……r 配列の各要素に t 文字列によるフィルタを適用する
│ │ │ │ ├$.parse……各種のフィルタを検索するための正規表現文字列の配列
│ │ │ │ ├$.filter()……再帰呼び出し
│ │ │ │ ├$(elem).not(sel)……sel に該当する要素を elem 配列から除外する
│ │ │ │ ├$.classFilter()
│ │ │ │ ├$.props……各種属性値の名称統一化のためのオブジェクト
│ │ │ │ ├$.attr……各種属性値を処理する
│ │ │ │ ├$.data
│ │ │ │ ├$.expr……":"に続く文字列などによるフィルタリングを行うためのオブジェクト
│ │ │ │ └$.grep(arr,fn(){})……関数で指定した条件に合う要素をarr配列から抽出する
│ │ │ ├$.trim()
│ │ │ └$.merge()
│ │ └$(args).pushStack(elem)……新たにjQuery(elem)のインスタンスを作り、そのプロ
│ │ │パティ値に今のインスタンスを格納してから新インスタンスを$(args)関数に返す
│ │ └$.unique(arr)……arr配列の要素で重複があれば重複をなくしその結果を受け取る。
│ │ └$.data()
上に見られるように、$.find() の中では、沢山の jQuery クラスメソッド等がサブルーチンとして処理を請け負い、役割分担して、context から selector に該当するエレメント等を検索することになります。
長大なコードなので内容に応じて適宜分節しながら解読を進めます。
1445: find: function( t, context ) {
1446: // Quickly handle non-string expressions
1447: if ( typeof t != "string" ) // tが文字列でない場合には
1448: return [ t ]; // 配列[t]をリターンして処理を終える。
1449:
1450: // check to make sure context is a DOM element or a document
// contextが要素ノードでもdocumentでもない場合には空配列を返して処理を終える。
1451: if ( context && context.nodeType != 1 && context.nodeType != 9)
1452: return [ ];
1453:
1454: // Set the correct context (if none is provided)
1455: context = context || document; //context がなければ document を代入する
1456:
1457: // Initialize the search 検索処理用変数定義
1458: var ret = [context], done = [], last, nodeName;
1459:
1460: // Continue while a selector expression exists, and while 1461: // we're no longer looping upon ourselves /* 1462 行の while は 1620 行まで 159 行も続く長いフレーズである。 * この中で文字列 t を様々に分解しながら切り取り、様々なフィルターに掛けて * それにヒットするかどうかを検証し、対象を抽出する。 * そのための初期処理が 1463-1468 行である。 */ 1462: while ( t && last != t ) { 1463: var r = []; // 検索結果の要素ノードを格納する配列を準備 // while 終了準備処置 これ以降の行で t の値が変更されれば last != t となるから // while 処理が継続され、つまり以下の検索が反復される。 1464: last = t; 1465: // tの先頭と末尾にある空白文字を削除 1466: t = jQuery.trim(t); 1467: // 検索結果有無を示すフラグを「ナシ」とする 1468: var foundToken = false; 1469:
/* "> 任意の文字列"形式による検索を行う箇所である。 * つまり子要素指定が有る場合の抽出作業である。 * 但し、検索対象文字列 t の先頭文字が > の場合を探しているので、selector * 文字列において、既に別の検索が処理された後の二度目以降の検索処理を行う箇所となる。 */ 1470: // An attempt at speeding up child selectors that 1471: // point to a specific element tag // 先頭文字が > でこれに何らかの文字列が続く場合の正規表現文字列 //(1356行で定義済み)を re に代入 1472: var re = quickChild; 1473: var m = re.exec(t); // 検索対象 t に re が含まれるかチェック 1474: 1475: if ( m ) { // 検索対象 t に re が含まれれば... //m[1] は quickChild の定義から chars=何らかの文字列 を指す 1476: nodeName = m[1].toUpperCase(); 1477: /* 検索対象 ret 変数の各対象毎に巡回し、 * 更に ret[i]の第一子要素を初期値とし、その兄弟要素を巡回する。 * ノードタイプが要素エレメントで、それが何らかの要素名か、あるいは * quickChild 検索で抽出済みのノードネームに等しければ * その子要素を r 配列に格納する。 */ 1478: // Perform our own iteration and filter 1479: for ( var i = 0; ret[i]; i++ ) 1480: for ( var c = ret[i].firstChild; c; c = c.nextSibling ) 1481: if ( c.nodeType == 1 && (nodeName == "*" || c.nodeName.toUpperCase() == nodeName) ) 1482: r.push( c ); 1483: /* 取得した検索結果配列を ret に代入する。これにより検索対象配列 ret が * 検索結果配列に置換される。 * 一方、検索文字列 t から 今検索した文字列=「>何らかの文字列」を削除する。 * 直前検索文字列削除後の t に空白が含まれれば検索を続ける。 */ 1484: ret = r; 1485: t = t.replace( re, "" ); 1486: if ( t.indexOf(" ") == 0 ) continue; 1487: foundToken = true; // 検索結果有無フラグをtrue、つまり検索結果ありとする。
ケースBでは、(1)先頭に>があり、(2)その後に空白文字が 0 個以上あって、かつそれに(3)数字かアルファベットかアンダースコアが続くような検索文字列であるかどうかをチェックする。
ケースAでは、数字/アルファベット/アンダースコアではない文字、例えば:、,、#、$などが含まれていても構わなかったが、ケースBではこれらの文字は対象とされない。
/* 正規表現文字列 /^([>+~])\s*(\w*)/iは次のことを意味する。 * 先頭文字が>、+ あるいは ~ のいずれか(第一部分文字列)であって * その後に空白文字が 0 個以上あり、 * 数字/アルファベット/アンダースコアが0個以上続く文字列を * 大文字小文字の区別なく探す */ 1488: } else { 1489: re = /^([>+~])\s*(\w*)/i; 1490: //t に対する re 正規表現文字列の検索結果を m に代入し、それが空でなければ 1491: if ( (m = re.exec(t)) != null ) { 1492: r = []; //空配列 r を準備(検索結果受け取り用) 1493: 1494: var merge = {}; //空オブジェクト merge を準備(id照合用) 1495: nodeName = m[2].toUpperCase(); // \w* に該当する文字列を大文字にして 1496: m = m[1]; // m に第一部分文字列(>、+ あるいは ~ のいずれか)を代入 1497: // 検索対象ノードを巡回する。 1498: for ( var j = 0, rl = ret.length; j < rl; j++ ) { /* 第一部分文字が ~ か + ならば(つまり兄弟要素を探す場合には) * ret[j].nextSiblingを、また第一部分文字列が > の場合には * ret[j].firstChild(第一子要素)を、それぞれ n に代入する。 */ 1499: var n = m == "~" || m == "+" ? ret[j].nextSibling : ret[j].firstChild; // その n 要素の兄弟要素を順に巡回し //そのノードタイプが要素ノードならば、それに uuid 番号を振って id に代入。 1500: for ( ; n; n = n.nextSibling ) 1501: if ( n.nodeType == 1 ) { 1502: var id = jQuery.data(n); 1503: /* 第一部分文字が ~ で(つまり兄弟要素を探していて)既に id 番号が振られた * 要素がある場合(正確に言えば既に 1507行で merge[id}==true が与えられて * いる場合)には、兄弟要素を検索するループから抜け出す。 */ 1504: if ( m == "~" && merge[id] ) break; 1505: /* nodeName がないか、n の nodeName が先に求めた nodeName と等しい時に * 第一部分文字が ~ ならば、merge[id] に true を代入してから * 当該の n 要素を r 配列に格納する。 * またもし 第一部分文字が + だった場合には(直近の兄弟要素を見つけるのだから) * 1つ見つかればもう探す必要はないので、兄弟要素を探すループから抜け出す。 */ 1506: if (!nodeName || n.nodeName.toUpperCase() == nodeName ) { 1507: if ( m == "~" ) merge[id] = true; 1508: r.push( n ); 1509: } 1510: 1511: if ( m == "+" ) break; 1512: } 1513: } 1514: /* 見つかった要素が入った配列 r を、検索対象要素が代入されている配列 ret に代入 * し、検索文字列のtから re に代入された部分検索用文字列部分を消去し、かつその * 後の t 文字列の先頭と末尾にある空白文字を削除してから、t を更新する。 * この節の最後に、検索対象有無を示すフラグを true とする。 */ 1515: ret = r; 1516: 1517: // And remove the token 1518: t = jQuery.trim( t.replace( re, "" ) ); 1519: foundToken = true; 1520: } 1521: } //END OF 1488 else 1522:
以上の検索に引っかからない場合(つまり子要素を探すのではない場合)、次の検索処理が 1525 行から 1611 行までで行われる。この節での検索は selectors による複数条件指定、.className 指定、#idName 指定の場合が処理される。
1523: // See if there's still an expression, and that we haven't already
1524: // matched a token
1525: if ( t && !foundToken ) {
: ・・・・・
1611: }
まず t が存在していて、検索結果有無フラグが false の時、1526-1539行でカンマを t から削除する処理が行われる。なおここでは検索そのものは行わない。
1526: // Handle multiple expressions
1527: if ( !t.indexOf(",") ) { // t の先頭文字が "," の場合
/* そもそも、検索文字列にカンマが入るのは
* jQuery(context).find( selector1,selector2,・・・・,selectorN ) が履行されている時
* である。
* さて、ret 配列には 対象要素エレメントの集合である context が代入されている。
* もし、ret 配列(それは [context] に等しい)の最初の要素が context そのものに
* 等しいならば、元々 context に1つの要素しかなかったことになる。
* つまり1つの要素ノードを対象として複数のパターンのセレクターを指示していること
* になる。この時にはdoneに2つの空要素を代入することによって、jQuery.find() から
* の返値を「なし」としている。
*/
1528: // Clean the result set
// context が1つの要素しかない場合、retを空白配列とする
1529: if ( context == ret[0] ) ret.shift();
1530:
1531: // Merge the result sets
// 空配列を第一要素とし、それにret(これも空配列)が続く配列を done に代入する。
// つまり done == [ [], [] ] とする。
1532: done = jQuery.merge( done, ret );
1533:
1534: // Reset the context
// r と ret に [context]配列を代入
1535: r = ret = [context];
1536:
1537: // Touch up the selector string
// 検索文字列 t の先頭文字(これはカンマである)を削除し、替わりに
// 半角スペースに置き換える。これにより別の節での検索対象としている。
1538: t = " " + t.substr(1,t.length);
1539:
次の検索処理は、いくつかの前処理が行われてから行われる。
1540: } else {
1541: // Optimize for the case nodeName#idName
// 文字通り「ノードネーム#idネーム」の文字列を探す
1542: var re2 = quickID; // 1357行で定義されている正規表現文字列
1543: var m = re2.exec(t); //検索結果を m に代入
1544:
1545: // Re-organize the results, so that they're consistent
// m があればその要素を次のように並び替える。
// [ 0, "#", idName, nodeName ]
1546: if ( m ) {
1547: m = [ 0, m[2], m[3], m[1] ];
1548:
1549: } else {
1550: // Otherwise, do a traditional filter check for
1551: // ID, class, and element selectors
1552: re2 = quickClass; // 先頭文字に "." または "#"が1個以下ある場合の正規表現文字列
1553: m = re2.exec(t); // t を re2 正規表現文字列でチェックしその結果を m に代入
1554: }
1555:
1556: m[2] = m[2].replace(/\\/g, ""); //chars文字列内から \ を削除 1557: 1558: var elem = ret[ret.length-1]; //elem に検索対象要素配列の最後の要素を代入 1559:
/* 検索文字配列 m の2番目の値が "#" の場合の検索を行う。
* 変数 oid にgetElementByIdメソッドの結果を代入する。
*/
1560: // Try to do a global search by ID, where we can
1561: if ( m[1] == "#" && elem && elem.getElementById && !jQuery.isXMLDoc(elem) ) {
1562: // Optimization for HTML document case
1563: var oid = elem.getElementById(m[2]);
1564:
/* IE と Opera において発生するバグ( form要素内のname属性の値をid値と誤って
* 認識するバグ)に対する対策を施す。
* XPathにおいて「@id は id という名前のアトリビュートを選択する」ことを指すから
* 1569 行によって、elem 内から id 属性値として m[2] を持つ要素が正しく選別される。
*/
1565: // Do a quick check for the existence of the actual ID attribute
1566: // to avoid selecting by the name attribute in IE
1567: // also check to insure id is a string to avoid selecting an element with the name of 'id' inside a form
1568: if ( (jQuery.browser.msie||jQuery.browser.opera) && oid && typeof oid.id == "string" && oid.id != m[2] )
1569: oid = jQuery('[@id="'+m[2]+'"]', elem)[0];
1570:
/* oid が見つかった場合に、m[3]がない、つまりセレクタ文字列が nodeName#idName
* 型ではなかった時か、あるいは nodeName#idName 型の時には oid のノードネームが
* m[3] つまり nodeName に一致すれば [oid]を、そうでなければ空配列を、
* それぞれ ret と r に返す。ここに空配列とはこの検索ルーチンでは目的の要素が
* 見つからなかったことを意味する。
*/
1571: // Do a quick check for node name (where applicable) so
1572: // that div#foo searches will be really fast
1573: ret = r = oid && (!m[3] || jQuery.nodeName(oid, m[3])) ? [oid] : [];
// 全ての子孫要素 (all descendant elements) を対象とした巡回検索を行う。 1574: } else { 1575: // We need to find all descendant elements 1576: for ( var i = 0; ret[i]; i++ ) { /* 最初の文字が "#" で nodeName が有れば、その nodeName 名を tag に代入し、 * そうでない場合、"." があるか m[0] が空白ならば(つまり quickID にも quickClass * にもヒットしなかった場合)"*"を tag に代入し、以上のいずれでもない場合には chars * 文字列を tag に代入する。 */ 1577: // Grab the tag name being searched for 1578: var tag = m[1] == "#" && m[3] ? m[3] : m[1] != "" || m[0] == "" ? "*" : m[2]; 1579: // tag が "*" で、ノードネームが object だったら tag に "param" を代入する。 1580: // Handle IE7 being really dumb about <object>s 1581: if ( tag == "*" && ret[i].nodeName.toLowerCase() == "object" ) 1582: tag = "param"; 1583: // これまでの r 配列に対して、検索対象内のタグ名が「tag」である要素を追加する。 1584: r = jQuery.merge( r, ret[i].getElementsByTagName( tag )); 1585: } 1586:
1587: // It's faster to filter by class and be done with it
1588: if ( m[1] == "." )
// classFilterメソッドを利用して r 要素内から m[2] の条件に該当する要素を抽出する
1589: r = jQuery.classFilter( r, m[2] );
1590:
1591: // Same with ID filtering
1592: if ( m[1] == "#" ) {
1593: var tmp = [];
1594:
1595: // Try to find the element with the ID
1596: for ( var i = 0; r[i]; i++ )
// id 属性値が m[2] と等しければ
1597: if ( r[i].getAttribute("id") == m[2] ) {
1598: tmp = [ r[i] ]; //tmpにその要素ノードを要素とする配列を代入
1599: break; //1596行の for ループを中断
1600: }
1601:
1602: r = tmp; // r にヒットした要素ノードが入った配列を代入
1603: }
1604:
1605: ret = r; //検索対象ノード配列に検索結果を上書き代入 1606: } //END OF 1574行のelse 1607: // re2(quickID 又は quickClass正規表現文字列)に該当する文字列を // 検索文字列 t から削除する。 1608: t = t.replace( re2, "" ); 1609: } //END OF 1540行の else 1610: 1611: } //END OF 1526行の if 1612:
子要素でもid名でもclass名でもない検索の場合、filter クラスメソッドを活用して検索を行う。
この filter クラスメソッドもまた長いコードなので、これについては別のエントリイで詳述する。
// これまでの検索を終えてもなお、t に文字列が残っている場合
1613: // If a selector string still exists
1614: if ( t ) {
1615: // Attempt to filter it
// 検索対象要素 r に t 文字列によるフィルタを適用し結果を val 配列に代入。
1616: var val = jQuery.filter(t,r);
1617: ret = r = val.r; // 配列 valから要素ノードを抽出して ret と r に代入
1618: t = jQuery.trim(val.t); // 配列 val から検索文字列を取りだして t に代入
1619: }
1620: }
1621:
// ここまで来ても t が残っていたらエラーが起こったはずなので、空配列を ret に代入 1622: // An error occurred with the selector; 1623: // just return an empty set instead 1624: if ( t ) 1625: ret = []; 1626: // ret がありかつ context が ret の第一項値に等しければ、retを空配列にする 1627: // Remove the root context 1628: if ( ret && context == ret[0] ) 1629: ret.shift(); 1630: // done に ret を併合し、その結果を return する。 1631: // And combine the results 1632: done = jQuery.merge( done, ret ); 1633: 1634: return done; 1635: },
上述の第二のケースの場合、65行により新たなjQuery(context)インスタンスが生成され、上述の一連の流れを経てinit()メソッドが終了します。この流れではjQuery()インスタンスが二度作成されるため複雑な動きをします。
そこでインタープリターがどの様に各種メソッドを呼び出しているのかを Firebug を使って追跡・解読してみました。解読結果を以下に図示します。
なお、ここで行った追跡は、次のようなセレクタとメソッドを指定して、ローカルパソコン上のHTMLファイル上で行いました。追跡に使ったセレクタとメソッドは jQuery("p > a").size()です。

jQuery().find() メソッドはjQuery()関数の初期化メソッドである init() メソッド内に2箇所登場します。
29: // A simple way to check for HTML strings or ID strings
30: // (both of which we optimize for)
31: var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/;
32:
33: // Is it a simple selector
34: var isSimple = /^.[^:#\[\.]*$/;
35:
36: jQuery.fn = jQuery.prototype = {
37: init: function( selector, context ) {
38: // Make sure that a selection was provided
39: selector = selector || document;
40:
41: // Handle $(DOMElement)
42: if ( selector.nodeType ) {
43: this[0] = selector;
44: this.length = 1;
45: return this;
46:
47: // Handle HTML strings
48: } else if ( typeof selector == "string" ) {
49: // Are we dealing with HTML string or an ID?
50: var match = quickExpr.exec( selector );
51:
52: // Verify a match, and that no context was specified for #id
53: if ( match && (match[1] || !context) ) {
54:
55: // HANDLE: $(html) -> $(array)
56: if ( match[1] )
57: selector = jQuery.clean( [ match[1] ], context );
58:
59: // HANDLE: $("#id")
60: else {
61: var elem = document.getElementById( match[3] );
62:
63: // Make sure an element was located
64: if ( elem )
65: // Handle the case where IE and Opera return items
66: // by name instead of ID
67: if ( elem.id != match[3] )
68: return jQuery().find( selector );
69:
70: // Otherwise, we inject the element directly into the jQuery object
71: else {
72: this[0] = elem;
73: this.length = 1;
74: return this;
75: }
76:
77: else
78: selector = [];
79: }
80:
81: // HANDLE: $(expr, [context])
82: // (which is just equivalent to: $(content).find(expr)
83: } else
84: return new jQuery( context ).find( selector ) ;
85:
86: // HANDLE: $(function)
87: // Shortcut for document ready
88: } else if ( jQuery.isFunction( selector ) )
89: return new jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector );
90:
91: return this.setArray(
92: // HANDLE: $(array)
93: selector.constructor == Array && selector ||
94:
95: // HANDLE: $(arraylike)
96: // Watch for when an array-like object, contains DOM nodes, is passed in as the selector
97: (selector.jquery || selector.length && selector != window && !selector.nodeType && selector[0] != undefined && selector[0].nodeType) && jQuery.makeArray( selector ) ||
98:
99: // HANDLE: $(*)
100: [ selector ] );
101: },
第一は 53: if ( match && (match[1] || !context) ) の時、つまり、
第二は文字列 selector が "・・<・・・>・・" でもなく、また "#id" でもない場合の new jQuery(context).find(selector) です(84行)。
どちらにも共通していることは、jQuery() 関数を再帰呼び出しすることであり、異なる点は new 演算子の有無です。どちらの場合においても jQuery 関数を再帰呼び出しするのですから、その定義から新しいインスタンスが作成されることに相違はありません。では何故 new 演算子の有無の別があるのか、興味深いところですが解明できていません。
new 演算子が有る jQuery 再帰呼び出しの場合には、二重に new 演算子が作用してしまうのではないかと考えますが、本当にそうなるのか、またその作用の意味は何か、解明できていません。
init() から呼び出される jQuery(context).find(selector) インスタンスメソッドは次のようになっています。
284: find: function( selector ) {
285: var elems = jQuery.map(this, function(elem){
286: return jQuery.find( selector, elem );
287: });
288:
289: return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ?
290: jQuery.unique( elems ) :
291: elems );
292: },
再帰呼び出しされる前者の jQuery().find("#id") では、DOM ツリー全体を対象として find("#id") メソッドが実行され、後者では新たなインスタンスを生成した上で context( context が与えられていなければ document 全体)を対象として find( selector ) インスタンスメソッドが実行されます。以下にその挙動の概要をまとめますが、find( selector ) インスタンスメソッドから呼び出される jQuery.find( selector, context )クラスメソッドの挙動の仕組みは別途解明することとし、それに先立ちこのクラスメソッドを利用する jQuery(context).find(selector) インスタンスメソッドの挙動を概観しておきます。
こうして context 内の selector に適合する要素が jQuery.find( selector, context ) クラスメソッドによって抽出され、これが jQuery( context ).find( selector ) に return されます。
これについては既に 1. で述べたことと全く同一になるので省略します。
jQuery.find() クラスメソッドを解読します。
33: jQuery.fn = jQuery.prototype = {
34: init: function(selector, context) {
・・・・
39: if ( typeof selector == "string" ) {
40: var m = quickExpr.exec(selector);
41: if ( m && (m[1] || !context) ) {
42: // HANDLE: $(html) -> $(array)
43: if ( m[1] )
44: selector = jQuery.clean( [ m[1] ], context );
・・・・・・
82: },
---------------------------------------------
765: clean: function(a, doc) {
766: var r = [];
767: doc = doc || document;
768:
769: jQuery.each( a, function(i,arg){
・・・・・・
854: },
直前のエントリイにおいて、何とか jQuery.each() メソッドを解明できましたので、次に44行の jQuery.clean([m[1]],context) で呼び出される clean()メソッド内で、eachメソッドが どの様に呼び出されているのかを調べます。
直前のエントリイで明らかにしたjQuery.each()メソッドの定義から、呼び出されたeachメソッドの最初の引数 a は、44行と765-769行によって cleanメソッドの最初の引数、つまり配列 [m[1]]となります。またfunction()の2つ目の引数argは、やはり定義からaの要素、つまり 配列[m[1]]の要素 となります。そしてこの場合のeachメソッド(769行)は引数が2つですので、先に見た4番目のケースとなります。
こうして769行の jQuery.each(a,function(i,arg)) は、具体的には jQuery.each([m[1]],function(i,[m[1]]の個々の要素)) となります。
次に、eachで反復対象となる a、つまり [m[1]] はどのような内容となるのか、改めて確認しておく必要があるでしょう。m[1] は31-40行の定義上から、タグの属性や子要素に任意の文字を含むhtmlタグの羅列となります。例えば"<div><p>Hellow!</p></div>"のような文字列です。従って [m[1]].length=1であり、arg="<div><p>Hellow!</p></div>"であり、これも1つしかありません。つまりeachは一回だけ適用されることになります。
わずか1度しか使わないのにどうしてeachメソッドを使う必要があるのか、この点は不明です。
さて、clean()メソッドを先に進むと、775-841行で「Convert html string into DOM nodes」(html文字列のDOMノードへの変換)が行われています。その最初の部分はタグのXHTMLスタイルへの変換ですが、ここで興味深いのは replaceメソッドが引数に関数を取っていることです。
これまではstring.replace(regexp,newString) しか知らなかったのです。関数を指定できるようになったのはJavascript1.3以降だそうですから、決して最近のことではないのですが、関数を使ったreplaceの例はこれまで見たことがなかったのです。
さて書籍等に拠ればこの場合の関数の引数は、順に「ヒットした文字列」、「最初の部分文字列」、「2番目の部分文字列」、・・・となるようです。
正規表現文字列「/(<(\w+)[^>]*?)/>/g」によって arg 内から抽出された文字列が第1引数 m に代入され、allには第1部分文字列である <(\w+)[^>]*? (mから末尾の2文字"/>"を省いた文字列)が、またtagには 第2部分文字列である \w+ (タグ名になる)が、それぞれ順次代入されることになります。
例えば argが"<img src='test' alt='test' />"である場合には m="<img src='test' alt='test' />"、all="<img src='test' alt='test' "、tag="img" と順に代入されます。
それにしても、置換メソッドは何を目的としているのか、最初は分かりませんでした。関数の返値を見るとここでも正規表現によるパターン検索が登場します。tag名に代入されたタグ名を対象として、tag.matchでパターン検索しているのはみな終了タグがないタグ名です。そして、当該パターン検索にヒットした場合には m が、その他の場合にはall+</tag>が返されます。つまり、tag.match検索にヒットした場合には、閉鎖タグがないままの<tag名・・・/>が返値となり、ヒットしなかった場合には<tag名・・・></tag名>が返されるのです。
当たり前のことをやっているだけではないか、と最初は不審に思いました。
そこで以下のテストコードを作成しfirebugでチェックしてみました。
(function(arg){
var result = arg.replace(/(\w+)[^>]*?)\/>/g, function(m, all, tag){
var tmp =[m,all,tag].join(", ")
alert(" m, all, tag = " + tmp);
return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area)$/i)? m : all+"></"+tag+">"
}
alert(result);
})("<div src='test' />");
上記のargに色々なタグ文字列を入れてみると、<img・・・/>はそのまま<img・・・/>となって帰ってくるし、/がない<img・・・>はそのまま<img・・・>で帰ってきます。そこで敢えて謝った記述 <div・・・/></div>をreplaceしてみたら、誤りを正して<div></div>が帰ってきました。
つまり、この置換メソッドは不正なXHTML構文を正す役割を負っているようです。ユーザーによって終了タグを記述しない誤記があった場合において、自動的に終了タグを挿入させるのです。しかし同時に終了タグがない要素名であって / が付いていないタグ記述に対して、それを付与する機能は持ってない、ということが分かりました。
jQuery.trim()───(911-913行)これは trim の名から推測されるとおり不要な空白文字列を削除するメソッドです。正確に言うとjQuery.trim(arg)メソッドは、arg文字列の先頭または末尾に存在する1個以上の空白文字列を削除し、途中にある空白文字は削除しません。引数tがなかった場合にエラーにならぬよう、(t||"")をreplace対象文字列としています。これによりarg文字列の先頭又は末尾にあるホワイトスペースが削除され、変数 s にその結果が代入されます。
このように空白文字を削除するのは、直後に生成されるdivエレメントと併せて、この後に続く"入力値に対する次なるエラーチェック"を行うためです。
「clean」の意味はこのwrapを解明することでかなり見えてきそうです。結論を先取りすれば誤ったタグ記述を正しく修正する為のコードがこのwrapに続く部分です。
この部分では極めてトリッキーなコードが3つあります。
第一はindexOf()メソッドの特異な利用方法です。string.indexOf("str")は、string文字列内にstrが含まれる場合には 0 または自然数を返します。含まれなければ -1 が返ります。さて、否定演算子を前に置いて、!string.indexOf("str")とすると、string.indexOf("str") = 0 の時だけこの値はtrueとなり、その他はfalseになります。0 の否定だけが true だからです。
こうして !string.indexOf("str") は string 文字列の先頭に str が含まれる場合にだけ true となります。このトリッキーな方法を使って s 文字列の先頭に或る文字列が含まれる場合に、或る配列を指定しそれを wrap に代入しているのが785-810行です。
トリッキーなコードの第二は、&& 演算子及び || 演算子の使い方です。var wrap への代入値は、連続する || 演算子によってその前後にあるいずれか一つの値となりますが、そのいずれかを選択するのは && の前に置かれたコードです。&& の前に置かれた式の値が true の時だけ && の右の値が取得されるからです。
因みにここで行われることを馴染みのコードで書けば、極めて冗長ですが次のようになります。