views:

39

answers:

1

Hi,

I recently wrote a class creation script (with the help of Crockford's and Resig's code for certain parts) that automates certain tasks such as: merging defaults with passed in params, requiring params, creating a bridge between DOM and class instance, binding custom events with a common namespace, inheritance with access to super method, and the ability for mixins.

My 1st question is, what are the pros and cons of creating your classes this way? My concern is performance and portability (all my classes depend on this script now).

My 2nd question is, what would be better ways to write a class creation script? Code is below.

(function($){
// Crockford's way of creating new objects
function F(){};
function _createObject(o) {
    F.prototype = o;
    return new F();
};

$.Class = function(){
    var
        prototype = {}
        ,_super = {}
        ,requiredError = ''
        ,extendedInits = []
        ,fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;

    // copy properties/methods to prototype
    for (var i = 0, ln = arguments.length; i < ln; ++i){
        var obj = arguments[i];

        // if extending another class
        if ($.isFunction(obj)) _super = obj = obj.prototype;

        if (typeof obj == 'object'){
            for (var prop in obj){
                var objMethod = obj[prop];

                // Resig's Simple Javascript Inheritance
                // if method already exists, map old method to this._super()
                prototype[prop] = typeof objMethod == "function" && typeof _super[prop] == "function" && fnTest.test(obj[prop]) ?
                    (function(prop, fn){
                      return function() {
                        var tmp = this._super;

                        // Add a new ._super() method that is the same method
                        // but on the super-class
                        this._super = _super[prop];

                        // The method only need to be bound temporarily, so we
                        // remove it when we're done executing
                        var ret = fn.apply(this, arguments);
                        this._super = tmp;

                        return ret;
                      };
                    })(prop, objMethod) :
                    // or overwrite the method/property
                    objMethod;

                // if __init method is defined on any of the objects passed in, they will be automatically run at instantiation
                if (prop == '__init') extendedInits.push(obj[prop]);
            }
        }
    }

    prototype.__initialize = function(){
        var
            self = this
            ,dataNS = this.__dataNS
            ,requiredParams = this.__requiredParams;

        // check required parameters
        if (requiredParams){
            for (var i = 0, ln = requiredParams.length; i < ln; ++i){
                var param = requiredParams[i];
                if (arguments.length == 0 || !arguments[0][param]) requiredError = requiredError + param + ', ';
            }
        }

        // if all required params are passed in
        if (requiredError.length == 0){
            // set defaults
            this.cfg = $.extend(true, {}, this.__defaults, arguments[0]);
            this.$elem = this.cfg.$elem;

            // create bridge between dom and instance
            if (dataNS) this.$elem.data(dataNS, this);

            // init class instance
            if (this.init) this.init.apply(this, arguments);

            // init objects instance was extended with
            if (extendedInits.length > 0){
                $.each(extendedInits, function(k, fn){
                    fn.apply(self, arguments);
                });
            }

            // init instance level 
            if (this.cfg.__init) this.cfg.__init.apply(this, arguments);
        }
        else {
            // alert missing properties
            requiredError = requiredError.substring(0, requiredError.lastIndexOf(','));
            var str = 'Required Parameters: ' + requiredError;
            if (dataNS) str = dataNS + ' - ' + str;
            alert(str);
        }
    };

    function _Class(){
        this.__initialize.apply(this, arguments);
    }

    _Class.prototype = _createObject(prototype);
    _Class.constructor = _Class;

    return _Class;
}
})(jQuery);

And this is how it would be used:

$.Accordion = new $.Class({
__requiredParams: ['$elem']
,__defaults: {
    speed: 200,
    onlyOneOpen: true,
    classNames: {
        panel: 'panel', panelHeader: 'panelHeader', panelContent: 'panelContent'
    }
}
,panels: {}
,init: function(opts){
    console.log('1st init');
    var self = this;

    // loop thru panels
    this.$elem.find('.' + this.cfg.classNames.panel).each(function(){
        var $this = $(this);
        var panelName = $this.attr('id');
        // uses panel dom element id as name
        self.panels[panelName] = new $.Panel({$elem: $this});
    });

    // panel header on click
    this.$elem.find('.' + this.cfg.classNames.panelHeader).click(function(e){
        e.preventDefault();
        var panelName = $(this).parents('.' + self.cfg.classNames.panel).attr('id');
        var action = (self.panels[panelName].isOpen()) ? 'close' : 'open';
        self[action](panelName);
    });
}
,closeAll: function(){
    $.dispatch('close.panel.accordion');
}
,openAll: function(){
    $.dispatch('open.panel.accordion');
}
,open: function(panelName){
    if (this.cfg.onlyOneOpen) this.closeAll();
    if (typeof this.panels[panelName] != 'undefined') $.dispatch('open.' + panelName + '.accordion');
}
,close: function(panelName){
    if (typeof this.panels[panelName] != 'undefined') $.dispatch('close.' + panelName + '.accordion');
}
}, $.ClassUtils);

Note: $.ClassUtils in the example above is a set of public methods mixed into the $.Accordion class

A: 

Lots of frameworks do what you have done. The primary concern I have with this is that it tries to make javascript something it is not. Javascript is prototypal inheritance language. Its is not a classical inheritance language.

This is not to say that 'class management' libraries are not useful because they are. Some feedback on yours: other examples of the code you have written make it easier to create classes. If you are going to do this, the end result when creating a class should be something like

var myObj =  MyStuff.create({
   prop: prop,
   doSomething: function(){}
 });

that is much easier to read, and less to type, than the system you put in place, as far as I can tell from your example...

hvgotcodes
Hm I might be missing something, but isn't your snippet similar to my 2nd snippet? Your example is to define the class itself, not instantiate one correct? I have seen other implementations use something like Class.create({}), but I like using the new operator (if only for a semantic reason).
alex
@alex, Ah I see what you got going on. How would you extend a class?
hvgotcodes
@hvgotcodes, it would be like var extendedClass = new $.Class(thisClassJson, classToExtendFrom, secondClassToExtendFrom); so basically the first json passed in is used as that class' definition, and every other one is mixed in
alex