10 | 2007/11 |  12

  1. 無料サーバー

User forum-FC2BLOG-Info-Edit Template-Post-Edit-Upload-LogOut

CSSやJavascript自習の苦闘史を綴っていきたい。恐縮ですがJavascriptを有効にしてご覧ください。
2005年12月から社会問題も掲載!


jQueryに学ぶJavascriptの基礎(2) this 活用──jQuery解読(17)

jQuery解読作業を進めるにつれ、Javascriptの基礎が如何に分かっていないか、ほぼ毎日のように痛感させられます。その意味ではjQuery解読は無謀なチャレンジであった訳ですが、それでも誤謬を犯すマイナスを埋め合わせて遙かに余りあるプラスがあることも毎日自覚されるので、恥を忍んで引き続きjQuery解読を進めるつもりでいます。

しかし、基礎の基礎が余りに分かってない自分に嫌気が指してきてしまい、匙を投げ出すような醜態は演じたくありません。そこで、自戒を込めて敢えて誤解していたこと、理解していなかったこと、不十分な理解に留まっていたことなどについて、つらつらと記述していきたい、と思います。

thisとは何か?

「this」──Javascriptを始めたばかりの素人は、直ぐに「thisって何?」と躓きます。しかも度々、あちこちで登場するので、どうも役割がいくつかあるらしいと気がつくと共に、混乱は深まります。

半可通のままでは駄目なことを自覚させられ、やむを得ず学習を深めると

  1. タグ属性に this と記述して(記述しなくても)そこから起動した関数内で this と記述すると当該タグを指し示すこと
  2. メソッドの場合に当該関数内で当該関数の起動元オブジェクトを指し示すこと
  3. new 演算子とコンストラクタ関数によって生成された所謂インスタンスの場合、当該コンストラクタ関数内(入れ子関数含む)や、プロトタイププロパティで設定するインスタンス関数内において、this が当該インスタンスを指し示すこと

などが次第に分かってきます。

▲ToTop

jQuery.jsで多用される 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) ってどういうこと?───と一瞬思いました。これについてはこのエントリイの末尾で触れたいと思います。

▲ToTop

jQuery()インスタンスオブジェクトを「視覚化」する

それにしても、ユーザーが入力した 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・・改行、・・・・」のように表示されますから、 インスタンスオブジェクトに、それぞれプロパティとして格納されていることが分かります。勿論その他に 60 を越える数の、プロトタイプを利用して設定されたインスタンスメソッドやインスタンスプロパティが存在していることが、改めて分かります。

jQuery()インスタンスオブジェクトから指定した要素を取り出す

インスタンスオブジェクトの内容が分かったところで、次にそこから指定した要素ノードを抽出する方法を考えてみます。

それはたやすいことです。指定した要素オブジェクトは、単純な配列のように格納されているのですから、上の例で言えば $("a")[0]、あるいは $('a')['2'] とすれば目的を達します。実際 FireBug のコンソールに $("a")[0] を入力して実行すると、当該サイトの最初のリンク要素が <a href="・・・"> と表示されます。

序でに触れれば、プロトタイプを利用して作成されたインスタンスメソッドも連想配列を利用して自在に表示出来ます。

$("a")["index"]、$()["init"]、・・・・

この件については、jQueryに学ぶJavascriptの基礎(4) インスタンスオブジェクトからの値の取得について──jQuery解読(20) で再考しました。よろしければご覧ください。

▲ToTop

jQuery(・・).meth(・・・) の meth(・・・)内で this を利用する

次に、jQuery()のインスタンスメソッドである meth()において this をどのように使うのか、その方法を考えてみます。

メソッド内の this はその定義から、そのメソッドが実行されているオブジェクト、つまり jQuery(・・) を指し示すことになります。ところで、jQuery(・・) は init() メソッドによって、「 指定した要素ノードを要素とする配列を、そのプロパティの一部に内包しているインスタンスオブジェクト 」を返値として受け取っています。つまり meth()内の this はこの jQuery インスタンスオブジェクトを指し示すことになります。

jQuery(InsObj) を実行すると InsObj が返される

いきなりですが、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);

具体的な meth() 内の this 利用

改めて 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の基礎(1) return 値は「何に」返されるのか?──jQuery解読(16)

jQuery 解読作業を進めるにつれ、Javascript の基礎が如何に分かっていないか、ほぼ毎日のように痛感させられます。その意味では jQuery 解読は無謀なチャレンジであった訳ですが、それでも誤謬を犯すマイナスを埋め合わせて遙かに余りあるプラスがあることも毎日自覚されるので、恥を忍んで引き続きjQuery解読を進めるつもりでいます。

しかし、基礎の基礎が余りに分かってない自分に嫌気が指してきてしまい、匙を投げ出すような醜態は演じたくありません。そこで、自戒を込めて敢えて誤解していたこと、理解していなかったこと、不十分な理解に留まっていたことなどについて、つらつらと記述していきたい、と思います。

ここでは return 値の受け取り先について、『Javascript第5版 日本語訳』における記述の曖昧さを指摘すると共に、jQuery()関数実行過程における return 値についてまとめたいと思います。

本稿の目的とは関係ないが・・・return の 返値

四則計算のような簡単な関数の場合、return で返される値は余りに明白です。return a*b ならば a に b を乗じた計算結果が返値になります。

また return Boolean の場合には true か false が返されます。そして同時に或る関数から true や false が返された場合のJavascriptの振る舞いについても、状況に応じた一定の決まり事があるようです。( status行に関するコードやタグ属性として記述された Javascript 文の場合等)

一般に、返値は数値、文字列、論理値及びオブジェクトのいずれでもあり得ます。クロージャーの場合のように関数自身を返値とすることさえある訳ですから、返値はあっさり言えば Javascript で扱えるものならば「何でもあり」と思います。

return はそれを含む「関数の呼び出し元に返値を返す」

『Javascript第5版』には「return はそれを含む「関数の呼び出し元に返値を返す」」と書いてあります。

関数がリテラル標記されている場合で右辺の関数が無名だった場合について考えてみます。

jQuery関数もこのように無名関数として定義されていますが、例えば、
   var funcName = function (a,b) {return a + b };
のような場合です。

この関数は funcName(2,3) のような標記によって呼び出され return すべき値として 5 を算出します。この場合の呼出し元はグローバル変数 window です。

勿論、変数 funcName に返値 5 が代入されるのではありません。funcName は関数オブジェクトそのものであって、alert( funcName ) で出力すれば function (a,b) {return a + b } が表示されます。

以上のことは、関数標記を function funcName(a,b) {return a + b } とすればより分かりやすくなるかもしれません。こうすればここには返値の受け取り先は何もありませんが、受け取っているのはwindow オブジェクトです。

さて、返値を取得するには var result = funcName (2,3) のように関数の返値の受け取り先を明示的に指示しなければなりません。こうすれば変数 result に 返値 5 が代入されます。

但し、それは呼出し元に返された return 値が 変数 result に代入されているからです。返値を最初に受け取る受け取り先は 決して変数 result でもなければ、funcName 関数オブジェクトでもなく window オブジェクトです。funcName (2,3) により window オブジェクトに返された返値が代入式によって result に代入されているに過ぎません。

以上は余りに当たり前のことですが後述することとの関係でまず確認のために記しました。

▲ToTop

メソッドの場合の返り先

よく知られているように、無名関数をあるオブジェクトのプロパティ値に設定する場合、つまり或るオブジェクトのメソッドにするには obj.meth = function (a,b) {return a + b }; とします。

この場合オブジェクト obj のプロパティである meth に関数オブジェクトへの参照が代入され、実行演算子()を使って obj.meth(3,4) のように指示してこの関数が実行されます。

では、この場合の返り先はどこでしょうか? それはオブジェクト obj の meth プロパティです。

さて、ここから本題です。『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 をそれぞれ異なる適切な言葉で表現すべきであって、共に「呼び出す」と訳していることが読者に混乱を招いている、と思います。

▲ToTop

返値を jQuery() の定義コードで見てみると...

定義によれば、jQuery() には jQuery.prototype.init メソッドをコンストラクタとするインスタンス(仮にこのインスタンスを insObj とします)が return されます。(jquery.js Ver 1.3.1 の場合には 24~27行)

コンストラクタ・プロトタイプの仕様から、insObj は init メソッドの prototype オブジェクトのプロパティを継承しますが、この init.prototype は、jQuery.prototype オブジェクトのプロパティを継承するように設定されています。(同532~533行)

この結果、insObj は init メソッドの返値及び jQuery.prototype オブジェクトから継承したプロパティを持つことになります。

かくして、insObj を受け取る jQuery() は init メソッドの返値及び jQuery.prototype オブジェクトから継承したプロパティを持つことになります。

以上のことをjQueryの this.init() で呼び出された init() メソッドでチェックしてみます

<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 値があり大変勉強になります。

まずreturn値の返り先について。

下記コードの進行過程を具に解析する作業を通じて返値を解析してみます。

var jQuery = function (a,c){
	return  this instanceof jQuery ? this.init(a,c) : new jQuery(a,c)
}
  1. ユーザーが jQuery(x、y) を実行する。
  2. インクルードされている jQuery.js 内の jQuery(x,y){・・・} が呼び出され実行される。
  3. この時 this は window オブジェクトを指しているから、3項演算子で条件分岐されて new jQuery(x,y) が実行される。つまり new 演算子によって jQuery インスタンスオブジェクトとして新しい空オブジェクトが生成され、かつ、今度は jQuery(x,y) がコンストラクタ関数として呼び出され、その中で使われる this は 仕様上当該のインスタンスオブジェクトを指すことになる。
  4. コンストラクタ関数として呼び出された jQuery(x,y) では、今度は this instanceof jQuery == true となるので、条件分岐されて this.init() が実行される。
  5. ところでその先が興味深い点である。 return this.init()とcoding されているから、ここから呼び出し元を逆順に辿って順次返値が返されていく。さてその最終の受け取り先は?
  6. その受け取り先こそ、ユーザーが記したjQuery(x,y)に他ならない。そもそもユーザーが記述したコードによって起動された jQuery.js内のjQuery(x,y)関数 が、その内部処理を経て return 値をユーザー側の入力 API に返すからこそ、メソッドチェーンと言われている、jQuery(●,■).×××().△△() のような連鎖的なAPIが可能となるのである!

因みに、jQuery()は var jQuery = function(){・・・return this.init()} というリテラル形式関数ですから、先に見たように無名関数から返された return 値はこの式内では受け取れません。受け取り先はこの関数を呼び出したもの、つまりユーザーが入力した jQuery(x,y) に他なりません。

また蛇足ながら、returnチェーンこそがメソッドチェーンを実現している「 秘策 」に他なりません。

▲ToTop

各々のケースの解読
<1>return this

55行:this[0]にtmpが代入されます。つまりインスタンスの最初の要素がtmpとなります。

56行:次にその配列の要素数が1とされます。これによりもし要素数が2以上であった場合であっても強制的に1となります。

57行:最後にこうして加工された this、つまりインスタンスが jQuery()関数に 返され、最終的にユーザー入力 API に返されます

実は、jQueryではインスタンスがjQuery()関数に返されるシーンは結構沢山あります。それこそがjQuery.jsの神髄とも言えるメソッドチェーンを実現するために、こうした仕様となっているのでしょう。

<2>return Instance

これは細かく説明するまでもなく、new 実行後の新たなインスタンスがjQuery()関数に返され、それがユーザー入力 API に返されることになります。

<3>return this.setArray

この場合には setArray() メソッドを分析すれば、Array.prototype.push.apply( this, a ) によって、取得されたエレメントノードを要素とする配列がインスタンスオブジェクトに返され、それが init() からの返値となり、結局それがユーザー入力 API に返されることになります。

<結論>
以上から、全ての場合においてインスタンスオブジェクトが init() メソッドから返され、それが jQuery() 関数の連鎖を通じて、ユーザー入力 API に返されることになります。
見えないインスタンス

以上のように、this キーワードと return が組み合わされて使用されると、一見複雑に見えます。jQueryでは生成されるインスタンスを明示的に固有の変数に代入しませんので、インスタンスオブジェクトが直接変数としてコード内に出現することはありません。、それは常に this によって指し示されるだけであり、このことがコードの理解をより一層分かりにくくしているのではないか、と痛感しています。この見えないインスタンスに散々苦労させられたのは、私自身に他なりませんから(苦笑)。

▲ToTop

それにしても記憶先はどこか?

命令 funName() あるいは obj.meth() それ自体は関数オブジェクトではなく関数の実行です。これに返値が返されることをこれまで見てきました。では返された return 値は一体どこに記憶されるのでしょう?

もちろん物理的にはメモリ上ですが、Javascript においてどこに保存されるのでしょうか?

裏返せば変数に代入されない返値を参照する何らかの方法はあるのでしょうか?

関数が定義されると Call オブジェクトが生成され、そのプロパティに引数やローカル変数が格納されるそうです。従って関数実行時にはこの 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)】、更にその返値として返される後の無名関数を実行して【最後の()】その返値を求めています。

jQuery()活用(1) Slide Toggle Button──jQuery解読(15)

$.find() メソッドの解読に時間を要している・・・

$.find() メソッド解読を始めてから数週間が経過したような気がします。それだけ長い時間を掛けてもまだその全容解読に至っていません。それは言うまでもなく私の非力故なのですが 、$.find() メソッドが jQuery において Selector と呼称されている、 css や DOM を縦横に利用したドキュメント内検索のための根幹のメソッドであって、 $.find() メソッドを解明するためには、検索条件に関連する部分のjQueryコードの全てを解読する作業を同時に行わなければならないからに他なりません。

CSS style属性の全て、id属性やclass属性の他、HTML要素の諸々の属性名称と属性値、DOM の子孫要素、兄弟要素等々、様々な条件によってHTMLドキュメント内を横断的に検索する、その検索ツールが $.find()メソッドなのですから、そう易々と全貌は見えないのかも知れません。

それでも、一人悶々と思案する過程を通じて、jQueryへの理解は格段に深まり、またJavascriptに関する基礎的な知識も、日に日に深化していることを実感しています。やはり思い切ってjQuery解読に踏み込んで良かった、と痛感するこの頃です。

それにしても jQuery を使ってこそ!

解読作業ではひたすらコード進行を追いかけるのですから、jQueryをあれこれと使ってHTML文書を自由に操作することは余りありません。しかし、それでは何かおかしい、使ってこそのjQueryであり、使いつつ解読することが相互作用的に学習効果を高めるのではないか、と思い始めました。

そんな訳で、手始めに Slide Toggle Button を関連する各エントリイに設置してみました。

紙幅を要する抜粋されたコード部分について、エントリイ表示時には初期設定として隠蔽しておき、ボタン操作でコード掲載部分を表示したり、隠したりするようにしたのです。これまでならばDOMを使って直接コードを書いていたのですが、ここでは jQuery APIを意識的に使用してコードを書いてみました。

▲ToTop

Slide Toggle Button 説明

HTML文 
<!--表示/隠蔽用のボタンとその操作対象となる 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 対策

    エントリイ表示モードではない時、例えば検索結果を一覧で表示する場合や月別表示などの場合において、Google Addsence は3つ以上のエントリイにおいては表示されない仕様となっているようです。仕様というよりも複数のアドセンスが1つのページに表示されることを前提としたコードになっていないのでしょう。そこで3つ以上のエントリイを一覧表示すると、アドセンスのコード内でスクリプトエラーが発生してしまい、slideToggle ボタンも無効となってしまいます。

    そこで非エントリイモードの時にはスライドトグルボタンを無効にしてしまって、表示対象を非表示から表示に変更して最初から表示してしまうようにしました。

    むしろ、Google Addsenceを エントリイモードの時だけしか表示しないようブロック変数で制限するのが賢明というものでしょう。ですからそのように変更して、非エントリイモード時のエラー発生を排除し、それにより非エントリイモード時の特殊な対応(トグルボタンの無効化と表示対象の最初からの表示)をやめることとしました。

  • Toggle対象要素について

    toggle対象要素となる pre タグには、初期値としてdisplay:none;を設定しました。そうしないとエントリイ表示モードではこの要素が一旦表示されてから、隠蔽されるからです。

  • .end()メソッドの活用

    pre.end().children("button") で end メソッドを使ってみました。変数 pre は var pre = $(this).children("pre") と定義したので、$(this)をゲットするために end() メソッドを使って children("pre") を解除しました。こうして $(this) を改めて取得せず、取得済みのそれを利用して $(this).children("button") を実行させています。

jQuery() の挙動を解読する(10) 表にしてjQuery.jsの挙動を追跡する──jQuery解読(14)

このエントリイはこちらに統合しました。

閑話休題───jQuery関連サイトの動向

jQuery解読の合間に・・・・

jQuery 1.2.1 版及び UI の登場してから約2ヶ月が経過しました。そして今、jQueryの評判はますます高まっているのではないか、と勝手に推測しています。

そんな訳で改めてjQuery関連サイトをサーフィンしてみました。確かに一部のサイトでは動きがあるようで最新版を対象として記述を更新したサイトも見つかりました。

有名どころ、と思ったjQuery関連サイト

決して推奨順に並べた訳ではありません。これまでお世話になった順でもあり、またより総合的なサイトを優先して並べたと言うことも可能です。

特筆すべきは古籏一浩氏がずっとVer1.0.3版をベースにしたリファランスを掲載していましたが(1.)、新たに Ver1.2.1 や UI を取り上げた Web 頁を作成したことでしょう。(2.)

なお、私はリファランスには興味がなく、個人的なお勧めは、1.及び2.よりも IBM のWebサイト(5.)です。また3.の開発者向けメモは日本語化の努力成果として大いに歓迎すべきでしょう。

  1. jQuery リファレンス
  2. jQuery入門 (ver 1.2.1)(Last update:2007/10/4 AM 1:00)
  3. jQuery 開発者向けメモ
  4. jQueryの基礎知識 - Emotional Web
  5. jQuery を使って Ajax 開発を単純化する
  6. ウノウラボ Unoh Labs: JavaScriptライブラリといえば jQuery(入門編)

▲ToTop

jQuery() の挙動を解読する(9) jQuery.map()メソッド解読 upon ver1.3.2──jQuery解読(13)

このエントリイの改訂履歴
  • 初稿:2007/11/14
  • ver1.3.2 対応改訂:2009/3/18

全く知りませんでしたが、「map」とはプログラミング界では一般的な専門用語のようです。ある関数の引数として別の関数を受け取る関数を「高階関数(higher-order function)」と呼ぶらしい。その結果、ある配列の各要素を対象として順番にそれを操作した結果を返す関数を作ることが出来る、ということらしい。

C言語やPerlなどでは一般的なようですが、Javascriptの場合実装されてないため、each()メソッドと同様に、フレームワークで色々と工夫されている模様です。

cf. こちらに数少ないJavascriptの高階関数の説明があります。
第2回 JavaScriptの関数をマスターしよう - @IT

$.map()の例

まずjQuery.js 1.1.4の解説にあった例題を掲載します。

  • $.map( [0,1,2], function(i){ return i + 4; }); //result [4, 5, 6]
  • $.map( [0,1,2], function(i){ return i > 0 ? i + 1 : null; }); // result [2, 3]
  • $.map( [0,1,2], function(i){ return [ i, i + 1 ]; }); //result [0, 1, 1, 2, 2, 3]

これらの例によって、$.map()を使って、単純に配列要素に加算したり、条件づけて配列要素数さえ増減させたり、自在に加工出来ることがよく分かります。

そこで$.map()の定義を踏まえて独自のサンプルを作ってみます。

$.mapの定義から、引数となる関数には2つの引数(元の配列の各要素とインクリメント値)を渡していますので、jQuery.js1.1.4のサンプルではなかった2つの引数を取る場合を作成してみました。

▲ToTop

$.map() メソッド解読

このエントリイのjQuery.js 1.3.2における対象箇所
1146: map: function( elems, callback ) {
1147:  var ret = [];
1148:
1149:  // Go through the array, translating each of the items to their
1150:  // new value (or values).
1151:  for ( var i = 0, length = elems.length; i < length; i++ ) {
1152:   var value = callback( elems[ i ], i );
1153:
1154:   if ( value != null )
1155:    ret[ ret.length ] = value;
1156:  }
1157:
1158:  return ret.concat.apply( [], ret );
1159: }
1160:

map() メソッドの要所は 1152 行の var value = callback( elems[ i ], i ); にあります。map メソッドの第二引数である関数 callback の第一引数に、mapメソッドの第一引数である配列の各要素を渡し、その演算結果を value に代入し、この value を新たな配列 ret の要素として取込んでいます。これを元の配列要素数だけ繰り返した上で、作成された配列 ret を map メソッドの返値として返しています。

map() にはもう 1 つ注目すべき箇所があります。それは 1158 行の return 値です。

単に return ret ではなく何故 return ret.concat.apply( [], ret ) とするのか、その理由を以下で考え解明します。

実は ver 1.2.6 では該当箇所は次のようになっていました。

▼jquery.js ver 1.2.6
1187:    ret = ret.concat( value );
1188:  }
1189:
1190:  return ret;

concat() を使うことにより、value が配列の時には要素に分解してから ret に取込み最後に ret を返しています。

一方、1.3.2 では まず ret に単純に value を取込んでから、要素の取り込みが終わった配列 ret に対して concat 処理を施して、配列内配列をシリアライズしています。

▼jquery.js ver 1.3.2
1155:    ret[ ret.length ] = value;
1156:  }
1157:
1158:  return ret.concat.apply( [], ret );

どうして jquery.js ver 1.3.2 で #1155-1158 が変更されたのか、最初は疑問に思いました。そこで原点に返って concat メソッド自体を復習することにしました。

▲ToTop

concat() メソッドについて

少し脱線しますが、concat() メソッドの基本を振り返ります。

concat は或る配列に新たな要素を追加するメソッドですが、追加される要素自身が配列である場合には、その配列を要素に分解してから、元の配列の要素として追加します。いわば線形化するわけです。

例題を作ってそれを通じて学習を確実にしたいと思います。

■ 例 1
 var ary =[1,[5,8],4,9];
 ary.concat(10,[15,80]) //return: [1, [5, 8], 4, 9, 10, 15, 80]
 /* 組み込み元の配列 ary の要素配列 [5,8] は concat 後もそのまま
   ですが、引数の配列 [15,80] は、シリアライズされ、15,18 と分解されて
   配列 ary に取込まれています。
 */

しかし、次のように引数の配列内に更に配列が内包されている場合には、それはシリアライズされません。

■ 例 2
 var ary =[1,[5,8],4,9];
 ary.concat( 10, [15, [ "test", "sample" ] ] );
   //return: [ 1, [5, 8], 4, 9, 10, 15, ["test", "sample"] ]
 /* 配列 ary の要素である配列 [5,8] は concat 後もそのままですが、
   第 2 引数の配列 [15,["test","sample"]] は、外側だけシリアライズされ、
   15, ["test", "sample"] となりますが、["test", "sample"] は分解されません。
 */

▲ToTop

ary2.concat.apply(ary1,ary2) メソッドについて

map() メソッドの 1158 行 return ret.concat.apply( [], ret ) を分析するために、幾つかの例をチェックしてみます。なお、チェックには Firefox 3.0.7、Firebug 1.3.3 を使いました。

以下の 4 つの微妙に異なる concat メソッドの結果を比べると幾つかのことが分かります。

■ 例 3
 var ary =[1,[5,8],4,9];
 var ret =["昨日",["今日",500]];
 ary.concat.apply(ret,ary)    //["昨日", ["今日", 500], 1, 5, 8, 4, 9]
 ret.concat.apply(ret,ary)    //["昨日", ["今日", 500], 1, 5, 8, 4, 9]
 ary.concat.apply(ary,ret)    //[1, [5, 8], 4, 9, "昨日", "今日", 500]
 ret.concat.apply(ary,ret)    //[1, [5, 8], 4, 9, "昨日", "今日", 500]
  • concat.apply(a,b) メソッドは、第 2 引数配列だけを要素に分解する。
  • 結果配列の要素順は、apply メソッドの引数順となる。
  • 結果配列の要素順は、concatを a,b どちらの配列のメソッドとして作用させるかに無関係である。

▲ToTop

改めて map() メソッドの #1155-1158 について

以上を踏まえて map() メソッドの #1155-1158 を解明します。

▼jquery.js ver 1.3.2
1155:    ret[ ret.length ] = value; // ここでは単なる代入だけを行う。
1156:  }
1157:  // ここで一回だけconcat()を使用し、ret 配列内の配列を分解し ret
     // を更新する。ここに [].concat.apply( [], ret ) でも全く同様の
     // 結果が取得できる。しかし、concat.apply( [], ret ) ではエラーとなる。
1158:  return ret.concat.apply( [], ret );

▼jquery.js ver 1.2.6
       // イテレートの度に concat() メソッドを起動している。
1187:    ret = ret.concat( value );
1188:  }
1189:
1190:  return ret;

要点は以下の点です。単なる代入よりも時間を要する cancat() メソッドをイテレートする回数だけ起動する( ver 1.2.6 )よりも、最後に 1 回だけ concat() を起動する( ver 1.3.2 )方が全体の処理時間が短くなる。

Firebug を使って本当に処理時間が変わるのかを調査し、確認出来た

以下のサンプル結果により、concat() 適用回数を減らすことにより明らかに処理時間が短いことが分かります。

このサンプルは5000回loopさせながら、同一の 2 つの配列を作るものです。変数定義時点からタイマーをスタートさせ、コンソール出力させてからタイマーを止め、その間の所用時間を計測しました。

▼毎回 concat() 適用の場合
console.time("Timers")
var i = 0, ary = [], ret = [];
while ( i < 5000 ){
    ary.push( i );
    ret = ret.concat( ary[i] );
    i++;
}
console.log(ret);
console.timeEnd("Timers"); // 計測結果 Timers: 1035ms

▼最後に一回だけ concat() 適用の場合
console.time("Timers2")
var i=0,ary=[],ret=[];
while ( i < 5000 ){
    ary.push( i );
    ret[i] = ary[i];
    i++;
}
    ret.concat.apply([], ret)
console.log(ret);
console.timeEnd("Timers2"); // 計測結果 Timers2: 669ms 半分以下で完了!

jQuery() の挙動を解読する(8) jQuery.find() クラスメソッド解読──jQuery解読(12)

  • 初稿:2007/11/14
  • 改訂:2007/11/21……jQuery.js の挙動を FureBug により追跡し、表にまとめた。
  • 改訂:2008/02/03……抜本改訂(上の表は削除した)

jQuery.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() の構造と各種サブルーチンメソッドの役割

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 に該当するエレメント等を検索することになります。

jQuery.find() メソッド全行解読

長大なコードなので内容に応じて適宜分節しながら解読を進めます。

1. 初期処理
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:
2. 検索開始・前処理:(1460-1469)
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:
3-1. 検索ケースA: 子要素 "> chars" 検索(1470-1487)
 /* "> 任意の文字列"形式による検索を行う箇所である。
  * つまり子要素指定が有る場合の抽出作業である。
  * 但し、検索対象文字列 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、つまり検索結果ありとする。
3-2. 検索ケースB: 子要素 "> [0-9A-z_]" 検索(1488-1521)

ケース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:
4. 検索C:子要素検索ではない検索

以上の検索に引っかからない場合(つまり子要素を探すのではない場合)、次の検索処理が 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:   }
4-1. 先頭文字がカンマである場合の検索前処理(1527-1539)

まず 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:

次の検索処理は、いくつかの前処理が行われてから行われる。

4-2-1. "nodeName#idName" 検索の場合の検索前処理
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:
4-2-2. 先頭文字が "." か "#" の場合の検索前処理
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:
4-2-3. 4-2-1及び4-2-2の場合の更なる検索前処理
1556:     m[2] = m[2].replace(/\\/g, ""); //chars文字列内から \ を削除
1557:
1558:     var elem = ret[ret.length-1]; //elem に検索対象要素配列の最後の要素を代入
1559:
4-3. やっと検索開始
 /* 検索文字配列 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] : [];
4-4. 全ての子孫要素を対象とした検索
 // 全ての子孫要素 (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:
4-5. class 名による検索
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:
4-6. ID名による検索
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:
4-7. この分節での検索終了措置
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:
5. 最後の検索処理

子要素でも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:
6. find() クラスメソッドの終了処理
 // ここまで来ても 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: },
return 連鎖がもたらす階層性

上述の第二のケースの場合、65行により新たなjQuery(context)インスタンスが生成され、上述の一連の流れを経てinit()メソッドが終了します。この流れではjQuery()インスタンスが二度作成されるため複雑な動きをします。

そこでインタープリターがどの様に各種メソッドを呼び出しているのかを Firebug を使って追跡・解読してみました。解読結果を以下に図示します。

なお、ここで行った追跡は、次のようなセレクタとメソッドを指定して、ローカルパソコン上のHTMLファイル上で行いました。追跡に使ったセレクタとメソッドは jQuery("p > a").size()です。

  1. jQuery.js がインクルードされるとグローバル変数 jQuery 及び $ 並びにこれら の prototype オブジェクトが生成され、定義されている各種 prototype プロパティが設定される。こうしてinit()メソッドの返値が配列であることも設定される。
  2. ユーザーが jQuery("p > a") の実行をインタープリタに指示する。ここに第一引数の expr は「<・・・>付きの html タグ名でもなく、# 付きの id 名称だけでもない文字列」となる。
  3. 指示されたインタープリタは jQuery()関数の定義に従い、(1)new 演算子を起動して空オブジェクトを生成し、それへの参照をキーワード this に与え、jQuery.prototype.init("p > a").prototypeを生成するする。
  4. ここに 既に 526 行(jQuery.prototype.init.prototype = jQuery.prototype;)により init.prptotype は jQuery.prototype に等しくなっているから、jQuery.prototype.init("p > a").prototype は jQuery.prototype.init("p > a") の実行となる。
  5. init("p > a")は jQuery("p > a") は init("p > a") の返値を受け取り、かつ 1. により init() の返値は配列とされているから jQuery("p > a") のインスタンス(=this)の型も配列となる。
  6. 指示を受けたインタープリタは、 jQuery("p > a") メソッドを実行し、jQuery.js に記述されているコードに従って、return new jQuery( context ).find("p > a"); を実行しようとする。
    しかし、context は与えられていないから、新たに init() コンストラクタが呼び出され、新たなインスタンスの生成が始まる。この時 this の参照先はそれまでの参照先を捨て init( context )コンストラクタによるインスタンスとなる。
  7. init()コンストラクタの引数は39行から documentとなり、42-45行により init()には[document]が返される。
  8. this.setArray(context) が実行されると、jQuery(context) インスタンスは context を唯一の要素とする配列 [context] を生成し、return this.setArray(context) によって当該配列が init(expr,context) メソッドの返値となる。
  9. ところで、この init(expr,context) は return this.init(expr,context) 行より呼び出されたのであるから、当該init()メソッドの返値は、jQuery( context ).find( expr ) コンストラクタに返される。
  10. かくして、やっと .find(expr) メソッドが開始される。

jQuery() の挙動を解読する(7) jQuery().find()メソッド解読(start)──jQuery解読(11)

  • 初稿:2007/11/14
  • 改訂:2008/02/03……抜本改訂

jQuery().find() が登場する箇所

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) ) の時、つまり、

  1. (1)selector が文字列型で(48行)、
  2. (2)31行で定義された quiclExpr にマッチし、
  3. かつ (3)m[1] が存在し、
  4. 更に(4)context が付与されていない場合において、
  5. (5)selector 文字列が "#id" の形式の場合に、
jQuery().find( selector ) として登場します。(68行)

第二は文字列 selector が "・・<・・・>・・" でもなく、また "#id" でもない場合の new jQuery(context).find(selector) です(84行)。

どちらにも共通していることは、jQuery() 関数を再帰呼び出しすることであり、異なる点は new 演算子の有無です。どちらの場合においても jQuery 関数を再帰呼び出しするのですから、その定義から新しいインスタンスが作成されることに相違はありません。では何故 new 演算子の有無の別があるのか、興味深いところですが解明できていません。

new 演算子が有る jQuery 再帰呼び出しの場合には、二重に new 演算子が作用してしまうのではないかと考えますが、本当にそうなるのか、またその作用の意味は何か、解明できていません。

init() メソッドから呼び出される jQuery(context).find(selector) はどう動くのか

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) インスタンスメソッドの挙動を概観しておきます。

1. jQuery(context).find(selector) インスタンスメソッドの挙動概観
  1. jQuery.map(this,fn(elem){}) メソッド起動
  2. context 内の各々の要素 elem を対象として jQuery.find( selector, elem ) 実行
  3. そのリターン値を elems に代入
  4. 次に selector 文字列に対して、(1) "a b"( + でもなく>でもない a 及び b (共に1文字)の間にスペースがある文字列)があるかどうか、あるいは、(2) ".." を含む文字列かどうかをチェック
  5. この検証にヒットすれば jQuery.unique( elems ) を起動してその結果を返す。
  6. ヒットしなければ elems を返す。

こうして context 内の selector に適合する要素が jQuery.find( selector, context ) クラスメソッドによって抽出され、これが jQuery( context ).find( selector ) に return されます。

2. $("#id") → $().find("#id") 呼び出しの場合の挙動
  1. new 演算子がない $().find("#id") が呼び出されると、$() 呼び出しによって直ぐに 19 行で新たなインスタンスが生成され init() が起動されます。すると $ に引数はありませんから、init() 関数の第一引数には document が代入され、インスタンスは 45 行から [document] となりこれが 19 行から $() に return されます。
  2. 次に、$().find("#id") つまり [document].find("#id") メソッドから呼び出される 285 行の map() メソッドにおいて、this は [document] を指し示し、第二引数の elem は map() メソッドの定義から document 内の個々の要素エレメントを意味します。こうして document 内の各要素に対して 286 行のクラスメソッド $.find( selector, elem ) が適用されます。ここで第一引数には当然 "#id" が渡されます。つまり、
    var elems = jQuery.map([document], function(elem){ return jQuery.find("#id", elem) })
    です。
  3. elems 取得後には、 "#id" 文字列に対して、(1) "a b"( a も b も + でもなく>でもない1文字で間にスペースがある文字列 )があるかどうか、あるいは(2) ".." を含む文字列かどうかがチェックされます。
  4. この検証にヒットすれば jQuery.unique( elems ) を起動してその結果が返されますが、 "#id" 文字列の場合その内容からこの検証にはヒットしません。
  5. こうして "#id" に適合する要素 elems が jQuery.find( "#id", [document][i] ) クラスメソッドによって抽出され、これが $().find("#id") に return され、更に $("id") に返されて目的が達成されます。
3. new jQuery(context).find(selector) 呼び出しの場合の挙動

これについては既に 1. で述べたことと全く同一になるので省略します。

次のエントリイでは...

jQuery.find() クラスメソッドを解読します。

jQuery()の挙動を解読する(6) jQuery.clean()メソッド解読──jQuery解読(10)

このエントリイの改訂履歴
  • 初稿:2007/11/13
  • 抜本改定:2009/3/20-23……jquery 1.3.2 対応改訂並びに新たに clean() メソッドコード全文解読を履行

直前のエントリイにおいて、何とか jQuery.each() メソッドを解明できましたので、次に clean( elems, context, fragment ) メソッドにチャレンジします。

まず、このメソッドが jquery.js 内で呼び出されるのは次の 3 箇所だけです。

  1. ユーザーが入力した jQuery(s) において、引数 s 文字列が html 文字列であった場合に init() メソッドから ( #57 )

    ここに html 文字列とは、<> で囲われた whitespace を含む何らかの文字列で、例えば<a href="hoge.html">を意味しています。

  2. clone() メソッドから ( #320 )
  3. domMaip() メソッドから ( #517 )

これら全てにおいて、clean() メソッドは、DOM を走査する前に、引数 elems 配列内の html 文字列を正しい表現に修正したり、IE バグ対策を施したりする役割を担っています。名にし負う「 html 文字列クリーナー 」です。

更に 3. のケースだけ fragment 引数が使われており、単なる html 文字列整形だけではなく、文書断片( =Document Fragment )を活用して、当該文字列内に埋め込まれたスクリプトを実行することまでやってのけます。

さて、clean()メソッドはかなり長大で難解です。以下に各行への解説を付けた cleaner() コード全行を掲載しますが、解読には多くの時間を要しました。

このエントリイが対象とする jquery.js 1.3.2 における箇所
■ jQuery.clean() メソッド
847:clean: function( elems, context, fragment ) {
848: context = context || document;
849:// ▼第 1 ブロック--------------------------------------------------------
850: // !context.createElement fails in IE with an error but returns typeof 'object'
   // IE は document ノードにしか要素を作れず、context が document 以外
   // の時にエラーを発生させてしまう。当該エラー処理のために #851-852がある。
851: if ( typeof context.createElement === "undefined" )
    // 要は context に document オブジェクト を代入する。上記原典コメ
    // ントにあるように、このフレーズは IE のみが通過する。他のブラウザ
    // はこのフレーズはパスして 848 の後には 856 行が適用される。
852:  context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
853:// ▼第 2 ブロック--------------------------------------------------------
854: // If a single string is passed in and it's a single tag
855: // just do a createElement and skip the rest
856: if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) {
857:  var match = /^<(\w+)\s*\/?>$/.exec(elems[0]);
858:  if ( match ) // 1つだけ tag がある場合 ex. "<img src='xx.jpg' />";
     // match に代入された文字列から context 内に Element Node を作って配列に入れる。
     // 返値は作成されたノードへの参照を要素とする配列になる。
859:   return [ context.createElement( match[1] ) ];
860: }
  // ▼第 3 ブロック--------------------------------------------------------
861: // 以後の処理をするために、変数 div は新規に作成した div 要素ノードを参照する
862: var ret = [], scripts = [], div = context.createElement("div");
863:// ▼第 3-1 ブロック------------------------------------------------------
864: jQuery.each(elems, function(i, elem){ // elems の要素を巡回処理する
865:  if ( typeof elem === "number" )
866:   elem += ''; // 数値ならば文字列化する
867:// ▼第 3-2 ブロック------------------------------------------------------
868:  if ( !elem ) // なければ、あるいはなくなれば
869:   return;   // 何もしないでコード進行を終える。
870:// ▼第 3-3 ブロック------------------------------------------------------
871:  // Convert html string into DOM nodes  文字列をノードに変換する
872:  if ( typeof elem === "string" ) {
  // ▼第 3-3-1 ブロック----------------------------------------------------
873:   // Fix "XHTML"-style tags in all browsers
874:   elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
875:    return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
876:     all : // 終了タグを持たないタグ名の時にはヒットした文字列をそのまま返し、
       // 終了タグを持つタグ名の場合には、開始タグ内にある / を除いてから開始
       // タグを閉じ、その後に終了タグを付加する。
877:     front + "></" + tag + ">"; // ex. <div /> → <div ></div>
878:   });
879:// ▼第 3-3-2 ブロック----------------------------------------------------
880:   // Trim whitespace, otherwise indexOf won't work as expected
     // ホワイトスペースがあると indexOf メソッドが期待通りに
     // 作動しないので、冒頭の witespace を削除して最初から 10 文字を切り取り
     // 全て小文字にする。★しかし何故10文字? 最長のタグ名が 10 文字か?
881:   var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase();
882:   // 以下の !tags.indexOf(str) は tags の先頭に str がある場合のみ true となる
883:   var wrap = // elem を囲む文字列を準備する。
884:    // option or optgroup 
885:    !tags.indexOf("<opt") &&
886:    [ 1, "<select multiple='multiple'>", "" ] ||
887:
888:    !tags.indexOf("<leg") &&
889:    [ 1, "<fieldset>", "</fieldset>" ] ||
890:
891:    tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
892:    [ 1, "<table>", "</table>" ] ||
893:
894:    !tags.indexOf("<tr") &&
895:    [ 2, "<table><tbody>", "</tbody></table>" ] ||
896:
897:    // <thead> matched above
      // tags 文字列内に <td かも <th がある場合
898:    (!tags.indexOf("<td") || !tags.indexOf("<th")) &&
899:    [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||
900:
901:    !tags.indexOf("<col") &&
902:    [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||
903:
904:    // IE can't serialize <link> and <script> tags normally
905:    !jQuery.support.htmlSerialize &&
906:    [ 1, "div<div>", "</div>" ] ||
907:
908:    [ 0, "", "" ];
909:
910:   // Go to html and back, then peel off extra wrappers
     // wrap 配列の 2 番目と 3 番目の要素で elem を挟み込んで div 内に代入する。
911:   div.innerHTML = wrap[1] + elem + wrap[2];
912:
913:   // Move to the right depth
914:   while ( wrap[0]-- ) // wrap[0] が 1 以上の間は 1 になるまで
915:    div = div.lastChild; // 最後の childNode を div に代入する。
916:
917:   // Remove IE's autoinserted <tbody> from table fragments
     // IE は自動的に tbody タグを挿入してしまうのでこれを取り除く処理
918:   if ( !jQuery.support.tbody ) {
919:// ▼第 3-3-3 ブロック----------------------------------------------------
920:    // String was a <table>, *may* have spurious <tbody>
      // 偽の tbody タグを調査するため、elem 内に <tbody があるかどうか走査
921:    var hasBody = /<tbody/i.test(elem), // tbody 有無を調べる
       // table タグが tags の先頭にあり、elem 内に tbody がなければ
922:     tbody = !tags.indexOf("<table") && !hasBody ?
        // div に第一子があればその childeNodes を tbody に代入
923:      div.firstChild && div.firstChild.childNodes :
924:
925:     // String was a bare <thead> or <tfoot>
       // #925-928 は tags 内に table タグがある場合の
       // 処理である。#922-928 内で 3 項演算子が入れ子になっている。
       // table タグだけがあって tbody がなければ
926:     wrap[1] == "<table>" && !hasBody ?
927:      div.childNodes : // childNodes を tbody に代入し、
928:      []; // さもなくば空配列を tbody に代入する。
929:    // 以上で処理した tbody を巡回処理する。
930:    for ( var j = tbody.length - 1; j >= 0 ; --j )
       // tbody[j] が tbody ノードで子ノードを持たなければ
931:     if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
        // tbody[j] の親ノードから tbody[j] 子ノードを削除する。
        // つまり IE が勝手に作った空の tbody ノードを削るのである。
932:      tbody[ j ].parentNode.removeChild( tbody[ j ] );
933:
934:    } // End of #918 if
935:// ▼第 3-3-4 ブロック----------------------------------------------------
936:   // IE completely kills leading whitespace when innerHTML is used
     // IE で elem の先頭文字が半角スペースなどの whitespace の場合
937:   if ( !jQuery.support.leadingWhitespace && /^\s/.test( elem ) )
      // elem 文字列の 最初の ゼロ個以上の whitespace を、div の最初の
      // 子ノードの前に挿入する。ここで使われている insertBefore は
      // jQuery のインスタンスメソッドではなく native な DOM エレ
      // メントメソッドである。
938:    div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
  // ▼第 3-3-5 ブロック----------------------------------------------------
939:   // div の子ノード群を配列の要素に入れ、できあがった配列を elem に返す。
940:   elem = jQuery.makeArray( div.childNodes );
941:  } // End of #872 if(長~い if 文がここで終わる。)
942:// ▼第 3-4 ブロック------------------------------------------------------
943:  if ( elem.nodeType ) // elem が何らかの DOM ノードの場合
944:   ret.push( elem ); // 配列 ret の要素として elem を追加
  // ▼第 3-5 ブロック------------------------------------------------------
945:  else // elem DOM ノードでない場合
     // elem が文字列の場合には #872-941 において一連の処理が施され、#940
     // で elem は配列となっている。
946:   ret = jQuery.merge( ret, elem ); // 配列 ret に配列 elem を合体する。
947:
948: });
  // ▼第 4 ブロック----------------------------------------------------------
949: // やっと fragment がある場合の処理が始まる。
   // なおこのメソッド内には createDocumentFragment() メソッドは存在しないので、
   // fragment を使う場合には、別途、cleanメソッドに先だって、 fragmentNodeを
   // 作っておかなければならない。domManip() メソッドにその実例がある。
950: if ( fragment ) {
951:  for ( var i = 0; ret[i]; i++ ) { // 配列 ret を巡回処理する
     // #952-953 のコードは配列 elems の要素として存在するscript タグを
     // 取り出すとと共に当該の script タグを削除するためのものである。
     // 処理対象のノード名が script でその type 属性がないか、または "text/javascript" の場合には
952:   if ( jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
      // 処理対象に親ノードがあれば、当該親ノードから処理対象ノードを
      // 除去した上で、その除去ノードを scripts 配列に追加する。
      // 親ノードがない場合には単純に処理対象を scripts 配列に追加する。
      // ここに親ノードがない場合とは、処理対象がスクリプトタグのみから
      // 構成されている場合である。
953:    scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
954:   } else {
955:    if ( ret[i].nodeType === 1 ) // 処理対象が要素ノードならば
       // 処理対象内にある script タグを取得して配列に入れ、その配列を
       // [i+1,0] 配列に結合する。こうして出来た splice メソッドの引数を使って
       // ret 配列の i+1 位置から、script タグの配列要素を ret 配列に追加する。
956:     ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
      // 別途作成済みの fragment ノードの最後の子ノードとして、
      // 処理対象を追加する。なお、この fragment ノードは clean メソッド
      // からは返されないので、利用するには別途 fragment ノードを呼び出さ
      // なければならない。
957:    fragment.appendChild( ret[i] );
958:   }
959:  }
960:  
961:  return scripts; // 整形された要素を持つ配列 scripts を返す。
962: } // End of #950 if
963:// ▼第 5 ブロック--------------------------------------------------------
964: return ret; // 配列 ret を返す。
965:},

clean(elems, context, fragment) メソッドの構成

clean メソッドの引数は順に、(1) 要素ノードを指し示す html 文字列を格納した配列、(2)コンテキスト、(3)フラグメントノード。返値は配列で、コードの構成は以下のようになっています。

ここに、番号は上のコード説明内に設けた▼で始まる番号に符合します。

    ※ 以下において、elem は 配列 elems の各要素を表す
  1. IE の createElementメソッドバグ対策……#850-852
  2. 第 3 引数がなくて、html 文字列が 1 つだけのタグで構成されている場合への対応……#854-860
  3. 配列 elems の要素に対する巡回処理(このメソッドの肝部分)……#864-948
    1. elem が数値の場合の処理……#865-866
    2. elem が空文字の場合の処理……#868-869
    3. elem が html 文字列の時に、それを DOM ノードに変換する処理
       この部分こそが clean 処理の核心部分 ……#871-941
      1. elem をXHTML表記に変換する……#873-878
      2. ホワイトスペースの削除などを処理する……#880-915
      3. IE の tbody タグに係るバグ対策……#917-934
      4. IE の innerHTML メソッドのバグ対策……#936-938
      5. 正規化され、DOMノードに変換された文字列を elem に代入する……#940
    4. elem がノードの時に返値を作る……#943-944
    5. elem がノードではない時の返値を作る……#945-946
  4. 第 3 引数がある(flagment ノードを作成する)場合……#950-962
  5. 最終処理(修正を施した配列を return する)……#964

学習その 1 ── str.replace(RexExp,function())……#874-878

clean() メソッド内には、replace メソッドが引数に関数を取る例が出てきます。

これまでは string.replace(regexp, newString) しか知らなかったのです。関数を指定できるようになったのは Javascript1.3 以降だそうですから、決して最近のことではないのですが、関数を使った replace の例はこれまで見たことがなかったのです。

さて書籍等に拠ればこの場合の関数の引数は、順に「ヒットした文字列」、「最初の部分文字列」、「2番目の部分文字列」、・・・となるようです。

正規表現文字列「/(<(\w+)[^>]*?)/>/g」によって elem 内でヒットした文字列が第1引数 all に代入され、第 2 引数 front には第1部分文字列である <(\w+)[^>]*? (all から末尾の2文字 "/>" を省いた文字列)が、また tag には 第2部分文字列である \w+ (タグ名になる)が、それぞれ代入されることになります。

例えば elem が"<img src='test' alt='test' />"である場合には all="<img src='test' alt='test' />"、front="<img src='test' alt='test' "、tag="img" と順に代入されます。

なお、この置換メソッドは不正なXHTML構文を正す役割を負っています。ユーザーによって終了タグを記述しない誤記があった場合には、自動的に終了タグを挿入するのです。しかし同時に、終了タグがない要素名であって / が付いていないタグ記述に対して、それを付与する機能は持っていません。

▲ToTop

学習その 2 ── 空白文字の削除と div エレメントの生成……

jQuery.trim()───(911-913行)これは trim の名から推測されるとおり不要な空白文字列を削除するメソッドです。正確に言うとjQuery.trim(arg)メソッドは、arg文字列の先頭または末尾に存在する1個以上の空白文字列を削除し、途中にある空白文字は削除しません。引数tがなかった場合にエラーにならぬよう、(t||"")をreplace対象文字列としています。これによりarg文字列の先頭又は末尾にあるホワイトスペースが削除され、変数 s にその結果が代入されます。

このように空白文字を削除するのは、直後に生成されるdivエレメントと併せて、この後に続く"入力値に対する次なるエラーチェック"を行うためです。

学習その 3 ── 誤ったタグ記述を正しく修正する……#880-915

clean メソッドのの 1 つの神髄がここにあります。誤ったタグ記述を正しく修正する為のコードがvar 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 に代入しています。

トリッキーなコードの第二は、&& 演算子及び || 演算子の使い方です。var wrap への代入値は、連続する || 演算子によってその前後にあるいずれか一つの値となりますが、そのいずれかを選択するのは && の前に置かれたコードです。&& の前に置かれた式の値が true の時だけ && の右の値が取得されるからです。

因みにここで行われることを馴染みのコードで書けば、極めて冗長ですが次のようになります。

&& と || の2つの演算子を巧みに組み合わせて条件分岐を行っているわけで switch 文を代行していることになります。この記述方法は自作コード内でも極めて重宝します。

第三のトリッキーコードは極めつけです。915 行の div=div.lastChild がタグの入力ミスを修正する役割を負っているのです。

どういうことかと言うと、wrap に代入された配列と elem の値を組み合わせて、コード内で生成した div タグ内に wrap[1] + elem + wrap[2] を代入していますが、これは例えば<table><tbody></tbody><colgroup><col></col></colgroup></table>のような文字列になります(elem が <col を含んでいた場合)。そして914-915行によって、生成した div タグ内に <col></col> を代入しています。

これだけ見ると元の値を返して一体何をしているのか?、と疑問に思いますが、実はこの一連のコードは入力ミスを修正してくれる機能を持っているのです。例えばjQuery("<col>abc</tr>").*** のようにタイプミスをしたとします。これがclean()メソッドに掛けられて<col>abc</tr>の部分が<col>abc</col>と修正されるのです。

どうしてこのような修正が行われるかと言えば、lastChild(別に firstChild でも構わない)プロパティの効能です。一端ラッピングされたタグ文字列から DOM のプロパティを利用することによってエラーを修正しているのです。この方法には驚きました。そんな効能が DOM ツリー検索機能に存在していることなど、全く知らなかったからです。

とにかくこれらのトリッキーなコードを重ねて、ユーザーによるタグ標記のエラー訂正を行っているわけです。

▲ToTop

学習その 4 ── IE バグ対策……

cleanメソッド内にもまた、IE バグ対策が盛りだくさんあります。列挙すれば

  1. document ノード以外のノードにはノードが作れない(#850-852)
  2. tbody 強制挿入対策(#917-934)
  3. innerHTML メソッドによる whitespace の除去対策(#936-938)

以上のとおりです。

ここではまず、2番目のバグ対策=IEによって自動挿入されてしまうことがある tbody 空要素 を削除している部分について学習します。

まず、ユーザーが入力した標記文字列の先頭に <table が含まれ、かつ当該タグ標記文字列内に <tbody が含まれなかったら、tbody に div の孫ノード全てを代入し(921-923行)、elem を囲むタグが table だけで、elem に tbody がなければ、tbody に div の子ノードを代入しています。(9256-928行)つまり table タグの子要素を抽出しています。

次に、tbノードの要素数を頼りにその最後から順に「子ノードを持たない tbody ノード」を抽出し(931行)、当該tbodyノードを削除しています。(931-832行)

以上により IE が勝手に挿入する tbody タグ(それは子要素を持たない)を削除することが出来るわけです。

次に、3 番目の空白文字の扱いです。elem の最初に空白文字がある場合に(835行)、その空白文字列を取りだして、補正されたelem つまり div.firstChild の前に挿入しています。(936-938行)

▲ToTop

学習その 5 ─── script の実行

clean() メソッドの第 3 引数 「 fragment 」 は ver 1.3.x で初めて登場しました。1.2.6 まではこのメソッドは 2 つの引数しか取りませんでした。この fragment 引数は一体何をするために設けられたのか?───それがなかなか解明できませんでした。今でも完全に解明しきったとは思えません。まが霧が完全には晴れないからです。

それでも、コードを睨み続け、サンプルを幾つか試してみて分かったことがあります。

新たに追加する html 文字列内にスクリプトを埋め込んで、fragment 引数つきで clean() メソッドを実行すると興味深いことが起こるのです。

くだくだ説明するよりも、実例を示すのが手っ取り早いでしょう。

下のボタンをクリックすると 2 つの変化が起こります。或る文章がボタンの前に挿入され(文字列を内包する p タグです)、同時に画面中央に alert 表示が出ます。

ここで行ったことは以下の通りです。

上のボタンがクリックされると、このページに埋め込まれた或る Javascript コードが起動します。そのコードでは jQuery().before() メソッドを使って、このボタンの前に兄弟要素 p タグとスクリプトタグを挿入します。

このとき、jQuery().before() メソッドはその定義により、domManip メソッドを呼び出し、その中では cleanメソッドが fragment 引き数付きで起動されます。この結果、挿入する html 文に記述されているスクリプトタグは挿入された直後に削除され、かつ実行されるのです。こうして alert 表示が実行されます。

なお、以上を実現する Javascript コードでは name エンティティに悩まされました。フゥ~(;´_`;)

jQuery.clean()メソッド全体の意味

途中で Javascriptの学習を深めながら、延々と jQuery.clean() メソッドの解読を続けてきました。何とかその全容を解き明かすことが出来たと思っていますが、最後に、init()メソッドから呼び出された jQuery.clean() メソッドが果たす役割についてまとめておきます。

それは第一に、ユーザーが記述したタグ名(子孫を持つ場合を含む)の整合性をチェックし、もし記述にミスがあればそれを修復します。開始タグと終了タグがきちんと対になっているかを調べます。

第二に、IE の各種バグ対策を行います。

第三に、fragment 引数の活用です。これによりスクリプトタグを含むノード管理が出来るようになります。

以上のように、jQuery.clean()メソッドは3つの清掃作業をおこなうメソッドとして定義されていると理解することが出来ます。

jQuery() の挙動を解読する(5) jQuery.each()解読──jQuery解読(9)

このエントリイの改訂履歴
  • 初稿:2007/11/13
  • 改訂:2009/3/12…… ver1.3.2 対応とするために最小限の改訂を施しました。
jquery.js 1.3.2 におけるこのエントリイの対象箇所
■ jQuery.each() メソッドコード
670: // args is for internal usage only
671: each: function( object, callback, args ) {
672:  var name, i = 0, length = object.length;
673:
674:  if ( args ) {
675:   if ( length === undefined ) {
676:    for ( name in object )
677:     if ( callback.apply( object[ name ], args ) === false )
678:      break;
679:   } else
680:    for ( ; i < length; )
681:     if ( callback.apply( object[ i++ ], args ) === false )
682:      break;
683:
684:  // A special, fast, case for the most common use of each
685:  } else {
686:   if ( length === undefined ) {
687:    for ( name in object )
688:     if ( callback.call( object[ name ], name, object[ name ] ) === false )
689:      break;
690:   } else
691:    for ( var value = object[0];
692:     i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
693:  }
694:
695:  return object;
696: },

jQuery.each() はどんなことをするための関数か?

何はともあれjQuery.each() の実例を見て何をするメソッドなのかを理解する必要があるでしょう。

jQuery.each(
 ["white","red","blue"],
 function(i, n){alert(i +"番目は : " + n +"です");}
);

上の例は jQuery.com Web サイトの例題をちょっと改訂したものです。序でに本家の解説も引用しておくと「普遍的な反復処理関数で、オブジェクトと配列の両方で継ぎ目なく反復処理を行う場合に使える。なおこの関数は$().each()とは別物で、何であろうと反復処理する場合に使用出来る。ここでcallback(呼び出)される関数の引数は2つあり、1番目はオブジェクトのキー文字列か配列のインデックスで2番目は値である。」

これを踏まえて対象が配列ではなくオブジェクトの場合の反復関数を作ってみます。

jQuery.each(
 {"a":"white","b":"red","c":"blue"},
 function(i, n){alert(i +" は " + n +" です");}
);

上を実行すると、「a は white です」「b は red です」「c は blue です」と順にalertされます。つまり jQuery.each()は、任意のオブジェクトか配列に対して、その内容を順番に取りだし何らかの処理をさせるための関数です。これは所謂イテレーターと呼ばれる関数で、Javascript そのものに実装されていないことから、それぞれのフレームワークであれこれと定義されているようです。prototype.js にも arrayObject.each(arg) なる形式のイテレーターがあった気がします。

さて、jQuery.each() は最大 3 つの引数( obj, callback, args )を取り、obj が配列であってもオブジェクトであっても、また args があってもなくても、それぞれの要素又はプロパティに対して callback 関数でその各々に対して何らかの処理をするよう定義されています。

▲ToTop

jQuery.each() の解読──概要編

5jQuery.each() は最大3つの引数(obj, fn, args)を取り、第3引数の有無と第1引数が配列であるか否かによって、都合4つのケースに分けて定義されています。

670: // args is for internal usage only
671: each: function( object, callback, args ) {
672:  var name, i = 0, length = object.length;
673:
674:  if ( args ) { // 第3引数argsがある場合
675:   if ( length === undefined ) { // 配列でないならば
676:    for ( name in object )  // オブジェクトを走査して
       // object[ name ]を呼び出し元として、callback
       // 関数を args を引数にして起動する。返値が得られなければ break する。
       // これにより例えば、プロパティ値を args で加工するような callback
       // 関数を定義すれば、プロパティ値の変更が出来る。
677:     if ( callback.apply( object[ name ], args ) === false )
678:      break;
679:   } else
680:    for ( ; i < length; ) // 配列の場合
       // その要素を呼び出し元にして args 引き数付きの callback を実行する。
       // break は上と同様に作用する。
681:     if ( callback.apply( object[ i++ ], args ) === false )
682:      break;
683:
684:  // A special, fast, case for the most common use of each
685:  } else { // こちらの方が一般的な使われ方である。
686:   if ( length === undefined ) { // オブジェクトならば
687:    for ( name in object ) // オブジェクト走査
       // プロパティ値を呼び出し元に、プロパティ名とプロパティ値を引数とする
       // callback 関数を実行する。break は上と同様。
688:     if ( callback.call( object[ name ], name, object[ name ] ) === false )
689:      break;
690:   } else
691:    for ( var value = object[0]; // 配列走査
       // 配列要素を呼び出し元にして、インデックスと要素を引数とする
       // callback 関数を実行する。break は上と同様。
692:     i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
693:  }
694:
695:  return object; // 加工された obj を返す
696: },

▲ToTop

jQuery.each()の解読──学習編

apply()メソッドの活用

jQuery.each() の 4 つのケースでは全て apply() 又は call() メソッドが活用されています。そしてこれらのメソッドは素人には馴染みが薄く、これまできちんと理解していませんでした。

しかし、それらが理解できなければ jQuery.each は全く分かりませんので、必死に学習した結果、今は何とか分かったような気持ちになっています。

fn.apply(obj,args) は obj.fn(args) であり、fn.call(obj,arg1,arg2,・・・) は obj.fn(arg1,arg2,・・・) であることが何とか理解出来、やっと jQuery.each の解読を果たすことが出来ました。

なお、apply の第2引数 args は必ず配列でなければなりませんが、それが callbakck 関数に引数として引き渡される時点では、配列そのものとしてではなく、その要素が取り出されて渡されます。いわば args.join() 適用後の文字列が引数に渡される訳です。

そのことを試してみたのが以下のコードです。alertは 1,2,3,4,this,test と表示されます。

■ arguments プロパティの「展開」をテストするコード
 function doDisp(n){
     alert(n);
 }

 function doTest(){
     doDisp.apply(this,arguments) 
}

 doTest([1,2,[3,4],["this","test"]])
if(・・・) break;文

if ( callback.apply( obj[i], args ) === false ) break;
には戸惑いました。false でなかった場合、つまり break しない場合の処理がどこにも見あたらないからです。「if (expression) statement」の statement がないのです。jQuery の動作結果から判断して false でない場合には、それが for ループ文の出力結果となる、と理解せざるを得ないわけで、「そういうものか」と合点せざるを得ませんでした。

しかし、よくよく考えてみれば、callback.apply( obj[i], args ) の実行結果と false を同値演算子で比較しているということは、まさしく callback.apply( obj[i], args ) は実行されているわけです。

実は、正確に言えば、if 文の statement は break; であり、expression が statement 的な式文となっているのでした。

また、等値演算子ではなく同値演算子を使う理由についても調べる迄は全く理解できませんでした。もしこのケースで等値演算子を利用すると false を 0 に置き換えられてから左辺と比較されるため、この式は常に false となってしまいます。どうしても同値演算子でなければならないわけです。

またしても 1 つ学習しました!

for ループ文の特徴的使い方

for ループ文も特異です。

ここでもまた等値演算子ではなく同値演算子が利用されています。そしてこの for 文は空オブジェクト{}を返していますが、callback.call(val,i,val) !== false; である限り、つまり callback.call(val,i,val) の実行結果がある限り callback.call(val,i,val) を返しています。こうして意味のあるコードとなっている訳でやっと理解は出来ましたが、難解でした。

次に、691 行の for に続くループ条件部分の( )内も不思議の連鎖です。

条件が2つあるように見えるケースに、最初は戸惑いました。勿論 2 つの条件があるのではなく条件は 1 つですがそれが複合的になっているのでした。( i < ol && callback.call(value,i,value) !== false )

最後に増分値が単なるインクリメントではなく式であるケースも初体験です。

ここで使われている for loop 文はまさしく未知との遭遇でした。

---------------------------------------------------

次回は init() メソッド内で呼び出されて重要な役割を果たす jQuery.clean() メソッドを解読する予定です。

▲ToTop

jQuery()の挙動を解読する(4) init()メソッドにおけるjQuery固有の拡張されたプロパティとメソッドの相互呼応──jQuery解読(8)

extend()による拡張方法が分かったので、愈々個別の解読に進みたい。

これまでのエントリイで、(1)jQueryの全体の構造と特徴(2)jQueryグローバル変数の定立(3)コンストラクタ関数による初期化(4)インスタンスプロパティ・メソッドとクラスプロパティ・メソッド及び(5)extend()関数によるオブジェクトの拡張について、解読してきたつもりです。

これから先は、愈々これらの解読によるjQueryの理解を踏まえて先に進みます。基礎的な事項は一通り通過しましたので、愈々個別のコードを解明したい訳です。

But!。全体で2965行に及ぶjQuery.js(ver1.2.1 非圧縮版)の逐一の解読にはおそらく数年は掛かるでしょう。

何故ならば、本業の多忙さ故に毎日分析作業を行える訳でもないし、Javascriptに詳しいシステムエンジニアでもない一素人にとって、約3,000行のJavascriptコードの解読は、おいそれと登坂できてしまう山ではありません。まして大胆かつ無謀にも、Javascript界における最高峰の1つを極めようとしているのですから、一朝一夕の努力で山頂に到達できるはずもありません。

それでも一歩一歩先に、上に進もうと覚悟を決めている昨今ではあります。

▲ToTop

init()メソッドの分析のために

init()関数解読の道半ばであれこれの基本的問題に話題がそれてしまいました。init()関数の大枠については、こちらで詳述しましたので(jQueryインクルード時の挙動を追跡する(2) コンストラクタの初期化:init()──jQuery解読によるJavascript学習(5))、これ以降では各論に入ります。

init()関数の細部を理解するには、その中で呼び出される各種のjQuery固有の拡張されたメソッド及びプロパティのそれぞれを知る必要があります。init()で登場する固有のメソッド等は以下の通りで、僅か50行足らずの中に盛り沢山のメソッド等が登場します。しかもこれらのメソッドは幾重にも重層化されて利用されています。そこでその連関を表にしてみました。jQuery固有のメソッドやプロパティが init()メソッド内だけでも密接に相互に連関している様を、一目で分かるようにするためです。

<init()メソッドにおけるjQuery固有メソッドの連関表>
                呼出し元行 → 呼出され先行
│
├$.fn.init()..................................34-82行
│ ├$.clean().......................44行 → 765-854行
│ │ └$.each()...................769行 → 569-589行
│ │   ├$.trim()...............782行 → 911-913行
│ │   ├$.browser.msie.........820行 → 1017行
│ │   ├$.makeArray()..........840行 → 915-926行
│ │   ├$.nodeName()...........843行 → 505-507行
│ │   └$.merge()..............849行 → 935-950行
│ ├$().find()......................53行 → 225-229行
│ │ ├$.map()....................226行 → 987-1007行
│ │ ├$.find()...................226行 → 1224-1413行
│ │ │ ├$().trim()............1245行 → 911-913行
│ │ │ ├$().data()............1280行 → 511-531行
│ │ │ ├$().trim()............1296行 → 911-913行
│ │ │ ├$().merge()...........1310行 → 935-950行
│ │ │ ├$().isXMLDoc()........1339行 → 485-488行
│ │ │ ├$().merge()...........1362行 → 935-950行
│ │ │ ├$().classFilter().....1367行 → 1415-1424行
│ │ │ ├$().filter()..........1394行 → 258-266行
│ │ │ └$().trim()............1396行 → 911-913行
│ │ ├$().pushStack()............227行 → 102-106行
│ │ └$.unique().................228行 → 952-968行
│ │   └$.data()................957行 → 511-531行
│ ├$.isFunction()..................69行 → 479-482行
│ ├$().ready().....................70行 → 1925-1940行
│ │ ├bindReady()...............1927行 → 1990-2039行
│ │ └$.readyList...............1937行 → 1948行
│ ├$().load()......................70行 → 2041-2101行
│ │ ├$.isFunction()............2042行 → 479-482行
│ │ ├$.param().................2066行 → 2479-2504行
│ │ └$.ajax()..................2073行 → 2194-2422行
│ ├$().setArray()..................72行 → 108-112行
│ └$.makeArray()...................78行 → 915-926行
│

init()メソッド一つだけでも、jQuery.jsにより固有に定義されたこれだけのメソッドが連関しているわけで、自己拡張したメソッドやプロパティを縦横無尽に利活用して目的をゲットする──そんな貪欲な迄の"単純だがしかし無限に拡張出来るjQuery的欲望"が生き生きと展開されている、と言えるでしょう。

▲ToTop

init()メソッド解読手順

init()内で最初に登場するjQuery固有のメソッドはjQuery.clean()で、これはHTML要素を扱う場合に利用されています(44行)。jQuery.clean()本体は765-854行迄の約200行に亘る長~いコードで定義されていますが、実はその大半はクラスメソッドjQuery.each()です。ですから569-589行で定義されているjQuery.each()をまず解読しなければなりません。

更に上の表をみれば直ぐに分かるように、each()メソッド内には jQuery.trim()、jQuery.browser.msie、jQuery.makeArray()、jQuery.nodeName()、jQuery.merge()と続々と固有メソッドと固有プロパティが登場します。

従ってこれらについても順に解読していかなければ、jQuery.clean()メソッドは理解できないことになります。

これから先に続くエントリイは、init()メソッド解読を暫く連載するつもりです。

jQuery() の挙動を解読する(3) jQuery.extend()及びjQuery.prototype.extend()──jQuery解読(7)

このエントリイの改訂履歴
  • 初稿:2007/11/6
  • 改訂:2008/1/4……多くのミスがあったため全面更新しました。
  • 再改訂:ver1.3.2 対応とするために全面改訂しました。
jQuery.js 1.3.2 においてこのエントリイが対象とする箇所
562:jQuery.extend = jQuery.fn.extend = function() {
    ・・・・
610:},

extend メソッド解読の必要性

話が前後してしまいますが、先に進む前に extend メソッドを解読しておく必要があります。

まだ init() メソッドの解読途中ですが、先に進むためには init メソッド内の随所に鏤められている、extend ( { key1:fn1(){}, key2,fn2(){}, ・・・ } ) メソッド実行によって拡張された、jQuery クラスメソッドを解明しなければならず、そのためにはまず extend() による拡張がどの様に行われるのか、押えておく必要があるからです。

以下に jQuery.js の行を辿って解読しますが空白行は省略します。

※ extend 定義コードは ver1.1.4 以降において、以前よりも大幅に拡張されました。deep copy 対応や未定義値への対応が盛り込まれたためです。更に1.2.2以降においても再拡充されました。こうしてより一般化されました。

変数定義

562:jQuery.extend = jQuery.fn.extend = function() {
563:  // copy reference to target object
564:  var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;

562 行で、jQuery オブジェクトの extend メソッドと jQuery.prototype オブジェクトの extend メソッドを同時に定義しています。( jQuery.fn は 35 行で jQuery.fn = jQuery.prototype と定義済みのショートカット )

これにより、ユーザーが何らかのオリジナルな jQuery.extend( any codes ) を Web サイト内で定義した場合、any codes はそのページが読み込まれた時に prototype オブジェクトのプロパティにもなり、これが jQuery オブジェクトに継承されますから、サイトを開いた時点で、当該の any codes は jQuery オブジェクトに登録済みとなります。

続いて 563 行で変数を 5 つ定義しています。extend() メソッドの第1引数( それが与えられていなければ空オブジュクト )への参照を target に、インクリメント変数となる i に初期値1を、引数の数を length に、deep に偽値を各々代入し、options を未定義値として定義しています。

ここに deep は後に見るように、jQuery ではない複数オブジェクトの統合を行う場合に重要な役割を果たします。

deep copy ── オブジェクトの merging を行う

コメント #563 にあるdeep copy situation とは何か。──調べましたが既製の日本語の解説は見あたりませんでした。「オブジェクトの merge 」そのものを指す言葉ではなくメモリの使い方に係る専門用語のようですが解明出来ていません。

それは兎も角、コード解読を進めます。

第 1 ブロック 566-572 行

566:  // Handle a deep copy situation
567:  if ( target.constructor == Boolean ) {
    // ユーザーが第一引数を "true" とした場合にのみ、deep == true となる
568:   deep = target;
569:   target = arguments[1] || {}; // 第二引数がない場合には空オブジェクト
570:   // skip the boolean and the target
571:   i = 2; // こうしてコピー元からインクリメントを始める。
572:  }

第一引数が Boolean 型だった場合には、この値が既に代入済みとなっている target ( 564 行 )を 568 行で deep に代入します。この結果 deep 値は定義時には false に特定されていましたが true か false となります。

こんな面倒な処理の理由は以下のような複雑なコード進行をさせるためです。

■ 第一引数が true である場合

564 行において deep 値は false とされますが、直後の 567 行により true に置き換えられます。つまり、arguments[0] == true の場合には常に deep ==true となります。これにより、596 行の if の最初の条件がクリアされ、copy 値がノードではないオブジェクト型ならば、jQuery.extend() メソッドが再帰呼び出しされ、こうして第 2 引数オブジェクトに、第 3 引数以降のオブジェクトが deep コピーされます。

ところが、再帰呼び出しされた jQuery.extend() メソッド内では deep は必ず false となります。何故ならば 597 行により第 1 引数とされた deep 値が何であれ、直後に実行される 564 行により、deep = false と強制的に置き換えられてしまうからです。

この結果、再帰呼び出しされた jQuery.extend() メソッド内では 596 行の if 条件は必ず false となって必ず 603-604 行が実行され、プロパティ複写が行われます。

こうして 597-600 行の再帰呼び出しは必ず一度しか実行されないことになります。

では、arguments[n] ( n ≧ 3 )が存在した場合にはどうなるのでしょうか?

一度しか再帰呼び出しされない jQuery.extend() メソッド内において、 2 ≦ i< n+1 迄繰り替えされる for loop ( 584-606 行)によって、arguments[2]~arguments[n] 迄のオブジェクトが順にピックアップされます。

ピックアップされた arguments[i] オブジェクトに対して、次に 588 行の for in loop によって全てのプロパティが走査され、そのプロパティが 589-593 行及び 603-604 行により arguments[1] オブジェクトに複写統合されます。

■ 第一引数が false である場合

この場合には 568 行でも deep は false のままとなるため、597-600 行の extend メソッドの再帰呼び出しは決して行われません。他方意味のあるextend メソッドコードならば、false 以外にも引数は必ずあるはずですから、579-582 行が実行されることはなく、こうして jQuery オブジェクトの拡張は行われません。

その代わり、584-606 行の二重 for loop(但し、595-600 行は決して実行されない)によって、arguments[1] オブジェクトに対する arguments[m](m≧2)オブジェクトのマージが履行されます。

■ 第一引数が Boolean 型ではない場合

このときには 584 行の for loop つまり extend メソッドの引数に対する繰り返しピックアップは実行されますが、596 行の if 条件は必ず false となるため、597-600 行の extend メソッドの再帰呼び出しは決して行われず、604行のプロパティ複写が繰り返されます。

しかも、584-606 行のブロックは、extend メソッドの引数が 1 つしかない場合でも、複数個ある場合でも等しく機能しますので、前者の場合には jQuery オブジェクトが、後者の場合には arguments[1] オブジェクトが、それぞれ拡張されるわけです。

次に target 値に第二引数(なければ空オブジェクト)を代入します。

そしてインクリメントを 2 から開始するように、i の値を変えます。この i=2 は ver 1.1.4 では存在せず、そのためにバグとなっていた箇所です。

▲ToTop

引数がオブジェクトでも関数でもない場合の処理

574: // Handle case when target is a string or something (possible in deep copy)
575: if ( typeof target !== "object" && !jQuery.isFunction(target) )
576:  target = {};

target に空オブジェクトを代入していますが、これはエラーとせずに何も起こらないようにするエラー回避処理です。

引数が唯一の場合の処理──jQuery オブジェクト拡張の前処理

578: // extend jQuery itself if only one argument is passed
579: if ( length == i ) {
580:  target = this;
581:  --i;
582: }

たった1つの引数しか与えられない場合の処理が記されています。this つまり、jQuery オブジェクトを変数 target に代入し、インクリメント変数の初期値をゼロとしています。この i = 0 は 584 行以降で意味を持ちます。

ここにおいて、this(つまり jQuery オブジェクト)を target に代入することは、これより先で jQuery オブジェクト自体を拡張するための前処理となります。

愈々オブジェクトの統合

   // 583 行迄に定義した i を初期値とし引数個数未満迄インクリメントする
584: for ( ; i < length; i++ ) // オブジェクト走査ブロック開始
585:  // Only deal with non-null/undefined values
    // options に i 番目の引数を代入し、それが null でなければ、for loop を実行する
586:  if ( (options = arguments[ i ]) != null )
587:   // Extend the base object. 以下の loop で base オブジェクトを拡張する
     // 統合される側の options オブジェクトのプロパティをリストアップする定番 loop コード
588:   for ( var name in options ) { // プロパティ走査ブロック開始
      // target の name プロパティ値を変数 src に、また  options の name プロパティ値を
      // 変数 copy に代入。こうして src は base オブジェクトの name プロパティ
      // 値となり、copy は複写される統合元オブジェクトの name プロパティ値となる。
589:    var src = target[ name ], copy = options[ name ];
590:
591:    // Prevent never-ending loop
      // プロパティ値が同値ならば
592:    if ( target === copy )
593:     continue; //処理を中断し、次の繰り返しを実行する。( Back to #584 )
594:
595:    // Recurse if we're merging object values
      // オブジェクト同士を統合する場合には extend メソッドを再帰呼び出しする。
      // deep が true で copy が存在して DOM ノードではないオブジェクトの時には
596:    if ( deep && copy && typeof copy === "object" && !copy.nodeType )
       // extend を再帰呼び出し、再帰呼び出しされた jQuery.extend において
       // target = deep、length = 2、deep=false となり、567-572 行のブロックが機能し、
       // 569 行の target には 599 行の src、又は空配列か、空オブジェクトが代入され、
       // 571 行により i の初期値が  2 となる。次に、target(= src)がオブジェクトならば
       // 575-576 行はスルーされ、また length は 3 なので、579-582 もスルーされ、
       // 584 行に辿り着く。586行で options に copy が代入され、そのプロパティリスト
       // アップのための for loop が再び始まる。
       // ここに、再帰呼び出しされた場合には deep == false となっているので、
       // 596 行はスルーされて 603 行が実行され、copy が未定義でないならば、604 行が実行される。
597:     target[ name ] = jQuery.extend( deep, 
598:      // Never move original objects, clone them
599:      src || ( copy.length != null ? [ ] : { } )
       // 初期値 i = 2 なので 586 行により options には
       // 3番目の引数である次行の copy が代入される。
600:     , copy );
601:
602:    // Don't bring in undefined values
      // deep == false 等の場合に 603 行が実行され、604 行により
603:    else if ( copy !== undefined )
       // copy 値が、target オブジェクトの name プロパティ値として代入され、
       // コピー元オブジェクトの nameプロパティが、targetオブジェクトに複写される。
       // 再帰呼び出しの場合も同様の処理が行われる。
604:     target[ name ] = copy;
605:
606:   }
607:
608: // Return the modified object
   // 更新された target オブジェクトが返される。
   // 再帰呼び出しの場合も更新された target オブジェクトが返される。
609: return target;
610:};
興味深い for loop 記述

584 行の for ループに初期値がなく「 ; 」しかないことに注目しました。「 ; 」が2つなければ for 文として成立しませんから「 ; 」は不可欠ですが、初期値が記述されていないのです。

しかし、初期値 i は、既に定義されています。引数が1つの場合には 0 、deep 値が Boolean の時には 2、その他は最初に定義された1です。

このような for ループの標記には初めてお目に掛かりましたが、初期値を随意に変更するこの手法は大変勉強になります。

更にこの for 文には { } がありませんから、for 文は次の 1 行にしか係らず、さらにその当該行は if 文でありこの行にも { } がありませんので、結局、584 行の for ループは、auguments[i] が空でない場合( 586行 )にのみ機能して、588 行以下が働くことになります。

▲ToTop

extend メソッドの要所!

584-606行は extend 関数の引数が null 値でなかった場合に(586行)、オブジェクトを順に対象として、そのプロパティを取り出して別のオブジェクトに統合するコードで、jQuery オブジェクトを拡張するか、複数オブジェクトを統合合体します。

jquery.js の extend() メソッドの引数は JSON オブジェクトとして記述され、その中で様々な名称と値(殆ど関数)が定義されています。584-461行の for loop によって、 1 以上の引数プロパティから順番にプロパティ値を取り出し、それを target オブジェクトに格納してから、最後にこのオブジェクトを return することにより、1以上の個数の引数オブジェクトの各種プロパティを jQuery、又は任意のオブジェクトのプロパティ又はメソッドに複写します。

別エントリイ(jQuery() の挙動を解読する(27) 事例による jQuery.extend() 学習 upon ver1.3.2──jQuery解読(41))に、jQuery オブジェクトを拡張する場合と、任意のオブジェクトを統合する場合の具体例を幾つか記して、復習とします。

▲ToTop

jQuery() の挙動を解読する(2) インスタンスプロパティ・メソッドとクラスプロパティ・メソッド──jQuery解読(6)

  • 初稿:2007/11/03
  • 改訂:2007/11/21……用語を正確に書き換えた
  • 再改訂:2007/11/27……ケアレスミスを修正した。
  • 再改訂:2009/3/20 jquery.js ver1.3.2 対応に

jQuery() と jQuery のそれぞれのプロパティとメソッド

init() 内では、随所に jQuery(s,c).prop や jQuery(s,c).method() と jQuery.prop や jQuery.methop() が多用されています。また init() だけではなく jQuery 全体でこのような表記が沢山出てきます。ここではこれらの差異に着目し、これらが互いに全く別のものであることを確認し、またそれぞれの役割を解明したいと思います.

まず jQuery.prop や jQuery(s,c).method() は jQuery(s,c) インスタンスのプロパティ又はメソッドです。これらは new 演算子とinit() コンストラクタ関数によって生成されるインスタンスオブジェクトのプロパティ又はメソッドであり、このインスタンスは this キーワードで参照出来ます。そしてそれは決して関数オブジェクトである jQuery 自体のプロパティやメソッドではありません

一方、jQuery.prop や jQuery.method() は関数オブジェクト jQuery のメソッドやプロパティであって、決して、new 演算子とコンストラクタ関数によって生成されるインスタンスオブジェクトのそれらではありません。

端的に言えば、前者はインスタンスオブジェクトのメソッドやプロパティであり、後者は jQuery クラスのプロパティでありメソッドです。

ここに、jQuery のプロパティやメソッドは jQuery.extend() メソッドの実行(jquery.js ver1.3.2 の 612 行以降など)により定義されますますが、その extend() メソッドは 562~610 行で定義されています。そして実に巧みだと思うのは、その extend() メソッドは、同時にインスタンスオブジェクトのメソッドとして、定義されていることです( 562 行)。こうして extend() メソッドは2つの機能を担っている訳で、jQuery クラスのメソッド及び jQuery() インスタンスオブジェクトのメソッドを拡張する手段として機能するようになっています。

そして、この拡張方法こそがプラグインを自在に作成することを容易に可能としていることも強調すべき jQuery の利点であり、同時にこの複層的構造が jQuery.js を難解にしている一因ともなっているのではないでしょうか?。

▲ToTop

jQuery() とjQuery.extend()による拡張箇所リスト(該当行数リスト)

extend() メソッドの実行によって jQuery クラスオブジェクトと jQuery()インスタンスオブジェクトの、それぞれのプロパティとメソッドが様々に拡張される訳ですが、それらは次のように多用されています。( 以下は jQuery.js ver1.2.1 非圧縮版 における行数)

▼ jQuery インスタンスオブジェクトの拡張
  • 1855-1941(Event 関係)
  • 2040-2126(Ajax 関係)
  • 2507-2665(Animation 関係)
  • 2904-2991(Offset 関係)
▼ jQuery クラスオブジェクトの拡張
  • 469-1008(基本)
  • 1023-1044(BoxModelチェック)
  • 1139-1565(DOM 関係)
  • 1943-1976(DOM Ready 関係)
  • 2137-2506(Ajax 関係)
  • 2693-2738(Animation 関係)

インスタンスオブジェクトの拡張に 241 行が、そして jQuery クラスの拡張に 1438 行も充てられています。合計すると 1679 行となり、これらに extend() の定義の為の行数 39 行(404-442行)を加えた1718行は、全行数 2965 行の約 58 %を占めることになります。extend 関数はjquery.js のまさにキーとなる関数と言えます。

▲ToTop

jQuery.jsインクルード時に定義されるグローバル変数 jQuery のプロパティとメソッド

インクルード時に定義されるjQueryオブジェクトのプロパティとメソッド図。クリックすると拡大図を表示します。

以上を踏まえて、jQuery.js インクルード時に定義されるオブジェクトやそのプロパティを一覧してみます。例えば firebug の DOM インスペクターでそれを確認することが出来ます。(左図参照)

左図はグローバルオブジェクトとして $ と jQuery が定義されていること、そして jQuery クラスの 64 項目のプロパティ及びメソッドがあることを示すものです。なお、uiプロパティは jqueryui.js をインクルードしているために発生しているもので、jQuery.js だけインクルードした場合には生じません。

▲ToTop

 90%近いシェアを握っているインターネットエクスプローラの描画エンジンを利用したタブbrowser。沢山のタブbrowserがあるが、多機能、カスタマイズフリー、スクリプト利用等で一日の長がある。Gekkoエンジンへの対応も行われ、IEからの自立独立の方向に向かっている。2005年7月にはIE7が登場する見通しの中で、今後の発展が望まれる。

 多様なCSS作成支援機能を備えた、タグ入力式 HTML&CSS作成支援エディタ。スキンデザインもすっきりしている。テキストエディター上で作成するよりも確実で安全にタグ打ちが出来る。
文字コードを選べないのが欠点。

 StyleNote同様のタグ入力式 HTML&CSS 作成支援エディタ。長年使用してきたが現在StyleNoteに乗り換えつつある。

 クリップボード履歴情報を活用する為のソフト。画像まで履歴を取ってくれるのが嬉しい。このソフトを使わない日は絶対ない程に重宝し、愛用している。

 起動中のウィンドウの「コピーできない」説明文などの文字列を取得し、コピー可能な文字データにするツール。何かと便利。

 ストリーミングデータを保存することが出来るソフト。動画利用には不可欠なソフトだ。

 無料ながらレイヤー機能を有し、スクリプトによる拡張も可能な、sleipnir作者が提供している優れもの画像編集ソフト。

 画面キャプチャソフトと言えばこれに勝るものなし、ではないだろうか? 様々な取得方法を有しており、ブログ作成にもHomepage作成に不可欠だ。Jtrimと並んでWoodyBellsの作品。

 複数ファイルの同時編集は出来ないが、透過pngも作れる画像編集ソフト。
(以下当該サイトから抜粋)初心者にも簡単に操作が出来るフォトレタッチソフトです。多くの加工機能で画像に様々な効果を与えることができます。非常に軽快に動作するため、ストレスなく操作できます。

 Animation Gifファイルを作れる無料ソフト。

 キャプチャソフト。画面内にサイト全体が表示しきれない場合でも、これを使えば全体をキャプチャすることが出来る。

 画像処理。画像のフォーマット変換のみならず、色数やサイズ、圧縮率の変更まで一括処理できてしまう『BatchGOO!』は、大量の画像をまとめて処理したいときに大変便利なソフト。BMP, TIFF, JPEG, PCX, PNG の相互変換をはじめ、色数・サイズ・解像度の統一、JPEG圧縮率の調節など、ホームページ用の画像や携帯電話用の壁紙を揃えるのに抜群の相性を見せる。(Vectorの当該ソフト紹介頁より抜粋引用)

 名前から直ぐに想像が付くように画像のサイズを測るためのソフトだ。Homepage作成には欠かせない。2カラム、3カラムのレイアウトを行う場合に大変重宝する。

 ランチャーソフトは沢山あるが、中でもこれが一押しだ。2年以上使ってきたがその操作性には毎日満足している。これを使い始めてからデスクトップには一切のアイコンを表示することをやめてしまった。

 AdobeReader7によって、起動時間が長すぎるという長年のユーザーの不満はある程度解消した。そのためこの高速化ソフトは存在価値が低下してしまったかもしれない。AdobeReader6迄はこのソフトによる起動高速化で恩恵を受けてきた。

 IE専用が難点だが、様々なサイト内でIDやパスワードを入力するのに重宝するソフト。コンテキストメニューから簡単に起動できるのがGood! sleipnir等のIEの描画エンジンを利用しているブラウザでも使える。

 利用しているパソコンの諸元値を取得するには、このソフトがベストだ。インストール済みソフトの一覧が取得できるのも嬉しい。

 WMPは機能が豊富なだけ重い。RealPlayerも同様だ。そこでMedia Player Classicを使いたい。動作が軽快なだけではなく、対応しているファイル形式もすこぶる多く、これひとつで、wmvもrmも表示できてしまうのだから凄い! 数多あるMedia Playerの王様と言えるだろう。

 自宅でPCを起動しているときには必ず起動しているメディアプレーヤー。何かと過剰なWinampよりも、起動も速くスキンはシンプルだ。

 DivX, Xvid, Mov, Vob, Mpeg, Mpeg4, avi, wmv, dv, などの動画をDVD-Video形式に変換できるフリーソフト。クリックするとDVD関連ソフト紹介サイト=「DVDなToolたち」なるHomepageが開きます。