jQueryコードリーディング:配列編

はじめに

突然そういう気分になったので、jQueryのコードを読んでいきたいと思います。
本日は「配列に対するeach」をjQueryがどう扱っているかについて調査します。

対象

jQuery 1.5.0

本日の題材:配列に対するeach

var array=[1,2,3];
$(array).each(function(){alert(this)}); // 1, 2, 3

よんでみよう

まずjQueryの初期化処理から見ていきましょう。

(function( window, undefined ) { // L:16
// Use the correct document accordingly with window argument (sandbox)
var document = window.document;
var jQuery = (function() {

// 内部で使用するさまざまな変数の設定
var jQuery= /* ... */,
	// ...
	quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,
	// ...

// jQuery.fnはjQuery.prototypeと同じ
jQuery.fn = jQuery.prototype = {
	constructor: jQuery,
	init: function( selector, context, rootjQuery ) {
		// ...
	},
	// each,map,...
};


// jQuery.fn.initをjQueryオブジェクト化してるらしい
// Give the init function the jQuery prototype for later instantiation
jQuery.fn.init.prototype = jQuery.fn; // L:314

// extendの定義
jQuery.extend = jQuery.fn.extend = // ...

// extendを使用して色々追加
jQuery.extend({ // L:379
	// isXxx, parseJson, Deffered, ...
});

// さまざまな初期化処理が続く

// jQuery, $ をグローバルに公開する
// Expose jQuery to the global object
return (window.jQuery = window.$ = jQuery); // L:1074

})();
// これでjQueryオブジェクトの構築終わり


// ブラウザの挙動を判定して互換性レイヤーを設定しているようだ。ここを読むとクロスブラウザ力がつきそう
// jQuery.supportから色々取得できる
(function() { // L:1079

	jQuery.support = {};

	var div = document.createElement("div");

	div.style.display = "none";
	div.innerHTML = "   <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";

	var all = div.getElementsByTagName("*"),
		a = div.getElementsByTagName("a")[0],
		select = document.createElement("select"),
		opt = select.appendChild( document.createElement("option") );
	// ...
})();

// jQuery.data
// jQuery.queue
// jQuery.attr/*Class/val
// jQuery.event
// jQuery.Event
// さまざまなイベント関係の操作を定義


// L:3260
// SizzleなるCSSセレクタエンジンを使っているらしい。ここだけで1300行くらいある……
/*!
 * Sizzle CSS Selector Engine
 *  Copyright 2011, The Dojo Foundation
 *  Released under the MIT, BSD, and GPL Licenses.
 *  More information: http://sizzlejs.com/
 */
(function(){
// ...

// L:4530
// EXPOSE
jQuery.find = Sizzle;
jQuery.expr = Sizzle.selectors;
jQuery.expr[":"] = jQuery.expr.filters;
jQuery.unique = Sizzle.uniqueSort;
jQuery.text = Sizzle.getText;
jQuery.isXMLDoc = Sizzle.isXML;
jQuery.contains = Sizzle.contains;
})();

// エレメント探索関係の操作を定義
// jQuery.find/has/contains/...
// jQuery.fn.parent/next/siblings/...
// textとかwrapとかbeforeとか……
// css関係の定義が延々と続く
// ajax関係
// showとかhideとかエフェクト系
// jQuery.offset with ブラウザごとのさまざまな条件分岐。ここを読むとクロスブラウザ力がつきそう
// leftとかtopとか

// 終了。おつかれさまでした

})(window); // L:8176

8000行!コード全体をざっと眺めただけで一時間以上過ぎた気がします。jQuery巨大ですね……

気を取り直して。

var array=[1,2,3];
$(array).each(function(){alert(this)}); // 1, 2, 3

まず、$(array)が何を返すのかというのを調べましょう。

// 最初に呼び出される関数$はjQueryと同一です
// L:23
var jQuery = function( selector, context ) {
		// The jQuery object is actually just the init constructor 'enhanced'
		// ここではnew jQuery.fn.initしているだけですね。
		return new jQuery.fn.init( selector, context, rootjQuery );
	},


// L:95
jQuery.fn = jQuery.prototype = {
	constructor: jQuery,
	init: function( selector, context, rootjQuery ) {
		// これがnewされる対象です。
		// 渡されたarrayはselectorという引数に入ります。

		// このあたりにselectorに対するいくつかの条件判定がありますが、配列についてはひっかからないようです。

		// 結果として、一番下のこの式が実行されjQuery.makeArrayが呼び出されます。
		// さて、ここで渡されているthisとは何か。
		// initはnewされているので、thisはフレッシュなオブジェクトです。
		// 先ほどの初期化時に jQuery.fn.init.prototype=jQuery.prototype されているため(L:314)
		// thisはjQueryオブジェクトです。
		return jQuery.makeArray( selector, this );
	},
	// ...
}


// L:379
jQuery.extend({
	// ...
	// L:663
	// results is for internal usage only
	makeArray: function( array, results ) {
		// で、ここにたどり着く
		// resultsにはフレッシュなjQueryオブジェクトが入っている
		var ret = results || [];

		if ( array != null ) {
			// マルチブラウザ対応怖い……
			// The window, strings (and functions) also have 'length'
			// The extra typeof function check is to prevent crashes
			// in Safari 2 (See: #3039)
			// Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
			var type = jQuery.type(array);

			if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) {
				// このパスは通らないので無視しておきましょう
				push.call( ret, array );
			} else {
				// jQuery.mergeによってretとarrayが合成されているようです
				jQuery.merge( ret, array );
			}
		}

		// 最終的にretが返ります
		return ret;
	},
	// ...
	// L:698
	merge: function( first, second ) {
		// この関数はsecondの配列に含まれる要素をfirstに破壊的に追加して返すようですね。
		// Array#pushを使っていないのは、引数が純粋な配列とは限らないからでしょう。
		// lengthプロパティを持ち、数値のインデクスで各要素にアクセスできるものならなんでも受け入れるようになってるんだと思います。
		var i = first.length,
			j = 0;

		if ( typeof second.length === "number" ) {
			for ( var l = second.length; j < l; j++ ) {
				first[ i++ ] = second[ j ];
			}

		} else {
			while ( second[j] !== undefined ) {
				first[ i++ ] = second[ j++ ];
			}
		}

		first.length = i;

		return first;
	},
	// ...
});

はい、おつかれさまでした。
まとめると、

var a=$([1,2,3])

は、

var a=new jQuery();
a[0]=1;
a[1]=2;
a[2]=3;
a.length=3;

という処理を行っているようです。

var a=new jQuery();
a[0]=1;
a[1]=2;
a[2]=3;
a.length=3;

a.each(function(){alert(this)}); // 1,2,3

予想通り動きましたね。

次にeachについてみていきましょう。

jQuery.fn = jQuery.prototype = {
	// ...

	// L:260
	// Execute a callback for every element in the matched set.
	// (You can seed the arguments with an array of args, but this is
	// only used internally.)
	each: function( callback, args ) {
		// ここではjQuery.eachに委譲しているだけです
		return jQuery.each( this, callback, args );
	},
	// ...
};

jQuery.extend({
	// ...

	// L:611
	// args is for internal usage only
	each: function( object, callback, args ) {
		// ここがeachの本体のようです。
		// objectが対象となる配列、callbackがコールバック、
		// 第三引数は無視してよさそうですね。
		var name, i = 0,
			length = object.length,
			isObj = length === undefined || jQuery.isFunction(object);

		// isObjで、objectが配列っぽいのか(lengthを使ってインデクスで列挙する)
		// 一般のオブジェクトなのか(含まれるプロパティを列挙する)を判断しているようです。

		if ( args ) {
			// このパスは通らないので略
			// ...
		// A special, fast, case for the most common use of each
		} else {
			if ( isObj ) {
				// 今回渡したのは配列なのでこのパスは通らない
				// ...
			} else {
				// まあインデクスで回して各要素にcallback適用してるだけですが高速化の努力が見て取れますね。
				// callback.call(value,i,value) という呼び出しで、
				// コールバック内のthisが現在の要素を指すようになります。
				// あとコールバックがfalseを返すと中断できるらしい、はじめて知った。

				for ( var value = object[0];
					i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {}
			}
		}

		return object;
	},
	// ...
});

以上です。おつかれさまでした(2)

次回の予定としては

$('#hoge').text()

とか読んでみようと思いますがセレクタまわりは結構たいへんそうですね。どうなることやら。