jquery.jsを読み解く

第1回jQueryライブラリ(1~171行目)

はじめに

jQueryとは、John Resigによって開発され、最近非常に注目を集めている Javascriptライブラリです。 JavaScriptとHTMLの対話を劇的に改善し、Ajaxなどにより複雑化してきているWebアプリケーション構築に必要な処理を非常に簡潔に書くことができます。また、ブラウザの種類やバージョンによる違いも吸収してくれるため、プログラマの作業量も減らしてくれます。

本連載では、jQueryライブラリのコードを読みながら、実装として中で何が行われているのかを見ていこうと思います。

想定している読者は、jQueryライブラリをただ使うだけでなく、やっていることを理解したいという方、使われているコードを応用して自分なりの改造をしてみたい方、新たなプラグインを開発したいという方などです。

今回の連載では執筆開始時点の最新版であるバージョン1.2.2を対象としています。説明の都合上行番号に大きく依存しているため、最後までバージョン1.2.2を対象とすることになります。

jQuery ソースコードの入手

jQueryは、jquery-1.2.2.jsという数十KBの一つのファイルにすべて書かれています。このソースコードは、公式サイトからダウンロードできます。圧縮方法の違いによって3つのバージョンがあり、実際のサイトで利用するにはMinified版がお勧めなのですが、ソースコードの解説なのでここではUncompressed版を利用します。ぜひ、お手元にダウンロードして記事と一緒にご覧になってください。

Uncompressed 93KB 通常のソースコード(無圧縮)
Minified and Gzipped 51KB コメントや不要な空白を取り除いたもの
Packed 28KB Packerによって圧縮したもの

ローカルスコープ化

jQueryの良いところは、名前空間を汚染しないところです。すべての機能をロードした状態で、jQueryと$の2つのオブジェクトしか存在しません。このため、他のJavaScriptライブラリと混ぜて使うことができます。これを実現しているのが、ソースコードの最初に登場するこの部分です。

0001: (function(){
      ...
3383: })();

全体のコードが、(function(){...})() で囲まれています。初めて見ると不思議な感じがしますが、無名関数を定義してすぐに実行するという処理を行っています。関数は、ふつうhoge()のような形で呼び出しますが、最初の()で括られた部分がhogeにあたり、その後に引数なしの()が続くと説明すれば少し分かりやすいでしょうか。

これにより、すべての定義をローカルスコープに押し込めることに成功しています。

先頭のコメント, 著作権表示

0002: /*
0003:  * jQuery 1.2.2 - New Wave Javascript
0004:  *
0005:  * Copyright (c) 2007 John Resig (jquery.com)
0006:  * Dual licensed under the MIT (MIT-LICENSE.txt)
0007:  * and GPL (GPL-LICENSE.txt) licenses.
0008:  *
0009:  * $Date: 2008-01-14 17:56:07 -0500 (Mon, 14 Jan 2008) $
0010:  * $Rev: 4454 $
0011:  */
0012:

MITライセンスとGPLのデュアルライセンスで公開されています。あなたがパフォーマンスのためにスクリプトを圧縮したとしても、このクレジット表示は削除しないように注意しましょう。jQueryは、自ら"New Wave Javascript"と名乗るだけのことはあり、非常に興味深いライブラリです。深く読み進めるにつれて、このことが段々明らかになってくることでしょう。

初期化処理

0013: // Map over jQuery in case of overwrite
0014: if ( window.jQuery )
0015:     var _jQuery = window.jQuery;
0016:

14行目では、既にwindow.jQueryオブジェクトが存在するときは、_jQueryに一時的に格納しておきます。これは後で、noConflict()を呼び出したときに元の状態に戻すためです。

次に出てくるコードは jQueryの初期化の部分になります。522行目のコードも一緒に見た方が分かりやすいので、順番は前後してしまいますが、ここで一緒に説明することにします。

0017: var jQuery = window.jQuery = function( selector, context ) {
0018:     // The jQuery object is actually just the init constructor 'enhanced'
0019:     return new jQuery.prototype.init( selector, context );
0020: };
0021:
0522: // Give the init function the jQuery prototype for later instantiation
0523: jQuery.prototype.init.prototype = jQuery.prototype;

17行目がまさにjQueryの根幹となる部分で、ここでjQueryオブジェクトを定義しています。jQueryオブジェクトの実体はinitメソッドによるコンストラクタで、オブジェクトが生成された際にinit()メソッドを実行してインスタンスを返します。よって、jquery.jsのロード時には定義が行われるのみで、jQuery関数(もしくは$)が呼ばれて初めて jQueryのプロトタイプオブジェクトのinit()が実行され、処理が行われます。

また、523行目ではjQuery.prototype.init関数オブジェクトのプロトタイプにjQuery関数オブジェクトのプロトタイプを設定しています。どういうことかというと、jQuery.prototypeに定義された様々なメソッドやプロパティを継承することで、jQueryライブラリの特徴であるメソッドチェーンを実現しているのです。

この辺りのコードは1.2.2で大幅に書き換えられました。リリースノートによると、ここを改善することで$(DOMElement)の処理を3倍高速化できたそうです(38ms → 13ms⁠⁠。

※この部分を理解するにはJavaScriptのプロトタイプ指向に関する知識が必要となるのですが、とても興味深い部分ですので、余裕があればECMAScriptの仕様書等を読んでみることをお勧めします。

0022: // Map over the $ in case of overwrite
0023: if ( window.$ )
0024:     var _$ = window.$;
0025:
0026: // Map the jQuery namespace to the '$' one
0027: window.$ = jQuery;
0028:

14行目と同様に、既にwindow.$オブジェクトが存在するときは、_$に一時的に格納しておきます。また、27行目でjQueryのショートカットを$に割り当てます。

0029: // A simple way to check for HTML strings or ID strings
0030: // (both of which we optimize for)
0031: var quickExpr = /^[^)[^>]*$|^#(\w+)$/;
0032: 
0033: // Is it a simple selector
0034: var isSimple = /^.[^:#\[\.]*$/;
0035:

パラメータによって処理を分岐するための正規表現です。quickExprは、パラメータがHTML文字列か単純なID指定かを判定するために、isSimpleはシンプルな選択の場合にパフォーマンスを最適化するための分岐用として利用されます。

initメソッド定義

0036: jQuery.fn = jQuery.prototype = {
0037:   init: function( selector, context ) {
0038:     // Make sure that a selection was provided
0039:     selector = selector || document;
0040: 

36行目からが jQueryオブジェクトの初期化処理になります。jQuery.fnとjQuery.prototypeに対してメソッドを定義していきます。

37行目で定義されているinit関数がjQueryオブジェクトの事実上のコンストラクタになります。39行目の文により、$()のように何も引数を渡されずに呼び出された場合は、$(document)と同義になります。

0041:     // Handle $(DOMElement)
0042:     if ( selector.nodeType ) {
0043:       this[0] = selector;
0044:       this.length = 1;
0045:       return this;
0046: 

引数にDOMエレメントが渡された場合の処理です。渡されたDOMエレメントを格納し、lenghtに1を設定してそのままjQueryオブジェクトを返します。

0047:     // Handle HTML strings
0048:     } else if ( typeof selector == "string" ) {
0049:       // Are we dealing with HTML string or an ID?
0050:       var match = quickExpr.exec( selector );
0051: 
0052:       // Verify a match, and that no context was specified for #id
0053:       if ( match && (match[1] || !context) ) {
0054: 
0055:         // HANDLE: $(html) -> $(array)
0056:         if ( match[1] )
0057:           selector = jQuery.clean( [ match[1] ], context );
0058: 

引数に文字列が渡された場合で、それがHTML文字列かつコンテキストが指定されていない場合は、cleanメソッドを実行してselectorに設定し直しています。cleanメソッドについては次回以降で説明しますが、不正なHTML表記をきれいにしてDOM要素を構築していると思ってください。

0059:         // HANDLE: $("#id")
0060:         else {
0061:           var elem = document.getElementById( match[3] );
0062: 
0063:           // Make sure an element was located
0064:           if ( elem )
0065:             // Handle the case where IE and Opera return items
0066:             // by name instead of ID
0067:             if ( elem.id != match[3] )
0068:               return jQuery().find( selector );
0069: 
0070:             // Otherwise, we inject the element directly into the jQuery object
0071:             else {
0072:               this[0] = elem;
0073:               this.length = 1;
0074:               return this;
0075:             }
0076: 
0077:           else
0078:             selector = [];
0079:         }
0080: 

単純なID指定の場合には、getElementByIdでそのIDをもつ要素を検索し、みつかればその要素を配列に格納し、lengthに1を設定してそのまま返します。ただし、IEとOperaの場合は戻り値の形式が異なるために自身のfindメソッドを呼び出します。指定されたIDがみつからなかった場合は、selectorを空にします。

0081:       // HANDLE: $(expr, [context])
0082:       // (which is just equivalent to: $(content).find(expr)
0083:       } else
0084:         return new jQuery( context ).find( selector );
0085: 

contextが指定されている場合の処理です。一旦contextについてのjQueryオブジェクトを生成してから、findメソッドを実行します。82行目のコメントにもある通り、contextを指定した処理は、$(content).find(expr) と同義であることが分かります。

0086:     // HANDLE: $(function)
0087:     // Shortcut for document ready
0088:     } else if ( jQuery.isFunction( selector ) )
0089:       return new jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector );
0090: 

引数として関数が渡された場合、つまり $(function(){ ... }) のように呼び出された場合の処理です。これは、$(document).ready(function(){ ... }); と一緒でDOMツリーの構築を待ってから指定された関数を実行します。

0091:     return this.setArray(
0092:       // HANDLE: $(array)
0093:       selector.constructor == Array && selector ||
0094: 
0095:       // HANDLE: $(arraylike)
0096:       // Watch for when an array-like object, contains DOM nodes, is passed in as the selector
0097:       (selector.jquery || selector.length && selector != window && !selector.nodeType && selector[0] != undefined && selector[0].nodeType) && jQuery.makeArray( selector ) ||
0098: 
0099:       // HANDLE: $(*)
0100:       [ selector ] );
0101:   },
0102: 

上記のいずれでもない場合の処理です。もし、配列やjQueryオブジェクト、DOM要素の配列が渡されていたら、その配列で初期化して返します

バージョン

0103:   // The current version of jQuery being used
0104:   jquery: "1.2.2",
0105: 

104行目は、バージョン情報を定義しています。

size()

0106:   // The number of elements contained in the matched element set
0107:   size: function() {
0108:     return this.length;
0109:   },
0110:   
0111:   // The number of elements contained in the matched element set
0112:   length: 0,
0113: 

length と size は、それぞれ要素の個数とその個数を返す関数です。

get()

0114:   // Get the Nth element in the matched element set OR
0115:   // Get the whole matched element set as a clean array
0116:   get: function( num ) {
0117:     return num == undefined ?
0118: 
0119:       // Return a 'clean' array
0120:       jQuery.makeArray( this ) :
0121: 
0122:       // Return just the object
0123:       this[ num ];
0124:   },
0125: 

116行目は、get(n)でn番目の要素を取得する関数を定義しています。もし、引数nが渡されなかった場合は、makeArray関数(次回以降で説明します)によって返されるクリーンな配列を返します。

pushStack()

0126:   // Take an array of elements and push it onto the stack
0127:   // (returning the new matched element set)
0128:   pushStack: function( elems ) {
0129:     // Build a new jQuery matched element set
0130:     var ret = jQuery( elems );
0131: 
0132:     // Add the old object onto the stack (as a reference)
0133:     ret.prevObject = this;
0134: 
0135:     // Return the newly-formed element set
0136:     return ret;
0137:   },
0138: 

便利なスタック機能を実現している部分です。新しいjQueryオブジェクトのprevObjectというプロパティに現在のオブジェクトを格納して返します。

endメソッドが呼ばれた場合に、この格納しておいたprevObjectが使われます。

setArray()

0139:   // Force the current matched set of elements to become
0140:   // the specified array of elements (destroying the stack in the process)
0141:   // You should use pushStack() in order to do this, but maintain the stack
0142:   setArray: function( elems ) {
0143:     // Resetting the length to 0, then using the native Array push
0144:     // is a super-fast way to populate an object with array-like properties
0145:     this.length = 0;
0146:     Array.prototype.push.apply( this, elems );
0147:     
0148:     return this;
0149:   },
0150: 

142行目のsetArrayメソッドは、選択された要素の強制的に初期化します。lengthに0をセットし、JavaScript標準のArray pushを使って初期化します。コメントにもあるようにスタックに格納していたオブジェクトも破棄されます。

each()

0151:   // Execute a callback for every element in the matched set.
0152:   // (You can seed the arguments with an array of args, but this is
0153:   // only used internally.)
0154:   each: function( callback, args ) {
0155:     return jQuery.each( this, callback, args );
0156:   },
0157: 

154行目にて定義されているeachメソッドは、JavaScriptでいわゆるイテレータを実現するためのものです。選択された要素に対して、順番にcallback関数を適用していきます。ここは紛らわしいのですが、別の場所(709行目)で定義されているjQuery.eachメソッドを実行します。なお、第2引数のargsは内部的に利用するためのもので、ドキュメントにも書かれておらず外部からは利用するべきではありません。

index()

0158:   // Determine the position of an element within
0159:   // the matched set of elements
0160:   index: function( elem ) {
0161:     var ret = -1;
0162: 
0163:     // Locate the position of the desired element
0164:     this.each(function(i){
0165:       if ( this == elem )
0166:         ret = i;
0167:     });
0168: 
0169:     return ret;
0170:   },
0171: 

選択された要素の中で、ある要素が何番目に出てくるのかを返す関数です。先ほど出てきたeachメソッドを利用して、ある要素が見つかればそのインデックスを、みつからなければ-1を返します。

いかがだったでしょうか。第1回はjQueryを理解する上で重要な処理が多かったため、最初の171行までしか説明できませんでした。しかし、ここまで見ただけでもとても興味深いライブラリだということが分かっていただけたのではないでしょうか。

それでは、次回は172行目以降を説明していきます。お楽しみに!

おすすめ記事

記事・ニュース一覧