Sunday, July 12, 2015

JavaScript Inheritance Best Practices

Many sources document two major techniques and many minor variations to implement inheritance in JavaScript: constructors and prototypes. But constructors and prototypes are not exclusive, in fact they are inclusive and should always be used together. If you just use constructors or you just use prototypes, then you only did half of the job...

The best practices for JavaScript inheritance are:
  1. Move all of your methods out of the constructors and into the prototypes.
  2. Always define all of your properties in the prototype, it helps to document the new "type."
  3. Always use Object.create to link a new object to the "super-type" prototype, never build an instance of the "super-type" as the new prototype object. And always recreate the constructor property in the prototype.
  4. And always chain the constructor calls to the "super-type" constructor, even if they do not take any arguments and may not even do anything.
To explain them we are going to start from the beginning:

Constructors

The new operator works with a "constructor" function. While the constructor is executing "this" is a reference to the "context object," the new object being created:

// Rectangle
//

var Rectangle = function (width, height) {
@tab;
@tab;this.width = width;
@tab;this.height = height;
@tab;
@tab;this.volume = function () {
@tab;@tab;
@tab;@tab;console.log('Rectangle.volume');
@tab;@tab;return this.width * this.height;
@tab;};
};

var x = new Rectangle(16, 9);
var y = new Rectangle(8, 4);

console.log('The volume of x:', x.volume);

// The console output is:
// The volume of x: 144

In javascript you always have to reference "this" inside the constructor, an identifier without "this" is taken to be a local variable. Everything that the constructor adds to the object is duplicated every time the constructor is used, so the objects produced all have the same properties:



Right away we run headlong into a down-side to building objects through a constructor this way: when JavaScript encounters a function definition it has to create a function-object is created to hold the definition. So the volume property ends up with a reference to the new function-object. Every time a Rectangle is instantiated, a new function-object will be created for the new Rectangle being created. That is a whole lot of function-objects. Hold onto that thought, we will come back to it in a minute.

Inheritance can be achieved by calling the constructor method of an existing type inside the constructor of a new type, applying what the other constructor does to the new context object being created:

// Shape
//

var Shape = function () {

@tab;this.color = 'White';
};

// Rectangle
// Based on Shape and adds width, height, and volume.
//

var Rectangle = function (width, height) {
@tab;
@tab;Shape.call(this);
@tab;
@tab;this.width = width;
@tab;this.height = height;
@tab;
@tab;this.volume = function () {
@tab;@tab;
@tab;@tab;console.log('Rectangle.volume');
@tab;@tab;return this.width * this.height;
@tab;};
};

// Box
// Based on rectangle and adds depth.
//

var Box = function (width, height, depth) {
@tab;
@tab;Rectangle.call(this, width, height);
@tab;
@tab;this.depth = depth;

@tab;_volume = this.volume;
@tab;
@tab;this.volume = function () {
@tab;@tab;
@tab;@tab;console.log('Box.volume');
@tab;@tab;return _volume.call(this) * this.depth;
@tab;};
});

var x = new Box(16, 9, 3);
var y = new Box(8, 4, 2);

console.log('The color of x:', x.color);
console.log('The volume of x:', x.volume());
console.log('x instanceof Box:', x instanceof Box);
console.log('x instanceof Rectangle:', x instanceof Rectangle);
console.log('x instanceof Shape:', x instanceof Shape);

// The console output is:
// The color of x: White
// Rectangle.volume
// Box.volume
// The volume of x: 32
// x instanceof Box: true
// x instanceof Rectangle: false
// x instanceof Shape: false

Notice how the call method attached to the function object makes this work. The first argument to call method is the context object the function should be invoked with, "this" because we want it to use the same object that we are building. The remaining arguments are the arguments to the "super-type" constructor function.

So the downside of lots of objects being created is getting worse. Now there is one for every Rectangle and another for every Box! And that is not all. The volume functions are closures, function definitions made inside of an executing function. Since JavaScript puts local variables in function scope, any local variables inside of Box have to be preserved and present when volume is called!

JavaScript makes that happen by attaching all local variables to an anonymous object named the function environment. The function environment is bound to any newly defined closures. When the closure volume is called later, _volume will still be around in the function environment and the closure can use it. But to make that work, a new function environment is created every time Box is used. So now both a function-object and the function environment are created for every Box. Double the objects:


And we still have another problem: the instanceof operator works by checking to see if the prototype of a constructor function is in the prototype chain. So instanceof does not work in this example because Rectangle.prototype and Shape.prototype are not on the prototype chain of a new Box.

Prototypes

So enter prototypes. Prototypal inheritance is achieved by attaching a chain of prototype objects to a newly created object. When JavaScript cannot find a property on an object, it checks the prototype object. If it still cannot find the property, it checks the prototype's prototype. It keeps checking up the chain of prototypes for the property until it either finds it or it runs out of prototypes.

Prototypes allow defining properties that are shared by a group of other objects, a perfect place to move the volume function so it does not get created over and over again. In fact it is so good, a prototype object already comes attached to every function-object we create! All we have to do is move the function definition to Rectangle.prototype and every Rectangle will share the same volume method:

// Rectangle
//

var Rectangle = function (width, height) {
@tab;
@tab;this.width = width;
@tab;this.height = height;
@tab;
};

Rectangle.prototype.volume = function () {
@tab;
@tab;console.log('Rectangle.prototype.volume');
@tab;return this.width * this.height;
};

var x = new Rectangle(16, 9);
var y = new Rectangle(8, 4);

console.log('The volume of x:', x.volume());
console.log('The volume of y:', y.volume());

// The console output is:
// Rectangle.prototype.volume
// The volume of x: 144
// Rectangle.prototype.volume
// The volume of y: 32

The reference in the prototype property in Rectangle is copied by JavaScript to the [[Prototype]] property of each new object created by new Rectangle. That is the property that JavaScript follows to look for properties when it cannot find them in the target object. [[Prototype]] may have different names in different JavaScript engines, for example in Chrome it shows up as __proto__. It is normally not directly accessible, and cannot be changed. If you want to know what the reference is you can retrieve it with Object.getPrototypeOf(object):


We added the function to the prototype using a straight assignment. It is really tempting to simply replace the prototype with a literal. You will see many examples that look just like this:

Rectangle.prototype = {

@tab;volume: function () {
@tab;@tab;
@tab;@tab;console.log('Rectangle.volume');
@tab;@tab;return this.width * this.height;
@tab;}
};

You can find examples of this technique all over the place. The problem with this is when JavaScript creates the original prototype object it is not empty:


Notice the non-enumerable constructor property that references the constructor function-object to which the prototype is attached. You cannot see this property with for..in, and the Chrome developer tools show it in a lighter color. The property is defined by the ECMA-262 standard, so if we are going to replace the prototype with a new object then that property must be recreated:

Rectangle.prototype = {

@tab;constructor: Rectangle,

@tab;volume: function () {
@tab;@tab;
@tab;@tab;console.log('Rectangle.prototype.volume');
@tab;@tab;return this.width * this.height;
@tab;}
};

Technically we did a half-baked job. The constructor property should not be enumerable, so we cannot just assign it in the literal object. We need to skip assigning it in the literal and create it through a call to Object.defineProperty:

Rectangle.prototype = { ... }

Object.defineProperty(Rectangle.prototype, 'constructor', {
@tab;configurable: false,
@tab;enumerable: false,
@tab;value: Rectangle
});

Not a lot of people want to take that extra step though, it is a bit ugly. So recreating it in the literal as I did a moment ago is probably sufficient, and at least is a step in the right direction.

While we are at, it would be a good idea to list all of the properties with default values in the prototype, even though they should get created by the constructor. Doing this makes sure that all the properties will exist with a value when client code looks for them, and it documents to anyone maintaining the code all of the properties that should be there:

Rectangle.prototype = {

@tab;constructor: Rectangle,
@tab;height: 0,
@tab;width: 0,

@tab;volume: function () {
@tab;@tab;
@tab;@tab;console.log('Rectangle.prototype.volume');
@tab;@tab;return this.width * this.height;
@tab;}
};

But what about the linking to the "super-type" prototype in the next scenario? The Box constructor is chaining to the Rectangle constructor and the Rectangle constructor is chaining to the Shape constructor. But Box.prototype is not chained to Rectangle.prototype, and Rectangle.prototype is not chained to Shape.prototype. So a Box instance is still not an instanceof a Rectangle, nor is it an instanceof a Shape:

// Shape
//

var Shape = function() {

};

Shape.prototype = {

@tab;color: 'White',
@tab:constructor: Shape
};

// Rectangle
// Based on Shape and adds width and height.
//

var Rectangle = function (width, height) {

@tab;Shape.call(this);

@tab;this.width = width;
@tab;this.height = height;
};

Rectangle.prototype = {
@tab;
@tab;constructor: Rectangle,
@tab;height: 0,
@tab;width: 0,
@tab;
@tab;volume: function () {
@tab;@tab;
@tab;@tab;console.log('Rectangle.prototype.volume');
@tab;@tab;return this.width * this.height;
@tab;}
};

// Box
// Based on rectangle and adds depth.
//

var Box = function (width, height, depth) {
@tab;
@tab;Rectangle.call(this, width, height);
@tab;
@tab;this.depth = depth;
}

Box.prototype = {
@tab;
@tab;constructor: Box,
@tab;depth: 0,
@tab;
@tab;volume: function () {
@tab;@tab;
@tab;@tab;console.log('Box.prototype.volume');
@tab;@tab;return Rectangle.prototype.volume.call(this) * this.depth;
@tab;}
};

var x = new Box(16, 9, 3);
var y = new Box (8, 4, 2);

console.log('x instanceof Box:', x instanceof Box);
console.log('x instanceof Rectangle:', x instanceof Rectangle);
console.log('x instanceof Shape:', x instanceof Shape);

// The console output is:
// x instanceof Box: true
// x instanceof Rectangle: false
// x instanceof Shape: false

This happens because each prototype has its own prototype. The default [[Prototype]] of a new prototype links to Object.prototype. The box x is an instanceof a Box, and also an instanceof an Object:


We could beat that by using a new instance of a Shape as the prototype for a Rectangle (the prototype would be Shape.prototype), and a new instance of a Rectangle as the prototype for a Box (the prototype would be Rectangle.prototype). So there are examples all over the Internet that do exactly that. Do not forget to recreate the constructor property!

Box.prototype = new Rectangle();
Box.prototype.constructor = Box; Box.prototype.volume = function () {
@tab;
@tab;console.log('Box.prototype.volume');
@tab;return Rectangle.prototype.volume.call(this) * this.depth;
};

var x = new Box(16, 9, 3);

console.log('The volume of x:', x.volume());
console.log('x instanceof Box:', x instanceof Box);
console.log('x instanceof Rectangle:', x instanceof Rectangle);
console.log('x instanceof Shape:', x instanceof Shape);

// The console output is:
// Rectangle.prototype.volume
// Box.prototype.volume
// The volume of x: 432
// x instanceof Box: true
// x instanceof Rectangle: true
// x instanceof Shape: true

But this is ugly. There are all of those property assignments. And it creates a huge problem: when we create a new Rectangle the constructor runs for it. The constructor creates properties in the rectangle we don't want: width and height. Those two properties interrupt the inheritance chain and will get found for a Box instead of Rectangle.prototype.width and Rectangle.prototype.height.

We would not notice it with a Box because it always override the width and the height, but what if that did not happen? Instead of going up the chain to Rectangle.prototype, we would stop at the Box.prototype that does have them defined (with undefined values).

So the correct way to create a new prototype object that links to the "super-type" prototype object is to use Object.create and Object.assign. These low-level API functions are designed to create a new object linked to a specific prototype object, and mix-in the new prototype values:

Box.prototype = Object.assign(Object.create(Rectangle.prototype), {
@tab;
@tab;constructor: Box,
@tab;depth: 0,
@tab;
@tab;volume: function () {
@tab;@tab;
@tab;@tab;console.log('Box.prototype.volume');
@tab;@tab;return Object.getPrototypeOf(Box.prototype).volume.call(this) * this.depth;

@tab;}
});

var x = new Box(16, 9, 3);

console.log('The volume of x:', x.volume());
console.log('x instanceof Box:', x instanceof Box);
console.log('x instanceof Rectangle:', x instanceof Rectangle);
console.log('x instanceof Shape:', x instanceof Shape);

// The console output is:
// Rectangle.prototype.volume
// Box.prototype.volume
// The volume of x: 432
// x instanceof Box: true
// x instanceof Rectangle: true
// x instanceof Shape: true

Notice that we also changed how the volume method references the "super-type" volume method: Object.getPrototypeOf(Box.prototype).volume.call(this). We know what object we are part of: Box.prototype. But we do not know for sure that our prototype is Rectangle.prototype, that is set only because Object.create was given it. Better to programmatically find our prototype at run-time and use the volume from that.

Here is what the prototype chain looks like now:

Object.assign is a mix-in, a function that mixes in the properties of one or more objects into a target. If Object.assign is not defined substitute another mix-in from Underscore.js, jQuery, or some other library. Or, simply define it and rely on the ECMA-262 version if it does exist:

// Object.assign
//

if (!Object.assign) {
@tab;
@tab;Object.assign = function(target) {
@tab;@tab;
@tab;@tab;for (var i = 1; i < arguments.length; i++) {
@tab;@tab;@tab;
@tab;@tab;@tab;for (var p in arguments[i]) {
@tab;@tab;@tab;@tab;
@tab;@tab;@tab;@tab;target[p] = arguments[i][p];
@tab;@tab;@tab;}
@tab;@tab;}
@tab;
@tab;@tab;return target;
@tab;};
}


Best JavaScript Inheritance Practices

Here is a recap of the best practices:
  1. Move all of your methods out of the constructors and into the prototypes.
  2. Always define all of your properties in the prototype, it helps to document the new "type."
  3. Always use Object.create to link a new object to the "super-type" prototype, never build an instance of the "super-type" as the new prototype object. And always recreate the constructor property in the prototype.
  4. And always chain the constructor calls to the "super-type" constructor, even if they do not take any arguments and may not even do anything.
By the way, forgetting to chain the constructors is the most common mistake that I see. Always chain the constructors, even here in the full inheritance example where the Shape constructor does not actually do anything. Someday that constructor might actually do something. You can download a zip file with the examples at http://askjoelit.com/download/JavaScriptBestPractices.zip.

// Shape
//

var Shape = function () {
@tab;
};

Shape.prototype = Object.assign(Object.create(Object.prototype), {
@tab;
@tab;color: 'White',
@tab;constructor: Shape
});

// Rectangle
// Based on Shape and adds width and height.
//

var Rectangle = function (width, height) {
@tab;
@tab;Shape.call(this);
@tab;
@tab;this.width = width;
@tab;this.height = height;
};

Rectangle.prototype = Object.assign(Object.create(Shape.prototype), {
@tab;
@tab;constructor: Rectangle,
@tab;height: 0,
@tab;width: 0,
@tab;
@tab;volume: function () {
@tab;@tab;
@tab;@tab;console.log('Rectangle.prototype.volume');
@tab;@tab;return this.width * this.height;
@tab;}
});

// Box
// Based on rectangle and adds depth.
//

var Box = function (width, height, depth) {
@tab;
@tab;Rectangle.call(this, width, height);
@tab;
@tab;this.depth = depth;
}

Box.prototype = Object.assign(Object.create(Rectangle.prototype), {
@tab;
@tab;constructor: Box,
@tab;depth: 0,
@tab;
@tab;volume: function () {
@tab;@tab;
@tab;@tab;console.log('Box.prototype.volume');
@tab;@tab;return Object.getPrototypeOf(Box.prototype).volume.call(this) * this.depth;
@tab;}
});
@tab;
var x = new Box(16, 9, 3);
@tab;
console.log('The color of x:', x.color);
console.log('The volume of x:', x.volume());
console.log('x instanceof Box:', x instanceof Box);
console.log('x instanceof Rectangle', x instanceof Rectangle);
console.log('x instanceof Shape', x instanceof Shape);

// The console output is:
// The color of x: White
// Rectangle.prototype.volume
// Box.prototype.volume
// The volume of x: 432
// x instanceof Box: true
// x instanceof Rectangle: true
// x instanceof Shape true


No comments:

Post a Comment