JavaScript prototype inheritance
Repeat after me: JavaScript does not have classes
JavaScript has objects. Each object has a prototype, which is either null
or another object. Prototypes can be chained. When we ask for a property of an object that is not directly set on that object, the prototype chain is searched.
In modern browsers (basically IE9+ and everything else you care about), you can easily create a new object with another object as its prototype using:
var someObject = Object.create(someOtherObject);
Also in modern browsers, you can find out the prototype of an object with:
// would return someOtherObectj in this case
Object.getPrototypeOf(someObject)
In older browsers (see http://kangax.github.io/compat-table/es5/), you would need an empty constructor function (see below) and the new
keyword to create someObject
JavaScript has a class-like concept that involves constructor functions. It works like this (adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create):
// A 'superclass'.
var Shape = function(x, y) {
this.x = x;
this.y = y;
};
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
};
// A 'subclass'
var Circle = function(x, y, radius, color) {
Shape.call(this, x, y); // call the 'superclass' constructor
this.radius = radius;
this.color = color;
};
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
Circle.prototype.grow = function(amount) {
this.radius += amount;
};
It is now possible to do:
var circle1 = new Circle(10, 10, 100, 'blue');
circle1.move(5, 5); // calls the function from `Shape.prototype`
circle1.grow(10); // calls the function form `Circle.prototype`
circle1 instanceof Shape; // true
circle1 instanceof Circle; // true
Here's what's going on:
We define a constructor function called
Shape
. It uses thethis
keyword to set properties on objects created by it.The property
Shape.prototype
is an empty object ({}
) by default. It specifies the prototype for new objects created with theShape
constructor. Note the careful wording here: This object isn't the prototype forShape()
(a constructor function), it's the prototype for all objects created withnew Shape()
.We set a property
move
onShape.prototype
. This means that if we create an objectobj
usingnew Shape()
, that doesn't otherwise have amove
property, and then try to callobj.move()
, JavaScript will find this object in the prototype.We prefer to define functions on the prototype rather than set them with
this.move = function () {}
because this means the function is defined only once, not each time the constructor function is called!We then define another constructor function called
Circle
.We have chosen to call the
Shape()
function to help us set up the object. This is analogous to calling a "super-constructor" in an object-oriented language, but please note that this is entirely incidental. NeitherShape
norCircle
is a class, they are simply functions. They don't know anything about each other. All we do is call theShape()
function bound to the samethis
context as theCircle()
function.We then set up the prototype chain. To do this, we replace the default prototype instance for objects created with
new Circle()
, with a new object that hasShape.prototype
as its prototype.Note: If you have to support older browsers that don't have
Object.create()
, you might consider usingnew Shape()
instead ofObject.create(Shape.prototype)
. The problem with this is that thenew Shape()
constructor function may have side-effects that you don't necessarily want.When we do this, we will have the wrong
constructor
property, so we fix this up by setting it back to theCircle
class. Theconstructor
property is simply a reference back to the function that was used to create an object.We can now add new properties to the prototype, such as the
grow()
function.
Here is a slightly less verbose version that uses underscore
(http://underscorejs.org/) to copy properties:
// A 'superclass'.
var Shape = function(x, y) {
this.x = x;
this.y = y;
};
Shape.prototype = _.extend(Object.create(null), {
constructor: Shape,
move: function(x, y) {
this.x += x;
this.y += y;
}
});
// A 'subclass'
var Circle = function(x, y, radius, color) {
Shape.call(this, x, y); // call the 'superclass' constructor
this.radius = radius;
this.color = color;
};
Circle.prototype = _.extend(Object.create(Shape.prototype), {
constructor: Circle,
grow: function(amount) {
this.radius += amount;
}
});
In this case, we are using _.extend()
(which simply copies all properties from its second argument onto the first argument, and then returns the modified object) to set up any new properties in a single object literal, rather than repeating the <Constructor>.prototype.<name>
lines.
Technically, the Shape
class could be simplified further (we could just set Shape.prototype
to an object literal), but using the pattern consistently is likely to be less confusing.
There are various libraries out there that attempt to provide syntactic sugar for inheritance of properties. Backbone (http://backbonejs.org/) supports this pattern:
var MyModel = Backbone.Model.extend({
aProperty: ...,
constructor: function(...) {}
});
In this model, the constructor
property takes the place of defining a constructor function).
It does this by defining these two, rather well-commented functions, which are instructive to understand:
// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var extend = function(protoProps, staticProps) {
var parent = this;
var child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
// Add static properties to the constructor function, if supplied.
_.extend(child, parent, staticProps);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate;
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) _.extend(child.prototype, protoProps);
// Set a convenience property in case the parent's prototype is needed
// later.
child.__super__ = parent.prototype;
return child;
};
Backbone.Model.extend = extend;
See also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain
Written by Martin Aspeli
Related protips
1 Response
There is a "standalone" version of the Backbone extend()
function here: https://github.com/gre/backbone-extend-standalone