Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active December 13, 2015 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dfkaye/4948675 to your computer and use it in GitHub Desktop.
Save dfkaye/4948675 to your computer and use it in GitHub Desktop.
Constructor API proposal - declaring it done

Constructor Inheritance in JavaScript

First, I like inheritance, but not for everything.

A bad use of inheritance is the canonical rectangle and square example in Java that Rusty Harold examines. However, it is not inheritance per se but the inherited getters and setters that are the real evil. Allen Holub said as much - or pretty close to it.

Another abuse is the construction of subclass hierarchies beyond, say, 3 layers deep. Ideally one would use a base class or interface and subclass that into one-off implementations. An EventTarget, EventSource, EventEmitter or what-have-you serves as a common base for view and model objects in the two-way data-binding version of MVC or MVVM, for example.

Objections based solely on mystical incantations

Some contend that there is a "JavaScript way" of doing things - that JS has a "true nature" - and that anything that looks like class-based inheritance is not the JavaScript way - see this tweet, for example, re JavaScript enlightenment - or this post on why you should Stop Using Constructors - or Douglas Crockford's admission at the end of the inheritance post.

The objections come down to "new" and "this" don't work as expected, that super isn't well-supported, that native constructors like Array can't be subclassed properly, that mixins give you better polymorphism, that config arguments give you better multiple inheritance, that JavaScript inheritance is really "prototypal" and that it is really just "functional" rather than constructor-oriented.

Sigh

The "functional" way means this - create an object as a prototype, then create a copy, then modify the copy, then use the copy. In practice this really means creating additional functions as well that modify the object with deliberate side-effects.

Constructors give us all this in one function call at increasingly super-optimized speed. We should not just shrug that off.

Classes are coming - How will that affect the fashionable functional idee fixe?

At the time of this writing, the ES6 proposal includes Classes, mainly to organize source code into a more familiar pattern. I regard this, as well as the fat arrow notation for closures, as unnecessary. We don't need classes per se, but I sure would like some polymorphism without the side-effects, or having to create one-off objects, or highjacking functionality with call() and apply(), or wrapping instantiations with IIFEs with a base object to refer back to.

Constructor API

I'd like a constructor API that reduces exposure to the prototype keyword, and supports inheritance with super/parent access.

I'd really like a super or parent reference I don't have to name explicitly (in the familiar B.prototype = new A fashion), making code more portable -- and testable.

Prior Art

Kevin Lindsey identified the first solution to inheriting from base prototypes. It was popularized in the early YAHOO and Google solutions.

Dean Edwards modified Lindsey's approach, adding a 'base' property to subclassed instances. Please read the goals he set himself.

John Resig created something a little more clever.

Nicholas Zakas

Using the suggestion by Nicholas Zakas,

function type(details){
    details.constructor.prototype = details;
    return details.constructor;
}

Pass in an object specifier with a constructor function property - get a callable Constructor function back:

var Color = type({
     constructor: function(hex) {
         ...
     },
     ...
});

var mycolor = new Color("ffffff");

This still allows us to continue extending via the prototype:

Color.prototype.newFeature = function () {
    ...
};

First cut: Constructor()

Similar to Zakas' type() function, we can define Constructor(), and use a factory method, create(). Constructor.create() accepts either a function (which it returns immediately) or an object specifier. If the specifier has no 'own' constructor function property, just add an empty one and do the rest. Under the covers, Constructor.create() would just call new Constructor().

Not a big deal so far but it's useful for the next step.

Supporting Inheritance

Constructor.extend() - accepts two arguments, either can be a function or an object specifier. The first arg is the base, the second is the specifier for the inheriting function. Internally, Constructor.extend() will check types, delegate to Constructor.create() if necessary, then do the parent-child mapping, and return the child function.

Note: The code snippets here borrow from my namespace API proposal.

Pass in an object specifier

var depUser = Constructor.extend(someDep, {

    constructor: function (data) {
      this.__super__();
      
      this.data = data;
      
      ...
    }, 
    
    overriddenMethod: function () {
      return this.__super__.overriddenMethod + ' >> ' + this.data;
    }
});

Pass in a function

var depUser = Constructor.extend(someDep, function (data) {

    this.__super__();  // invoke the *super* constructor, an instance of someDep
    
    this.data = data;

    // etc.
});

You can still modify the function's prototype:

depUser.prototype.overriddenMethod = function () {

    // access overridden method names on the *super* constructor instance
    
    return this.__super__.overriddenMethod + ' >> ' + this.data;
};

How about the native Function() instead of Constructor?

Ideally JavaScript out-of-the-box could support all this with a Function.create() and Function.extend() methods in place of Constructor.create() and Constructor.extend() - Function() itself does not accept non-string arguments, unfortunately, so a Constructor function would have to be available somewhere.

What this might look like in a CommonJS / AMD format, in a file named 'depUser.js':

module.exports = depUser;
depUser = Function.extend(someDep, {
  
  constructor: function (data) {
    this.__super__();
      
    this.data = data.
    ...
  }, 
  overriddenMethod: function () {
    return this.__super__.overriddenMethod + ' >> ' + this.data;
  }
});

Implementation Now

This is now implemented (with one or two modifications) at https://github.com/dfkaye/Constructor.

So what?

  • What's wrong with cloning and Object.create()?

    They don't support privacy as well (IMHO, more hoops just to decorate objects with backrefs, closures, etc.).

  • Doesn't that encourage use of the new keyword which some argue is bad practice?

    Yes but new performs better than Object.create(), et al.

  • Actually, doesn't that require use of the new keyword?

    Yes - that is a valid objection where this maps to something other than a new instance of the constructor.

  • Doesn't that create more objects?

    Yes - but you'd actually create them anyway - even the parent has to be created at some point. Objects are cheap; function calls are not.

  • Doesn't that add more object chaining and lookup penalties?

    Yes and No - because parent is a property, it's resolved early on the instance; and the parent's property access is faster for the same reason.

  • What's wrong with calling a parent's prototype method with call() or apply()?

    Indirection through non-obvious syntax? I want the sugar instead. Why go through the indirection if you can just call a parent method from an inheriting instance?

  • But implementation inheritance is evil.

    If we had interfaces in JavaScript then that might be a valid objection. Instead we have prototypes which are objects whose properties are bound to instances. And using call() and apply() on prototypes runs the risk of clobbering an instance's other properties - much like global variable clobbering (an issue addressed in my namespace API proposal).

@dfkaye
Copy link
Author

dfkaye commented Mar 23, 2013

UPDATE 2013-03-22 ~ This is now in the repo at https://github.com/dfkaye/Constructor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment