なぜか$(/*何がし*/)でindex:0に全件分のa要素が入ったjQueryオブジェクトが返ってくる件調査(jQuery AOPを使ってみた。)

pixivのイラストをブックマークするのをブックマーク追加画面に遷移せずに行いたい - 文殊堂で、
イラスト表示画面(http://www.pixiv.net/member_illust.php?mode=medium&illust_id=${illust_id})から、
グループ一覧画面(http://www.pixiv.net/bookmark_group_setting.php)のHTMLを取得して、
なぜか$('#mypixiv_list>div[class^=bg_] a',htmlDoc)でindex:0に全件分のa要素が入ったjQueryオブジェクトが返ってくる件について調査してみた。

まずDocumentオブジェクトを作る

$.ajax({
type:'GET'
,url:'http://www.pixiv.net/bookmark_group_setting.php'
,success:function(html){
	var frag = html.replace(/^([\n\r]|.)*?<html.*?>|<\/html([\r\n]|.)*$/ig,"");
	htmlDoc=document.implementation.createDocument(null,"html",null);
	var r=document.createRange();
	r.setStartAfter(document.body);
	htmlDoc.documentElement.appendChild(r.createContextualFragment(frag));
}
});

この後、http://plugins.jquery.com/project/AOPで中身を見つつ、
何度も$('#mypixiv_list>div[class^=bg_] a',htmlDoc)を実行してみる。

jQuery.fn.find

jQuery.fn.findの戻り値をlogに出してみる。ついでにスタックトレースも。

var advices = $.aop.after({target:jQuery,method:'find'},function(result){
	console.info(result.length);
	console.trace();
	return result;
});

result.lengthは1だった。
$(/* selector */,context)での要素取得の実体はjQuery.fn.findなので当然。
jQuery1.2.6のjQuery.fn.findの実装(265-273行目)はこうなっている。

	find: function( selector ) {
		var elems = jQuery.map(this, function(elem){
			return jQuery.find( selector, elem );
		});

		return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ?
			jQuery.unique( elems ) :
			elems );
	},

なので次はjQuery.findを見てみるとする。
その前にlog出力を解除。

advices[0].unweave();

jQuery.find

jQuery AOPはtargetのクラスにprototypeがあるとそっちのメソッドを弄るので、
インスタンスメソッドも暮らすメソッドもあるようなクラスのクラスメソッドを弄れない。
なので、ちょっときもい事をやる。

$.prototype=undefined;
var advices = $.aop.after({target:jQuery,method:'find'},function(result){
	console.info(result.length);
	console.trace();
	return result;
});
$.prototype=jQuery.fn;

result.lengthは96だった。
この時点では「全件分のa要素が入ったjQueryオブジェクト」であるようだ。

jQuery.map、jQuery.unique、jQuery.fn.pushStack

面倒くさいのでまとめて

$.prototype=undefined;
var advices1 = $.aop.after({target:jQuery,method:'map'},function(result){
	console.info('jQuery.map:',result.length);
	return result;
});
var advices2 = $.aop.after({target:jQuery,method:'unique'},function(result){
	console.info('jQuery.unique:',result.length);
	return result;
});
$.prototype=jQuery.fn;
var advices3 = $.aop.after({target:jQuery,method:'pushStack'},function(result){
	console.info('jQuery.fn.pushStack:',result.length);
	return result;
});

結果

jQuery.map: 96
jQuery.unique: 96
jQuery.fn.pushStack: 1

どうやらjQuery.fn.pushStackで問題が発生したらしい。
で、その実装(111〜120行目)

	pushStack: function( elems ) {
		// Build a new jQuery matched element set
		var ret = jQuery( elems );

		// Add the old object onto the stack (as a reference)
		ret.prevObject = this;

		// Return the newly-formed element set
		return ret;
	},

113行目のjQuery( elems );以外で問題は起こりえない。
その実装(18〜21行目)

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

jQuery.fn.init

jQuery.fn.initの実装(34〜84行目)
なお、さっきのelemsが渡される引数の名前はselector

	init: function( selector, context ) {
		// Make sure that a selection was provided
		selector = selector || document;

		// Handle $(DOMElement)
		if ( selector.nodeType ) {
			this[0] = selector;
			this.length = 1;
			return this;
		}
		// Handle HTML strings
		if ( typeof selector == "string" ) {
			// Are we dealing with HTML string or an ID?
			var match = quickExpr.exec( selector );

			// Verify a match, and that no context was specified for #id
			if ( match && (match[1] || !context) ) {

				// HANDLE: $(html) -> $(array)
				if ( match[1] )
					selector = jQuery.clean( [ match[1] ], context );

				// HANDLE: $("#id")
				else {
					var elem = document.getElementById( match[3] );

					// Make sure an element was located
					if ( elem ){
						// Handle the case where IE and Opera return items
						// by name instead of ID
						if ( elem.id != match[3] )
							return jQuery().find( selector );

						// Otherwise, we inject the element directly into the jQuery object
						return jQuery( elem );
					}
					selector = [];
				}

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

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

		return this.setArray(jQuery.makeArray(selector));
	},

こいつの戻り値が1だった。
ぱっと見た感じ、この場合に実際に何かやっているのは、
83行目の「return this.setArray(jQuery.makeArray(selector));」だけっぽい。
各ブロックを通らない事を確認する。

var advices = $.aop.before({target:jQuery,method:'init'},function( selector, context ){
	console.assert( selector );
	console.assert( !selector.nodeType );
	console.assert( !(typeof selector == "string") );
	console.assert( !jQuery.isFunction( selector ) );
	console.trace();
});

こんな感じで出来るかと思ったがコンストラクタ関数は弄れないというか
弄って正常な動作をさせる事は出来ないので断念して、
普通にFirebugでステップ実行して確認した。

jQuery.fn.setArray、jQuery.makeArray

$.prototype=undefined;
var advices_makeArray = $.aop.after({target:jQuery,method:'makeArray'},function(result){
	console.info('jQuery.makeArray:',result);
	console.info('jQuery.makeArray:',result.constructor);

	console.trace();
	return result;
});
$.prototype=jQuery.fn;
var advices_setArray = $.aop.after({target:jQuery,method:'setArray'},function(result){
	console.info('jQuery.fn.setArray:',result);
	console.info('jQuery.fn.setArray:',result.constructor);
	console.trace();
	return result;
});

どうやらjQuery.makeArrayを通った段階で「$('#mypixiv_list>div[class^=bg_] a',htmlDoc)でindex:0に全件分のa要素が入った配列」になったようだ。

jQuery.makeArray(1129〜1143行目)

	makeArray: function( array ) {
		var ret = [];

		if( array != null ){
			var i = array.length;
			//the window, strings and functions also have 'length'
			if( i == null || array.split || array.setInterval || array.call )
				ret[0] = array;
			else
				while( i )
					ret[--i] = array[i];
		}

		return ret;
	},

デバッガで見てみると1136行目が実行されてしまっているのが原因のようだ。
arrayの中を見てみるとcallがあった。
Array.prototype.callに関数が設定されていた。
http://source.pixiv.net/source/js/lib/effects.jsの75〜78行目が原因。
http://blog.fulltext-search.biz/archives/2008/08/jquery-126-worrisome-code.html
↑で言及されているのと同じくscript.aculo.usとjQuery1.2.6のバッティングの問題でした。