Last Updated: February 25, 2016
·
2.058K
· optilude

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 the this 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 the Shape constructor. Note the careful wording here: This object isn't the prototype for Shape() (a constructor function), it's the prototype for all objects created with new Shape().

  • We set a property move on Shape.prototype. This means that if we create an object obj using new Shape(), that doesn't otherwise have a moveproperty, and then try to call obj.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. Neither Shape nor Circle is a class, they are simply functions. They don't know anything about each other. All we do is call the Shape() function bound to the same this context as the Circle() 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 has Shape.prototype as its prototype.

  • Note: If you have to support older browsers that don't have Object.create(), you might consider using new Shape() instead of Object.create(Shape.prototype). The problem with this is that the new 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 the Circle class. The constructor 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

1 Response
Add your response

There is a "standalone" version of the Backbone extend() function here: https://github.com/gre/backbone-extend-standalone

over 1 year ago ·