jQueryコードリーディング:初期化まわりを詳しく、およびextend

前回は配列およびeachについて読んでいきました。
今回は、前回軽く触れたjQueryの初期化について詳しく見ていきたいと思います(クエリセレクタ/エレメント操作まわりの話はまた今度)。

対象

jQuery 1.5.0

よんでみよう

jQueryのソースは、外側をfunctionで囲まれています。

(function( window, undefined ) {
	// さまざまな初期化
	// ...
})(window);

このfunctionはwindowとundefinedという二つの引数を取っています。
javascriptのundefinedというのは未定義の値を表現するのに使われていますが、有名な話として、これは定数や予約語ではありません。

var undefined="hello";
alert(undefined); // "hello"

このように、ユーザが同名の変数を定義して上書きすることが可能です。なのでライブラリ等ではvoid(0)を使ったりすることが多いようですね。

undefinedを自分で定義することなく参照した場合、

// ライブラリ
(function(window) {
	jQuery.hoge=function(){alert(undefined);}
})(window);

// ユーザのコード
undefined="hello!";
jQuery.hoge(); // "hello!"

後から値を上書きされた場合動作がおかしくなってしまいます。
jQueryの場合、関数呼び出し時に引数の数が足りなかったらundefined(値)が代入されることを利用してundefined(変数)にundefined(値)を代入しています。説明がわかりにくいですね。

(function(window){
	var undefined=void(0);
})(window);

という書き方でも問題ないと思うんですけど、どうして引数に取っているのかはわかりません(かっこいいからかな)。

windowについても同様の理由で引数になっています。
余談ですが、このwindowオブジェクトは単なる変数ではなく、結構特殊な挙動をするので謎です。

// on Firefox 3.6.13

window=1;
alert(window); // [object window]

var window=1; // TypeError: redeclaration of const window

alert(window.window.window === window) // true

window.window=1;
alert(window.window); // [object window]

// Firefoxではこの手法でwindowを変更できる。Operaだと無理っぽい
this.window=1;
alert(window); // 1

このwindowオブジェクト、どこかの仕様で動作が定義されてるのかとおもって探してみましたがみつかりませんでした。ECMAScriptの範疇ではないしDOMでもないようです。謎。

閑話休題
windowとundefinedを導入したところまできましたね。
次にjQueryオブジェクトを構築します。このオブジェクトはライブラリが公開する唯一のグローバルオブジェクトで、

jQuery('#element'); // 関数として使ったり
jQuery.each(array,function(){}); // メソッドの格納場所として使ったり
jQuery.fn.foo=function(){...};
jQuery('#hoge').foo(); // プロトタイプの格納場所として使ったり

さまざまなつかいかたをします。


まず、関数としてのjQueryオブジェクトの構築。
ここではjQuery.fn.initをnewして返すだけですね。

// L:23
var jQuery = function( selector, context ) {
		// The jQuery object is actually just the init constructor 'enhanced'
		return new jQuery.fn.init( selector, context, rootjQuery );
	},
	// ...

次に、jQuery関数の帰り値、いわゆるjQueryオブジェクトのプロトタイプを構築します。

jQuery.fn = jQuery.prototype = {
	constructor: jQuery,
	init: function( selector, context, rootjQuery ) {
		// ...
	},

	// eachとかgetとか、おもに配列操作関係のメソッドが定義されている
	// ...
};

ここではjQuery.fnおよびjQuery.prototypeにオブジェクトを設定しています。
なぜjQueryオブジェクトのプロトタイプをjQuery.prototypeに設定するのかは謎。jQueryオブジェクトのコンストラクタはjQuery.fn.initなので、そこにさえ設定されてればいいはずなんですよね。new jQuery()することはないのだし。


さて、これらの定義はfunctionに包まれています。

// L:20
var jQuery = (function() {

// Define a local copy of jQuery
var jQuery = function( selector, context ) {
	// ...
}
// ...
jQuery.fn = jQuery.prototype = {
	// ...
}
// ...

// L:1073
// Expose jQuery to the global object
return (window.jQuery = window.$ = jQuery);

})();

このfunction内でコア部分を構築して、値を返すときにグローバルオブジェクトとしても登録してます(なんでここでやってるんだろう)。


ちなみに他ライブラリとの競合を避けるためのjQuery.noConflictはこんな定義になっています。

// L:379
jQuery.extend({
	noConflict: function( deep ) {
		window.$ = _$;

		if ( deep ) {
			window.jQuery = _jQuery;
		}

		return jQuery;
	},
	// ...
}

グローバルオブジェクトの設定前に、もともと設定されていた$,jQueryを_$,_jQueryという名前で保存してあって、それを復元しているんですね。

(function($) {
	// $を使った操作
})(jQuery.noConflict(true));

とすれば、グローバル空間に一切の痕跡を残さずjQueryを使えるという寸法。


次、extendの定義。
ドキュメントによれば、Merge the contents of two or more objects together into the first object.だそうです。シグネチャは以下。

jQuery.extend( target, [ object1 ], [ objectN ] )

target An object that will receive the new properties if additional objects are passed in or that will extend the jQuery namespace if it is the sole argument.

object1
An object containing additional properties to merge in.
objectN
Additional objects containing properties to merge in.


jQuery.extend( [ deep ], target, object1, [ objectN ] )

deep
If true, the merge becomes recursive (aka. deep copy).
target
The object to extend. It will receive the new properties.
object1
An object containing additional properties to merge in.
objectN
Additional objects containing properties to merge in.

http://api.jquery.com/jQuery.extend/

ポイントは

  • 第1引数のオブジェクトにに以降の引数のオブジェクトの内容を合成する
  • 引数がひとつしかない場合、jQueryオブジェクトをターゲットにする
  • デフォルトは浅いコピー。
  • 第一引数がbooleanだったら、deepオプションとみなす。deep=trueだったらディープコピー。

では読んでみましょう。

// L:315
jQuery.extend = jQuery.fn.extend = function() {
	// ここでは明示的に引数を定義していません(引数の型や長さによって並び順が変わりますしね……)
	// 代わりにargumentsを使用して引数にアクセスしています。

	 var options, name, src, copy, copyIsArray, clone,
		// target(マージされる対象)を仮決めする。基本第一引数、ただしfalsyなら{}
		target = arguments[0] || {},
		i = 1, // マージの元オブジェクトが入っている引数の先頭インデクス
		length = arguments.length, // 引数の長さ
		deep = false; // ディープコピーするかどうか

	// あっtargetの指定かと思ってたのはdeepオプションだった!!
	// Handle a deep copy situation
	if ( typeof target === "boolean" ) {
		deep = target;
		// やっぱtargetは第二引数な
		target = arguments[1] || {};
		// マージ対象のインデクスはやっぱり2な
		// skip the boolean and the target
		i = 2;
	}

	// targetがオブジェクトじゃなかったら{}にする(ディープコピー時にはそういう呼ばれかたされる)
	// Handle case when target is a string or something (possible in deep copy)
	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
		target = {};
	}

	// あっマージ対象がない、今までtargetだと思ってたのがマージ対象で真のtargetはthisだ
	// extend jQuery itself if only one argument is passed
	if ( length === i ) {
		target = this;
		--i;
	}

	// どたばたしましたが無事targetを特定、マージを開始します
	for ( ; i < length; i++ ) { // arguments[i]がマージ元
		// nullを渡されたら無視というアンドキュメンテッドな挙動。
		// これどういうときに嬉しいんですかね
		// Only deal with non-null/undefined values
		if ( (options = arguments[ i ]) != null ) {
			// optionsが現在のマージ元
			// Extend the base object
			for ( name in options ) {
				src = target[ name ];
				copy = options[ name ];

				// 無限ループを避けるため、マージ元のプロパティの値がtargetだったらスキップ
				// Prevent never-ending loop
				if ( target === copy ) {
					continue;
				}

				// ディープコピーモードだったら、コピー先を再帰的にたどってマージします
				// Recurse if we're merging plain objects or arrays
				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
					// クローン対象が配列かオブジェクトかで場合分けして、
					// マージ先がfalsyだった場合の初期値を設定。
					// 変な引数を渡した場合アンドキュメンテッドな挙動をする!!
					if ( copyIsArray ) {
						copyIsArray = false;
						clone = src && jQuery.isArray(src) ? src : [];

					} else {
						clone = src && jQuery.isPlainObject(src) ? src : {};
					}

					// Never move original objects, clone them
					target[ name ] = jQuery.extend( deep, clone, copy );

				// Don't bring in undefined values
				} else if ( copy !== undefined ) {
					// ディープコピーモードじゃなかったら単純に代入するだけ
					// copyがundefinedかどうか調べる意味はなんなんでしょう。
					// パフォーマンスの問題?
					target[ name ] = copy;
				}
			}
		}
	}

	// Return the modified object
	return target;
};

以上です。お疲れ様でした。

extendが定義されて以降は、ひたすらjQuery.extendでクラスメソッドを、jQuery.fn.extendでインスタンスメソッドを設定しまくっているだけです。

余談として、jQueryプラグインを作るにはjQuery.fnにメソッドを追加します。
詳細については公式サイトのドキュメントに詳しいんですが、そのうちこの辺の話もできるといいですね。

参考書籍

JavaScript 第5版
JavaScript 第5版
posted with amazlet at 11.02.12
David Flanagan
オライリー・ジャパン
売り上げランキング: 8437



前回と違って、今回はアサマシリンクというものが掲載されています。これは金がほしいからなんですね。しかしどちらも良書ですから、Javascriptの知識を深めたい方はぜひ。