jQuery プラグインの定義パターンについて調べてみた

jQueryプラグインの定義手法については、いろいろな記述の仕方が考えられるかと思います。今回、実際に公開されているソースを参照して、どのような定義パターンがあるのかを調べてみました。

jQuery 標準 API の定義構造のおさらい

まず、jQuery が標準で提供している各種 API が、どうのような構造で定義されてるかおさらいしてみます。

$ や jQueryグローバル変数、つまり window オブジェクトのプロパティ名であり、その実体は関数オブジェクトです。

window.$ = window.jQuery = function(){ ... }

jQuery が提供する API には 関数 API と メソッド API があり、関数 APIjQuery 関数オブジェクト(以降 jQuery セレクタと記述します)が持つ、関数オブジェクトのことを指します。

jQuery.ajax = function(){ ... } //関数 API

メソッド API は、jQuery セレクタの prototype オブジェクトが持つ関数オブジェクトのことを指します。prototype は全ての関数オブジェクトが持っているオブジェクトで、プラグイン定義の際に使用される jQuery.fn は jQuery.prototype の別名(ショートカット)です。

jQuery.prototype.show = function(){ ... } //メソッド API
jQuery.fn = jQuery.prototype;

jQuery セレクタで要素を取得すると、jQuery セレクタを new することで生成される jQuery インスタンスが返されます。jQuery インスタンスは、jQuery.prototype に定義された関数郡を、自身が保有するメソッドのように扱うことができます。

var instance = new jQuery();
instance.show();    // jQuery.prototype に定義された関数がメソッドとして使える

jQueryプラグインを定義するという行為は、これら関数 API 、メソッド API を独自に定義することを意味します。

では実際に、どのようなかたちでプラグインが実装されてるか見てみます。

1. 関数 API 系のプラグイン

1.1 標準的な定義
jQuery.myPlugin = function( option ) {
    //...   
}

前述した標準 API と同じ定義パターンです。

1.2 複数の関数を定義
(function($){
    $.plugin = {
        hoge : function(){
            //...   
        },
        fuga : function(){
            //...   
        }
    }
})(jQuery);

プラグイン名自体を名前空間用オブジェクトとして定義し、その直下に複数の関数 API を定義してます。

2. メソッド API 系のプラグイン

2.1 標準的な定義
(function($){
    $.fn.myPlugin = function( option ) {
        return this.each(function() {
            ... init
            var hoge = function(){ ... }
            var fuga = function(){ ... }
        });
    };
})(jQuery);

プラグイン作成の入門記事で紹介されてるような標準的な定義(だと思うのですが該当するプラグインがあまり見つかりませんでした)。プラグイン実行の度、サブメソッド(hoge , fuga)が再定義されるので無駄な処理コストが発生します。

2.2 実装独立型の定義
(function($){
//  var myPlugin = function( elem , option) {   // private
    $.myPlugin = function( elem , option) {     // public
        // init
        var hoge = function(){ ... }
        var fuga = function(){ ... }
    }
    $.fn.myPlugin = function( option ) {
        return this.each(function() {
            $.myPlugin( this , option);
        });
    };
})(jQuery);

インターフェース部と実装部を分離した書き方です。インデントが浅くなることと、実装定義の外部参照が可能になること意外は「2.1 標準的な定義」と大差ありません。

2.3 サブメソッドの外部定義
(function($){
//  var myPlugin = {    // private
    $.myPlugin = {      // public
        hoge : function(){ ... },
        fuga : function(){ ... }
    }
    $.fn.myPlugin = function( option ) {
        return this.each(function() {
            // init
        });
    };
})(jQuery);

サブメソッド(hoge , fuga)をプラグインメソッド外で定義してるので、プラグイン実行時のサブメソッドの再定義コストは無くなりますが、サブメソッド実行時に基本パラメータ( option )の受け渡しを考慮する必要があります。

2.4 サブメソッドをプラグインメソッド直下に定義
(function($){
//  var myPlugin = function( option ) {     // private
    $.fn.myPlugin = function( option ) {    // public
        return this.each(function() {
            // init
        });
    };
    $.extend($.fn.myPlugin , {
        hoge : function(){ ... },
        fuga : function(){ ... }
    });
})(jQuery);

「2.3 サブメソッドの外部定義」とほぼ同じですが、サブメソッドをプラグインメソッド( $.fn.myPlugin )の直下に定義してるので、jQuery セレクタは汚染されません。

2.5 サブメソッドをプライベート関数として定義
(function($){
    var hoge = function(){ ... }
    var fuga = function(){ ... }
    $.fn.myPlugin = function( option ) {
        return this.each(function() {
            // init
        });
    };
})(jQuery);

サブメソッドをプライベート関数として定義するので、プラグイン外部から参照できなくなります。

2.6 関数 API / メソッド API の同時定義
(function($){
    $.myPlugin = function( option ){
        $.myPlugin.init( window , option);
    }
    $.extend($.myPlugin,{
        init : function( elem , option ){
            // init
        },
        hoge : function(){ ... },
        fuga : function(){ ... }
    });
    $.fn.myPlugin = function( option ) {
        return this.each(function() {
            $.myPlugin.init( this , option);
        });
    };
})(jQuery);

関数 API / メソッド API の 2 つを定義し、実装ロジック部分を共有してます。

2.7 実装を prototype オブジェクトで定義
(function($){
    $.myPlugin = function( elem , option ){
        // init
    }
    $.extend( $.myPlugin.prototype,{
        hoge : function(){ ... },
        fuga : function(){ ... }
    });
    $.fn.myPlugin = function( option ) {
        return this.each(function() {
            new $.myPlugin( this , option);
        });
    };
})(jQuery);

prototype ベースオブジェクト指向による JavaScript らしい実装(だと思う)。jQuery セレクタで取得された要素1つ1つが、専用のプラグインインスタンスを持つことになるで、各要素固有のパラメータをインスタンス側で保持することができます。jQuery UI ではこのインスタンスを data() メソッドにより対応する要素にもたせることで、プラグイン利用者がインスタンスの存在を意識することなく、自由なタイミングでサブメソッド( hoge や fuga )を実行できる API を提供してます。
ちなみ 実際の jQuery UI のソースでは、$.widget という関数を使用して動的に上記のようなプラグインの定義を行っています。

2.8 new を使った定義
(function($){
    $.myPlugin = new function(){
        var hoge = function(){ ... },
        var fuga = function(){ ... }
        this.init = function( option ) {
            return this.each(function() {
                // init
            }
        }
    }
    $.fn.myPlugin = $.myPlugin.init;
})(jQuery);

プラグイン定義のタイミングで new することで実行される無名関数内で、外部参照可能なプラグインメソッド( this.init )を定義した後 $.fn に割り当ててます。

2.9 名前競合の懸念のある定義
(function($){
    $.extend( $.fn,{
        myPlugin : function( option ){
            // init
        },
        hoge : function(){ ... },
        fuga : function(){ ... }
    });
})(jQuery);

プラグイン名としてうたってる名前以外のメソッド(hoge , fuga)も、プラグインとして定義しちゃってます(以下参照)。名前競合しないようなネーミングの工夫が必要かと思われます。

Treeview

  • $.fn.swapClass
  • $.fn.replaceClass
  • $.fn.hoverClass
  • $.fn.heightToggle
  • $.fn.heightHide
  • $.fn.prepareBranches
  • $.fn.applyClasses

flexigrid

  • $.fn.flexReload
  • $.fn.flexOptions
  • $.fn.flexToggleCol
  • $.fn.flexAddData
  • $.fn.noSelect

jTemplate

  • $.fn.setTemplate
  • $.fn.setTemplateURL
  • $.fn.setTemplateElement
  • $.fn.hasTemplate
  • $.fn.removeTemplate
  • $.fn.setParam

Easy Drag

  • $.fn.ondrag
  • $.fn.ondrop
  • $.fn.dragOff
  • $.fn.dragOn

結局どの定義パターンがいいのか?

個人的な好みになりますが...

  • ちょっとした処理なら「2.1 標準的な定義」
(function($){
    $.fn.myPlugin = function( option ) {
        return this.each(function() {
            // init
            var hoge = function(){ ... }
            var fuga = function(){ ... }
        });
    };
})(jQuery);
  • そこそこの処理だけど、要素個別のデータ保持やサブメソッドの外部実行が不要なら「2.3 サブメソッドの外部定義」(継承等も考慮するなら init 処理は $.myPlugin に書いた方がいいかも..)
(function($){
//  var myPlugin = {    // private
    $.myPlugin = {      // public
        hoge : function(){ ... },
        fuga : function(){ ... }
    }
    $.fn.myPlugin = function( option ) {
        return this.each(function() {
            // init
        });
    };
})(jQuery);
  • それ以外は「2.7 実装を prototype オブジェクトで定義」
(function($){
    $.myPlugin = function( elem , option ){
        // init
    }
    $.extend( $.myPlugin.prototype,{
        hoge : function(){ ... },
        fuga : function(){ ... }
    });
    $.fn.myPlugin = function( option ) {
        return this.each(function() {
            new $.myPlugin( this , option);
        });
    };
})(jQuery);

といったところでしょうか。