Monday, February 2, 2015

Exploring the smell of Javascript inheritance

In Javascript, objects inherit from other objects. By convention, the objects that are inherited from (prototypes) contain methods, while the inheriting objects contain data. This makes sense, as it is practical to share methods, but each instance must have its own data.
Given that we are interested in (only) the methods of the parent object, it strikes me as something of a code smell that inheritance is done by taking an instance of an object - including, in particular, its data members - to use as a new prototype to which we add functions. It's convenient, it's relatively low cost (what are a few unused variables, for a whole class), but you're still creating things that you will never use. For example, say we have this class:
function Button (label, position) {
  this.label = label;
  this.position = position;
}
Button.prototype = {
  // Button functions
  ...
};
You might derive a new subclass like so:
function RoundButton(size, label, position) {
  Button.call(this, label, position);
  this.size = size;
}
RoundButton.prototype = new Button();
RoundButton.prototype.newMethod = function () {
  // additional functions
  ...
};
You only create the new Button instance so you can inherit from its prototype and add methods to it. The data members don't matter at all to the prototype; you want them in the instance (hence the Button.call).
You could have the Button constructor check to see whether it receives any arguments, and if not, it doesn't create its members. That makes Button responsible for two things: constructing new buttons and constructing new prototypes for derived classes. Another approach would be to separate the prototype responsibility into another class:
function DerivedButton() {}
DerivedButton.prototype = Button.prototype;
Then RoundButton.prototype would be declared:
RoundButton.prototype = new DerivedButton();
Interestingly, because DerivedButton and Button share a prototype, deriving RoundButton from DerivedButton will make RoundButton instanceof Button yield true.
It looks like Object.create was set up to have prototypes inherit through prototypes and not through object instances (as well as to facilitate more traditional object-oriented features).