Constructor Inheritance in JavaScript

[ originally posted 12 Feb 2013 ]

Note: this post acts as justification for my Constructor kata on github. It’s really a re-post for posterity, as 1) I thought it worth preserving a specimen of my former thinking, because 2) I think far less often in the constructor/super inheritance pattern in JavaScript than I did even a year ago (i.e, summer of 2014).

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.

Some Objections

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 to ES6

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() which acts as a factory method. Constructor.create() Constructor() 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().

Note: The above API described has been changed to use only Constructor() without an additional Constructor.create() method. Constructor() is itself the factory method that can be called with or without the new keyword.

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() 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 (superArg, data) {
      this.__super__(superArg); // invoke the *super* constructor, an instance of someDep

      this.data = data;

      // alternatively, hide data in a closure
      this.data = function () { return data; }
    }, 

    overriddenMethod: function () {
      return this.__super__.overriddenMethod() + ' : ' + this.data; // or this.data()
    }
});

Pass in a function

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

    this.__super__(superArg);  // 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() Constructor and Constructor.extend(). Function() itself does not accept non-string arguments, unfortunately, so a Constructor function would have to be available somewhere.

[ example deleted ]

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 __super__ 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).

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s