何度やっても同じ

ただの日記

jQueryの謎: new jQuery.fn.init と jQuery.prototype

jQueryのソースを読んでいて、よく理解できない点が2つ。軽く調べて、一応の結論を得たのでメモっておく。

まずは2つの問題をまとめておく。

new jQuery.fn.init 問題

問題の箇所を2.0.0b1から抜粋すると、以下のとおりなのだけど

line:51~

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

端的に言えば、ここで new をしないで

jQuery = function( selector, context ) {
  return this.init( selector, context, rootjQuery );
},

こう書いたら何か問題あるのか、という話。こうすれば、jQuery.fn.init などではなく、きれいにjQuery(をコンストラクタとする)オブジェクトが返ってくるのに。

jQuery.prototype 問題

おなじく問題の箇所を2.0.0b1から抜粋。

line:85~

jQuery.fn = jQuery.prototype = {

このように、jQuery.prototype として jQuery.fn が設定されているわけだけど、new jQuery.fn.init 問題のところでみたとおり、関数jQueryは内部で new した別のオブジェクトを返すので、仮にこれをコンストラクタとして new jQuery() みたいなコードを書いても、jQuery.prototype を継承するオブジェクトは作れない。

じゃあ jQuery.prototype を設定する意味なくない?という話。

以下この2つの問題について考えていく。

昔のjQueryオブジェクトはホントにjQueryオブジェクトだった

いわゆるjQueryオブジェクトというのはjQuery.fn.initをコンストラクタとして作られたオブジェクトなわけだけど、実はこれはjQuery1.2.2以降の話で、jQuery1.2.1までのjQueryオブジェクトは、文字通り関数jQueryをコンストラクタとして作られたオブジェクトだった。1.2.1の関数jQueryを見てみると次のとおり。

var jQuery = window.jQuery = function(selector, context) {
  // If the context is a namespace object, return a new object
  return this instanceof jQuery ?
    this.init(selector, context) :
    new jQuery(selector, context);
};

thisがjQueryインスタンスかどうか判定して分岐しているけれど、これは要するに、jQuery("selector") と書いても new jQuery("selector") と書いても同様に、関数jQueryをコンストラクタとするオブジェクトが得られるよ、ということ。new を使わない前者の書き方だと this が window になるので、this instanceof jQuery は false となり、結局 new jQuery 呼び出しになるわけだ。

これが今のようなコードに変わったのが1.2.2です(下記)。jQuery.fn.init じゃなくて jQuery.prototype.init だったりはするけれど、この両者は同じオブジェクトだし、そこは気にしない方向で。今問題にしたいのは、このときから正確な意味でのjQueryオブジェクトが返されなくなったということ。

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

もっと昔はinitメソッドなんてなかった

さらに歴史をさかのぼると1.1.2まではjQueryオブジェクト初期化の役割をもつインスタンスメソッドとしてのinitメソッドは存在しなかった。かわりに、関数jQueryに初期化コードがベタっと書かれている。

この部分の設計が大きく変わったのは1.1.3。そこで、関数jQuery自体はシンプルになり、かわりにjQuery.fn(ひいてはjQuery.prototype)に init メソッドが定義された。

var jQuery = function(a,c) {
	// If the context is global, return a new object
	if ( window == this || !this.init )
		return new jQuery(a,c);
	
	return this.init(a,c);
};

jQuery.fn = jQuery.prototype = {
	init: function(a,c) { 
                // ... 略

これは単に、JavaScriptでクラス型オブジェクト指向的なコードを書くプログラミングスタイルに移行したということだろうか。インスタンスメソッドと同列に init とか initialize みたいな名前のメソッドを定義して、それをコンストラクタから呼ぶスタイルに変わった。

今問題にしている視点だけから言えば、この時代のコードは直感的でわかりやすい。jQueryオブジェクトは実際に関数jQueryをコンストラクタとして作られたオブジェクトであり、jQuery.prototype にそのメソッドが定義されている。jQuery.prototype には jQuery.fn という別名がつけられており、拡張したいときはjQuery.fnに関数を追加する。という感じです。

jQuery.prototype問題の答え

1.2.1までは関数jQueryがそのままjQueryオブジェクトのコンストラクタだったので、jQuery.prototype を設定することには意味があった。今ではそれが不要だとしても、逆に設定しないことに必要性があるわけでもないし一応残しておくか、という判断なのかな、というのがとりあえずの結論です。いわゆる「歴史的な理由」。

new jQuery.fn.init の理由は高速化・・・らしいけど

new jQuery.fn.init というコードが追加された1.2.2とはどういうリリースだったのか、もうちょっと詳しく見てみよう。

jQueryブログによると、バグフィックスなどのほかに $(DOMElement) 呼び出しの高速化が行われている。これは怪しい。

しかし、1.2.2の init メソッドには

    // Handle $(DOMElement)
    if ( selector.nodeType ) {
      this[0] = selector;
      this.length = 1;
      return this;

というコードが追加されているので、$(DOMElement) の高速化ってのはこのコードのことを指しているのだろう・・・などと思いつつ、しかし new jQuery.fn.init の理由は結局よくわからないので、githubの履歴を漁って、該当のコミットを探してみた。

https://github.com/jquery/jquery/commit/f97f77c034dc62001a687c728bdfdc71a23bf6b8

John Resig自身による修正。理由は、$(DOMElement) の高速化・・・あ、いや、all uses of $(...) と言っているので、DOMElementに限らないのかもしれない。実際この修正で $(DOMElement) の実行速度だけが改善されるというのはよくわからない。$(DOMElement) の改善はやっぱりこっち(下記)がメインだと思うんだよね・・・。

https://github.com/jquery/jquery/commit/1a2fdafd386a8f7be8b633634a684969921f8b8f

$(...) 全般の速度改善だとしても、なんでこれで改善するのかよくわからないが。

ともかく何故速くなるのかはよくわからないけど、理由は高速化だったということで、忙しいのでこのへんでノ