jQuery オブジェクトを汚染せずにプラグインを追加する方法を考えてみた

気軽にプラグインを追加したい

jQueryプラグインっていうと、モーダルウィンドウやツールチップを表示したり、データをグラフ化したりとそれだけで大きな恩恵を受けられるものが多々ありますが、自分なりのプラグインの定義方法などの汎用的な処理を考えていると、jQuery のメソッドをちょっと拡張したような自分用メソッド郡みたいなものを定義したくなることがあります。
また、複数のパーツで構成される UI 系のプラグインの場合だと各パーツ毎に専用メソッドを持たせたくなったりします。例えばモーダルウィンドウの場合なら、閉じるボタンには winClose メソッド、ウィンドウフレーム部には、winResize メソッドみたいなものです。ところが、

プラグインの追加 = jQuery の prototype オブジェクトへの関数の追加 = jQuery オブジェクトの汚染

と考えると上記のようなケースでは気軽にプラグイン(メソッド)を追加する気になれません。いろんなプラグインなどを入れてる場合だと名前競合なども気になります。だったら prototype に追加せずインスタンス化された jQuery の DOM ノードオブジェクト(って言うんですかね?)そのものにメソッドを追加するという方法もありますが、この場合だとイベントの起きたパーツに紐付く他のパーツを処理しようとした時など eq() メソッド経由で参照することになり結局メソッドを実行するには一工夫必要になったりします。

jQuery オブジェクトの複製

では、jQuery オブジェクトを必要に応じ複製(継承)し、その複製した jQuery オブジェクトに必要なメソッド(プラグイン)を追加してはどうか?
で、その中で上記の UI 紐付き問題等を容易に解決するメソッドを作ってはどうか?

という事でいろいろと頭をひねったのですが、jQuery の複製(というか継承)が、なかなかうまくいかず悩んでたところ、文殊堂さんのところにこんな記事が・・

testという名前のメソッドを呼ぶとそれ以降、jQueryオブジェクトをラップしたオブジェクトによってメソッドチェーンが繋がっていく。
jQueryオブジェクトを返さないメソッドを実行した場合には、ラップしないで元のメソッドの戻り値を返す。

条件付メソッドチェーン実現のためjQueryのラッパーを作ってみた - 文殊堂

apply でjQueryメソッドを実行した結果が、jQuery インスタンスのものだったらラッパーオブジェクトで再取得するというシンプルなものでしたが、自分にとっては目からうろこでした。
9 割方解答そのものな記事でしたが、jQuery の複製として使うにはこのままだと若干問題があったので少々手を入れさせていただき、セレクタ部の処理を追加し、プラグイン化してみました。

使用例

こんな感じで生成します。

var $j1 = $.MyJQ()

後は、$j1 が jQuery オブジェクトと同じように使えます。

必要なメソッドを追加します。

$j1.fn.pink=function(){
	this.css('background','pink')
	alert('pink')
	return this;
}
$j1('body').pink()

継承もできます。

$j2=$.MyJQ()
$j2.fn.extend($j1.fn)
$j2.fn.silver=function(){
	this.css('background','silver')
	alert('silver')
	return this;
}
$j2('body').pink().silver().pink()

jQuery オブジェクトは汚染されません。

alert($.fn.pink)    //undefined
alert($.fn.silver)  //undefined

ソース

セレクタ部は、apply とかで jQuery の処理を間借りしたかったのですが、良い方法が思い浮かばず自前で書いてしまっています。

(function($j){
    $j.MyJQ = function(){
        return $j.MyJQ.impl.selector()
    }
    $j.MyJQ.impl = {
        selector : function(){
            var s = function(expr){
                var o=this,arr=[];
                if(this instanceof arguments.callee){
                    o._$j=expr.each(function(idx){
                        arr[idx]=this;
                    })
                    return $j.extend(arr,this)
                }
                else return new arguments.callee($j(expr))
            }   
            for(var i in $j)if($j.isFunction($j[i]))s[i]=$j[i];             
            return this.jqWrap(s);
        },
        jqWrap : function(s){
            s.fn=s.prototype;
            for(var i in $j.fn)
                if($j.isFunction($j.fn[i]))
                    (function(i){
                        s.fn[i] = function(){
                            var target = this.constructor == s ? this : this._$j;
                            var ins = $j.fn[i].apply(target,arguments)
                            if(!(ins instanceof $j))return ins;
                            if(ins==target){
                                var _ins = new s(ins);
                                for(var j in this)
                                    if(typeof _ins[j]=='undefined' && this.hasOwnProperty(j))_ins[j]=this[j]
                                return _ins;
                            }
                            return new s(ins)
                        }
                    })(i)
            return s;
        }
    }
})(jQuery)