Mon, 26 Nov 2012 by Bolshevik in javascript | Comments

Little attention is paid to writing an extendable javascript code. Prototype framework despite its backwardness and old age does provide some defensive mechanisms and interfaces making developers use limited OOP. While jQuery does not. I would say that Prototype is concentrated on a thin architecture with an ability to manipulate the DOM while the main aim of the jQuery is DOM manipulation plus tasty javascript wrappers and enhancers.

Prototype

The prototype framework encourages prototype inheritance and simplifies the inheritance by providing a few wrapper functions. Reference Class class implementation two see the simple methods we need to implement basic OOP in javascript and their implementation in the framework.

One should pay attention that only prototype methods (I am referring to the object prototype, not to the framework here) can be subclassed by the prototype in a good simple way, but instance methods cannot. Here is an example of both methods:

var MyObject = function() {
    this.instanceMethod1 = function() {};
    this.instanceMethod2 = function() {};
};
MyObject.instanceMethod3 = function() {};

MyObject.prototype = {
    prototypeMethod1: function () {},
    prototypeMethod2: function () {},
    prototypeMethod3: function () {}
}

This may lead to some confusion, but the idea is that prototype methods are shared by all object instances, while instance methods can be overwritten in one object instance and be default in another. And it is possible to modify prototype methods which will affect all object instances.

var MyObject = function()
{
    this.foo = function () { console.log("foo") };
}
MyObject.prototype = {
    pfoo: function () { console.log("pfoo") }
}
var a = new MyObject(), b = new MyObject();

a.foo(); a.pfoo(); // foo pfoo
b.foo(); b.pfoo(); // foo pfoo
MyObject.prototype.pfoo = function () { console.log("Bar") };
b.foo = function () { console.log("B foo") };
console.log("----------------"); // ----------------
a.foo(); a.pfoo(); // foo Bar
b.foo(); b.pfoo(); // B foo Bar

So prototype provides prototype based inheritance and allows to extend any class using OOP approach. It is practically possible to extend or replace any object implementation on the site.

jQuery

Despite very easy and useful DOM and web API jQuery doesn't provide any guidelines to write extendable code by default. There are several ways to do it as mentioned here. As one may notice, jQuery UI provides some abstraction to substitute class implementations.

Important note: actually there are a some ways allowing to extend the existing jQuery classes. For example, one may use custom Events that can be observed by external code. But backend experience shows that it is impossible to forecast what event your code must trigger to be extendable and not to trigger too many of the events.

OOP inheritance

There are several OOP implementations that can be used in our modules. Please refer to

The best would be selecting one of the patterns to be used everywhere across our projects. But due to big number of code to be rewritten the article proposes intermediate solution.

Guidelines

Name-spacing

Avoid global functions or class definitions. If you do need a global function, attach it to the window object. All modules have to use window.ModuleName name space to add any custom methods. This must be done in the following way:

if (!window.ModuleName) {
    window.ModuleName = {
        somefunction: function () {}
    };

    window.ModuleName.myModuleClass = {
        init: function () {}
    };
}

No private variables/methods

Avoid private variables or methods. Attach all not REALLY local variables to the current object using "this". Protected methods or variables should be defined only by convention. Their names must be preceded by underscore ("_").

Assume definition of an object via function e.g.

window.ModuleName.myObject = function () {
    var _private; // possible but not welcomed due to extension problems.
    this._protected = 42;
    function private() { // No.
    }
    this.publicMethod = function () {};
}

Define methods in prototype

Actually it is possible to correctly extend the object described in the previous code block. But using prototype to define the methods simplifies the task as will be shown below. The correct object definition must look like this:

window.ModuleName.myObject = function () {};
window.ModuleName.myObject.prototype = {
    init: function () {
        this._protected = 42;
    },
    publicMethods: function () {}
};

This leads to the next guideline: init method must be always implemented and correctly construct your object. It must be explicitly called by the user of your object.

Follow jQuery plugin writing guidelines

One should follow general guidelines mentioned here.

Use closures when defining jQuery plugins

Correct:

;(function( $ ) {
    $.fn.MyPlugin = function( method ) {
        ...
    };
})(jQuery);

Implement correct plugin initialization and shutdown

Any jQuery plugin has to implement at least init and destroy methods to correctly initialize the plugin on the element and destroy it.

Expose jQuery plugin logic to an external basic class (Use Bridge/Pimpl)

One has to expose all the logic of the plugin to a separate class. jQuery plugin must contain a pointer to the implementation and allow to modify it. There are two examples following.

Example 1

if (!window.MyModule) {
    window.MyModule = {};
}

(function($) {

    var NameSpace = "MyPlugin";
    var NameSpaceImpl = NameSpace + '_Impl';

    (function (module) {
        module.MyPluginImpl = function () {};
        module.MyPluginImpl.prototype = {
            options: {
                name: "No name"
            },
            init: function (element, options) {
                this._protected = 42;
                this._options = $.extend( {}, this.options, options );
                this.exposed = "exposed";
                // All methods must refer to these variables, not this.
                this._$element = $(element); //jQuery object.
                this._element = element; // DOM object.

                this._$element.css('color', 'red');

                return this;
            },
            otherMethod: function () {
                this._$element.css('color', 'green');
            }
        };
    }) (window.MyModule);

    // Note: var is used, because we are not exposing these methods, but actually we can.
    // These methods are mapped.
    var methods = {
        init: function(options) {
            return this.each(function() {
                if ( !$.data(this, NameSpaceImpl) ) {
                    var object = new window.MyModule.MyPluginImpl();
                    $.data( this, NameSpaceImpl, object.init(this, options));
                }
            });
        },
        destroy: function() {
            return this.each(function() {
                var impl = $.data(this, NameSpaceImpl);
                if (impl) {
                    // Do some work.
                    $.data(this, NameSpaceImpl, undefined);
                }
            });
        },
        someMethod: function () {
            return this.each(function() {
                var impl = $.data(this, NameSpaceImpl);
                if (impl) {
                    impl.otherMethod();
                }
            });
        },
        replaceImplementation: function (impl) {
            return this.each(function() {
                $.data(this, NameSpaceImpl, impl);
            });
        }
    };

    $.fn.MyPlugin = function( method ) {
        if ( methods[method] ) {
            return methods[method].apply( this, Array.prototype.slice.call(arguments, 1));
        } else if ( typeof method === 'object' || !method ) {
            return methods.init.apply( this, arguments );
        } else {
            $.error( 'Method ' +  method + ' does not exist in jQuery.MyPlugin' );
            // or we can try to create universal mapper. Here is an example:
            /*
             return this.each(function() {
             var impl = $.data(this, NameSpaceImpl);
             if ( impl && typeof(impl[method]) == 'function') {
             impl[method].apply(impl, Array.prototype.slice.call(arguments, 1));
             }
             });
             */
        }
    };
})(jQuery);

So, what does this bunch of code help us with? All of this allows to replace plugin implementation globally or in any instance. Here is an example of overwriting the implementation using simple inheritance mechanism:

if (!window.OverWriteModule) {
    window.OverWriteModule = {};
}

function extend(Child, Parent) {
    var F = function() { }
    F.prototype = Parent.prototype
    Child.prototype = new F()
    Child.prototype.constructor = Child
    Child.superclass = Parent.prototype
}

(function($) {
    if (window.MyModule && window.MyModule.MyPluginImpl) {
        window.OverWriteModule.MyModule = {};

        (function (scope) {

            // Let's inherit the original implementation in a simpliest way.
            scope.MyPluginImpl = function() {};
            extend(scope.MyPluginImpl, window.MyModule.MyPluginImpl);
            // Let's owerwrite constructor, it will add some more text.
            scope.MyPluginImpl.prototype.init = function () {
                // Very important to call parent correctly.
                var result = scope.MyPluginImpl.superclass.init.apply(this, arguments);
                this._$element.html(this._$element.html() + ' Pwned!!!');
                return result;
            };

            // And finally let's replace the plugin implementation.
            window.MyModule.MyPluginImpl = scope.MyPluginImpl;

        }) (window.OverWriteModule.MyModule);
    }
})(jQuery);

It is suggested to add "extend" function to a global scope, maybe jQuery module.

Example 2

if (!window.MyModule) {
    window.MyModule = {};
}


(function($) {
    $.fn.MyPlugin = function( method ) {
        if ( typeof method === 'object' || !method ) {
            return this.MyPlugin.prototype.init.apply(this, arguments);
        } else {
            if (typeof(this.MyPlugin.prototype[method]) == 'function') {
                this.MyPlugin.prototype[method].apply(this, Array.prototype.slice.call(arguments, 1));
            }
        }
    };

    $.fn.MyPlugin.prototype = {
        options: {
            name: "No name"
        },
        init: function (options) {
            this._protected = 42;
            this._options = $.extend( {}, this.options, options );
            this.exposed = "exposed";

            $(this).css('color', 'red');

            return this;
        },
        otherMethod: function () {
            $(this).css('color', 'green');
        }
    };
})(jQuery);

And the plugin can be extended in this way:

if (!window.OverWriteModule) {
    window.OverWriteModule = {};
}

function extend(Child, Parent) {
    var F = function() { }
    F.prototype = Parent.prototype
    Child.prototype = new F()
    Child.prototype.constructor = Child
    Child.superclass = Parent.prototype
}

(function($) {
    if ($.fn.MyPlugin) {
        window.OverWriteModule.MyModule = {};

        (function (scope) {
            // Let's inherit the original implementation in a simpliest way.
            var parent = $.fn.MyPlugin;
            scope.MyPlugin = function () {
                parent.apply(this, arguments);
            }
            extend(scope.MyPlugin, $.fn.MyPlugin);
            // Let's owerwrite constructor, it will add some more text.
            scope.MyPlugin.prototype.init = function () {
                // Very important to call parent correctly.
                var result = scope.MyPlugin.superclass.init.apply(this, arguments);
                $(this).html($(this).html() + ' Pwned!!!');
                return result;
            };
            scope.MyPlugin.prototype.otherMethod = function () {
                $(this).css('color', 'blue');
                return this;
            };

            // And finally let's replace the plugin implementation.
            $.fn.MyPlugin = scope.MyPlugin;

        }) (window.OverWriteModule.MyModule);
    }
})(jQuery);

Here is an HTML page to try both examples:

<html>
    <head>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
        <script src="example.js" type="text/javascript"></script>
        <script src="overwrite.js" type="text/javascript"></script>
    </head>
    <body>

        <span class="example">Example test</span>
        <span class="example">Example test</span>
        <span class="example">Example test</span>
        <span class="example">Example test</span>
        <span class="example">Example test</span>

        <script type="text/javascript">
        $(document).ready(function() {
            $('.example').MyPlugin();
            setTimeout(function() {
            $('.example').MyPlugin('otherMethod');
            }, 3000);
        });
        </script>
    </body>
</html>

Links

  1. Essential jQuery Plugin Patterns
  2. jQuery Plugins/Authoring
  3. Pseudo-classical pattern (In Russian: ООП в Javascript: наследование)
  4. Prototype - Lang - Class
  5. Simple JavaScript Inheritance

Comments

comments powered by Disqus

About

PHP web developer, python developer