1
\$\begingroup\$

Edit - decl smokes dojo.declare - http://jsperf.com/dojo-declare-vs-decl

The code has been updated a bit. decl now accepts object literals as well, although I still like "declaration functions" better.


I'm looking for some feedback on an approach I've been taking to provide simple inheritance for my javascript code.

Usually when working with constructors in javascript, you create the constructor function first, and add properties to its prototype.

With this approach, you instead write a single function (we'll call it the "declaration function") which is invoked using new. The properties of the resulting object instance (the "declaration object") are copied to the prototype of your new constructor. The constructor property of the declaration object (if any) is the constructor itself; otherwise a new empty function or wrapper for the parent constructor is used as the constructor.

Before your declaration function is invoked, it is given a prototype providing it with a few useful function properties. So far these include extend, for extending another constructor, and augment, for continuing a partial declaration. Because the declaration function is invoked with new, these functions are available as properties of this in the declaration function. Note that these function properties only exist in the transient "declaration object," not in your constructor or its prototype.

Here is the code:

/** decl

    Create a prototype object and return its constructor.

    @param {Function|Object} declaration
*/
function decl (declaration) {
  if (!declaration) {
    declaration = {};
  }
  else if (declaration.call) {
    declaration.prototype=decl.proto;
    declaration.prototype[decl.dataKey]={};
  }
  return decl.getCtor(declaration);
}

// Name of property where declaration objects' metadata will be stored.
// If you want to pass objects to decl instead of functions,
// put the metadata (parent, partial, etc.) in this property.
decl.dataKey = 'decl-data';

// This object is used as a prototype for declaration objects,
// so everything here is available as properties of `this`
// inside the body of each declaration function.
decl.proto = {

  extend: function (ctor) { 
    return (this[decl.dataKey].parent=ctor).prototype; 
  },

  augment: function (ctor) { 
    return (this[decl.dataKey].partial=ctor).prototype; 
  }

};

// Create a copy of a simple object
decl.clone = function (obj) { 
  return this instanceof decl.clone ? this : 
         new decl.clone(decl.clone.prototype=obj);
};

// Merge src object's properties into target object
decl.merge = function (target, src) { 
  for (var k in src) {
    if (src.hasOwnProperty(k) && k!='prototype' && k!=decl.dataKey) {  
      target[k] = src[k];
    }
  }
};

// Generate empty constructor
decl.empty = function () { 
  return function(){}; 
};

// Generate wrapper for parent constructor
decl.wrap = function (parent) {
  return function(){ parent.apply(this, arguments); };
};

// Prepare a constructor to be returned by decl
decl.getCtor = function (declaration) {    
  var oldProto, p = 'prototype', c = 'constructor',
      declFn = declaration.call ? declaration : null,
      declObj = declFn ? new declFn(declFn) : declaration, 
      data = declObj[decl.dataKey] || {},
      parent = data.parent, partial = data.partial, 
      ctor = 
          declObj.hasOwnProperty(c) ? declObj[c] : // ctor is user-defined 
          partial ? partial : // ctor is already defined (partial)  
          parent ? decl.wrap(parent) : // ctor is generated wrapper for parent ctor
          decl.empty(); // ctor is generated empty function

  // If there's a parent constructor, use a clone of its prototype
  // and copy the properties from the current prototype.
  if (parent) {
    oldProto = ctor[p];
    ctor[p] = decl.clone(parent[p]);
    decl.merge(ctor[p], oldProto);
  }

  // Merge the declaration function's properties into the constructor.
  // This allows adding properties to `this.constructor` in the declaration function
  // without defining a constructor, or before defining one.
  decl.merge(ctor, declFn);

  // Merge the declaration objects's properties into the prototype.
  decl.merge(ctor[p], declObj);

  // Have the constructor reference itself in its prototype, and return it.
  return (ctor[p][c]=ctor);
};

Here are some simple examples.

// super, implicit ctor
var Animal = decl(function(){

  this.test = function(){
    console.log('ok');
  };

});

// sub, implicit ctor
var Dog = decl(function(){

  this.extend(Animal);

  this.legs = 4;

  this.bark = function(){
    console.log('woof');
  };

});

// super, explicit ctor
var Foo = decl(function(){

  this.constructor = function(x){
    this.x = x;
  };

});

// sub, auto ctor
var Bar = decl(function(){

  this.extend(Foo);

});

// partial prototype (the rest of Foo)
decl(function(){

  this.augment(Foo);

  this.test = function(){
    console.log('{'+this.x+'}');
  };

});

// tests
console.log (new Foo(24), new Bar(13), new Animal(), new Dog());

I feel like this solution allows for a pretty clean and straightforward coding style, but I haven't really seen it used. Is there something out there that already takes this approach? Or is it a bad idea for some reasons I'm missing?

\$\endgroup\$
  • 1
    \$\begingroup\$ Doing it wrong. \$\endgroup\$ – Raynos Dec 30 '11 at 20:52
  • 2
    \$\begingroup\$ Since that doesn't really add much and I'm already talking to you in the javascript room I won't bother addressing it ;) \$\endgroup\$ – Dagg Dec 30 '11 at 21:09
2
\$\begingroup\$

From a onceover:

  • instanceof works, so bonus points there
  • Some terrible names : decl, getCtor, declFn, declObj, just type the full name
  • You have an unmaintainable/undocumented nested ternary in getCtor()

It is a neat idea to define the constructor and auto-magically move the functions to prototype, but in the end I find vanilla OO still the best approach. It seems tricky now to add legs to the prototype of Dog.

\$\endgroup\$
  • \$\begingroup\$ getCtor, declFn, declObj are only used internally; I saw no reason to be extra verbose with those (IMO the names are doing their job, I still remember what they mean and this code is pretty old now). decl is supposed to be the name of the "library," so it should probably stay. Which nested ternary is undocumented? Surely you don't mean the one documented with comments on every line? Why is it hard to modify Dog's prototype? Dog.prototype.legs = 3 should do it. The augment method was supposed to make this even easier, but I ditched it in a more recent version, figuring YAGNI. \$\endgroup\$ – Dagg Jan 17 '14 at 0:41

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.