Classical Inheritance in JavaScript

Notes for an upcoming discussion.

With some work, JavaScript can be used in an Object-Oriented design pattern similar to Java or C++. This can be a good thing, with programmers who are familiar with these languages moving to JavaScript for the first time. Setting aside the discussion on which inheritance pattern — prototypal or classical — is better JavaScript for now.

Class Definition

To define a class in JavaScript, create a function:


// a simple Product class
function Product(id, name) {
    this.id = id;
    this.name = name;
}

Product.prototype = {
    getName: function() {
        return this.name;
    }
}

With a little more work, we can create a class with private variables:


var Product = function(id, name) {
    var myId = id;
    this.name = name;
    this.getId = function() {
        return myId;
    }
}

Product.prototype = {
    getName: function() {
        return this.name;
    }
}

To use the class, it should be instantiated:

var myProduct = new Product('1', 'My cool widget');

myProduct.getId();   // "1"
myProduct.getName(); // "My cool widget";

The Product class inherits the prototype from Object. So methods from Object exist in the instance:

typeof myProduct.toString; // "function"

Note: this method for creating private variables can create larger objects. Every copy of Product gets a copy of the getId function, instead of accessing the prototype. If your Product Class is more complicated, or you needed to create a large number of copies of products, this might be a design issue to consider.

It is possible to change the prototype of a Class; all instance of the class receive the update immediately:

Product.prototype.getName = function() {
    return this.name.toUpperCase();
}

myProduct.getName(); // "MY COOL WIDGET"

It is possible to override methods from the prototype chain:

myProduct.hasOwnProperty('getName'); // false.. the instance inherits the method from the prototype.

myProduct.getName = function() {
    return this.name.toLowerCase();
}

myProduct.getName(); // "my cool widget"
myProduct.hasOwnProperty('getName'); // true.. the instance overrides the prototype.

This works up the chain as well:

myProduct.toString(); // "[object Object]"

Product.prototype.toString = function() {
    return this.name;
}

myProduct.toString(); // "My cool widget"

The new keyword, which was used to instantiate an instance of the class does several things.

  1. It creates a new empty object of type Object.
  2. it sets this new object’s internal, inaccessible, [[prototype]] (or __proto__) property to be the constructor function’s external, accessible, prototype object.
  3. It executes the constructor function, using the new object whenever ‘this’ is mentioned. If the constructor function returns a value, this is returned. Otherwise the new object is returned.

Classical Inheritance

Let us suppose that you have several different types of Products, and all of the different types of Products share some common attributes, and that all different types of products have some unique attributes that are not shared. It would be nice to keep all of the shared attributes in a base class, and create classes that inherit the base product’s attributes.

To create a class that inherits from the base Product Class several lines of code are needed. JavaScript does not have a extend keyword or any other built in method for creating inherited objects, so a bit of the work is done manually.

// assume a base Product Class already exists
var Widget = function(id, name, type) {
    Product.call(this, id, name);
    this.type = type || 'generic';
};

Widget.prototype = new Product(); // set the prototype chain
Widget.prototype.constructor = Widget; // constructor was reset above, set it back.

// Add methods unique to Widgets
Widget.prototype.getType = function() {
    return this.type;
};

var myWidget = new Widget('2', 'A Widget');
myWidget.getName(); // "A Widget"
myWidget.getType(); // "generic"

To cut down on the amount of typing, and to be more efficient, a simple function to extend one object (subclass) with a base class (superclass) can be created. In this function, a new empty function is created to avoid having to instantiate a potentially large super class object each time.

function extend(subClass, superClass) {
    var F = function() {};
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
    subClass.prototype.constructor = subClass;
}

This does the same thing that we did manually in the example above. Use this extend function directly after declaring the constructor:

// assume a base Product object exists
var Widget = function(id, name, type) {
    Product.call(this, id, name);
    this.type = type;
}

extend(Widget, Product); 

Widget.prototype.getType = function() {
    return this.type;
}

This works, but our inherited class still calls the Product class directly. This tight coupling is undesirable, and can be avoided by alerting our extend helper function to extend one class (a subclass) from a base class (the superclass) and set the a reference to the superclass.

function extend(subClass, superClass) {
    var F = function() {};
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
    subClass.prototype.constructor = subClass;
    
    // set a reference to the super class 
    subClass.superclass = superClass.prototype;
    if (superClass.prototype.constructor == Object.prototype.constructor) {
        superClass.prototype.constructor = superClass;
    }
}

// Pro JavaScript Design Patterns: © 2008 Harmes/Diaz; apress

Now inherited classes have a convenient way to refer back to the superclass, which can be useful in methods that override the base class.

// assume the base Product Class and the extend function
var Widget = function(id, name, type) {
    Widget.superclass.constructor.call(this, id, name);
    this.type = type || 'generic';
}

extend(Widget, Product);

Widget.prototype.getName = function() {
    var name = Widget.superclass.getName.call(this);
    return name + ' (' + this.type + ')';
};

var myWidget = new Widget('2', 'a widget');
myWidget.getName();

Leave a Reply

Your email address will not be published. Required fields are marked *