5
\$\begingroup\$

I'm trying to write a JavaScript library to allow inheritance with knockout.

The fundamental problem when implementing inheritance in Knockout is that each Knockout observable is its own instance, thus if you try this:

 x = function () {};
 x.prototype.m = ko.observable();
 y = function () {};
 y.prototype = new x();
 z = new y();
 //z.m == y.prototype.m == x.prototype.m

The behavior I'm trying to get goes like this:

//psudo-code, I know this wouldn't work
x = function () {};
x.prototype.a = ko.observable();
y = function () {};
y.prototype = new x();
y.prototype.b = ko.computed(function () { return this.a() });
//enabling this:
z = new y();
z.a(5);
a = new y();
a.a(12);
// z.b() == 5, a.b() == 12

Here's my proposed solution:

var getParams = function (args, index) {
    return args.length > index + 1 ? Array.prototype.slice.apply(index) : Array.prototype.concat.call([], args[index])
};

var Ladder = function () {};

Ladder.prototype.init = function (config) {
    var self = this;
    if (!(config instanceof dontInit)) {
        // Only initialize if config is passed, this allows us to create sub-prototypes
        if (this.attachEntity) this.entity = config;
        for (var prop in self) {
            var extender = self[prop];
            if (extender instanceof doAttach) {
                self[prop] = extender.extender.apply(self, extender.args); // || ('' + extender.extender); // if the function doesn't return a value perhaps it only performs operations? Leave a tatle tale behind?
            }
        }
        if (this.populateObject) {
            for (var prop in config) {
                if (config.hasOwnProperty(prop)) {
                    var val = config[prop];
                    if (ko && ko.isObservable(self[prop])) self[prop](ko.unwrap(val));
                    else self[prop] = val;
                }
            }
        }
        for (var prop in self) {
            var extender = self[prop];
            if (extender instanceof doExtend) {
                self[prop] = extender.extender.apply(self, extender.args); // || ('' + extender.extender); // if the function doesn't return a value perhaps it only performs operations? Leave a tatle tale behind?
            }
        }
    }
}

Ladder.prototype.attachEntity = true;
Ladder.prototype.populateObject = true;

Ladder.prototype.attach = function (propertyName, extender) {
    var args = getParams(arguments, 2);
    this.prototype[propertyName] = new doAttach(extender, args);
}

Ladder.prototype.extend = function (propertyName, extender) {
    var args = getParams(arguments, 2);
    this.prototype[propertyName] = new doExtend(extender, args);
}

Ladder.inherit = Ladder.prototype.inherit = function () {
    var ctor = function (config) {
        this.init(config);
    };
    ctor.attach = Ladder.prototype.attach;
    ctor.extend = Ladder.prototype.extend;
    ctor.inherit = Ladder.prototype.inherit;
    ctor.prototype = new this(new dontInit());
    return ctor;
};

Ladder.prototype.attachComputed // doComputed

Ladder.createPrototypes = function (prototypes) {
    return duplicatePrototypes(prototypes);
}

var dontInit = function () {};

// doAttach is called before config and should attach a computed or observable, though you may attach any value you like.
var doAttach = function (extender, args) {
    this.extender = extender;
    this.args = args;
};
var doComputed = function (computed) {
    return new doExtend(function () {
        return ko.computed(computed, this);
    }, []);
};
// doExtend is called after config and would normally perform some init work
// naming convention for extenders is _00Name where 00 is a priority (00 will run before 01) and Name is a descriptive name
var doExtend = function (extender, args) {
    this.extender = extender;
    this.args = args;
};

My questions are:

  1. Are there performance issues with this type of inheritance? i.e. if I use this paradigm to build complex objects with long inheritance chains, will there be a performance hit vs some other 'better' inheritance paradigm?

  2. Does this make for clear code? Is it obvious what is going on when I define an object like this:

    var myObject = usefullBaseClass.inherit();
    myObject.attach("newProperty", ko.observable, 5);
    
    var instanceOfMyObject = new myObject();
    
    // instanceOfMyObjecct is an instance of myObject and has an observable
    // property called newProperty which is unique to this instance
    
\$\endgroup\$
  • \$\begingroup\$ I am most curious, what would you be modeling that requires a long inheritance chain ? I have yet to find something that requires more than 3 levels. \$\endgroup\$ – konijn Apr 1 '14 at 23:32
  • \$\begingroup\$ My current use case consists of a set of base classes used to generate a grid, I then create inherited classes for each instance of the grid, the user is then able to extend the classes for grid A without affecting the behavior of those classes in grid B (the goal is for a very customizable grid). The current paradigm would only involve 2-3 deep chains, but that will likely change as I continue adding features. For example the Row class will likely be extended by a sub-class AggRow, when a sub-class AggRow is created for an individual grid it will have an inheritance chain 3 deep. \$\endgroup\$ – cwohlman Apr 2 '14 at 1:58
  • \$\begingroup\$ I'd also like to plan for deeper inheritance if I can. \$\endgroup\$ – cwohlman Apr 2 '14 at 2:03
  • \$\begingroup\$ Good enough, can you update the snippet under 'Does this make for clear code', the snippet should include how you created the usefullBaseClass. \$\endgroup\$ – konijn Apr 3 '14 at 13:05
1
\$\begingroup\$

Interesting question,

  • Are there any performance hits ? I don't see problems, it would not be slower then building the objects in an old skool manner

  • Does this make for clear code? Your sample code would not run, you need to provide a sample that would actually run.

Furthermore:

  • You should run you code through JsHint
    • You are missing semicolons
    • You are declaring some variables more than once in the same function
  • Your code has Ladder.prototype.attachComputed // doComputed <- Pointless
  • Your code has return duplicatePrototypes(prototypes); <- duplicatePrototypes is not provided
  • dontInit <- doNotInit reads better, also there must be a better way to avoid initialization ;)
  • doAttach is a constructor, so definitely wrongly named. AttachedValue mayhaps?
  • Anonymous functions are a pain in stacktraces, if you are going to create a custom OO approach, then I would avoid avoid anonymous functions like the plague.
  • // naming convention for extenders is _00Name where 00 is a priority <- I do not believe this works, for (var prop in self) { does not pre-sort property names, at least that is not guaranteed
  • The distinction between doAttach and doComputed is unclear, are the comments possibly out of date? // doAttach is called before config and should attach a computed or observable <- It seems from the code that I would attach a computable through doComputed ?
  • This:

    //Note: extender.extender might simply return undefined
    self[prop] = extender.extender.apply(self, extender.args); 
    

    is better than

    self[prop] = extender.extender.apply(self, extender.args); // || ('' + extender.extender); // if the function doesn't return a value perhaps it only performs operations? Leave a tatle tale behind?
    
  • I guess var self = this; comes from your knockout background. If you are not using closures, then there is no good reason to use this.
  • Do not drop newlines from if statements;

    if (ko && ko.isObservable(self[prop])) 
        self[prop](ko.unwrap(val));
    else 
        self[prop] = val;
    
  • if (ko) should be at the very start of your script, there is no point if knockout is not available

All in all, I think I would not use your library until it has matured a bit more.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.