“Class”-like Inheritance, Overriding, and Superclass Calls in JavaScript

JavaScript is a prototype-based language with objects, rather than classes, as its fundamental construct. However, the use of functions to approximate classes (complete with the “new” keyword) is very common. Although we’re often told that JavaScript supports inheritance, it’s not entirely obvious how the class-like inheritance works in a prototype-based language such as JavaScript. Adding to the confusion is various syntax approaches that may be used to devise objects and “classes”.

As an aside, the techniques shown in this post require ES5 support, which, for example, excludes IE 8 and below. There are fairly simple shims that can be used to enable support in older browsers.

We’ll first consider a “naive” approach to defining a class in JavaScript, then discuss why this approach isn’t suitable for an inheritance scenario.

function BaseClass(initialValue) {
  this.value = initialValue;
  this.compute = function() { return this.value + 1; };
};

This class seems to work fine:

var instance = new BaseClass(3);
console.log(instance.compute()); // outputs 4

However, this approach is not suitable for inheritance. The function “compute” (and any other functions defined in this way) will be defined not on the “class” (prototype) but on the object itself. When overridden, there will be no reasonable way to access the base function, because the override will actually replace the original function on that object. Thus, functions should be defined on the prototype, while fields are defined on the object itself. If you define a field on the prototype, it is like a static member in C#.

function BaseClass(initialValue) {
  this.value = initialValue;
};
BaseClass.prototype.compute = function() { return this.value + 1; };

This produces the same result as before, but now the function is defined on the prototype instead of the object, so it can be referenced by derived classes.

Now imagine we want to construct a derived class. There are two important considerations: ensuring that the super-constructor is called correctly, and ensuring that “instanceof” continues to give the correct result. There is no “base” or “super” keyword in JavaScript, so you must use the actual name of the base class whenever you want to refer to it.

function DerivedClass(initialValue, secondValue) {
  BaseClass.call(this, initialValue); // *** Call super-constructor
  this.anotherValue = secondValue; // Additional field
};
// *** Establish prototype relationship
DerivedClass.prototype = Object.create(BaseClass.prototype); 

// Additional function
DerivedClass.prototype.anotherCompute = 
   function() { return this.compute() + this.anotherValue; }; 

Now let’s look at the things we can do with an instance of the derived class. They should all work as expected.

var instance2 = new DerivedClass(4, 6);
// base method with base fields: outputs 5 (4+1)
console.log(instance2.compute()); 

// derived method with base method and derived field: 
// outputs 11 ((4+1)+6)
console.log(instance2.anotherCompute()); 

// outputs true
console.log(instance2 instanceof DerivedClass); 

// ALSO outputs true
console.log(instance2 instanceof BaseClass); 

Now imagine we want to create a derived class that overrides some behavior of the base class. First, we will create a class inheriting from the previous derived class, and override the compute method.

function ThirdClass(initialValue, secondValue) {
  // In this case, the "base" class 
  // of this class is called "DerivedClass"
  DerivedClass.call(this, initialValue, secondValue); 

  // no new member fields
};
ThirdClass.prototype = Object.create(DerivedClass.prototype); 

// override base function compute
ThirdClass.prototype.compute = function() { return 100; }; 

Let’s see this overridden function in action. The override also impacts, for example, the “anotherCompute” function since it calls compute. The overriding works exactly as you would expect as in, say, C#.

var instance3 = new ThirdClass(4, 6);
// overridden method always outputs 100
console.log(instance3.compute()); 

// derived method with OVERRIDDEN method (inside) and derived field: 
// outputs 106 (100+6)
console.log(instance3.anotherCompute()); 

// confirm that the compute has only been overridden 
// for instances of ThirdClass: 
// outputs 5, as before
console.log(instance2.compute()); 

One final task remains: can we override a function, and then still make a call to the base class version of that function? In this case, we’ll replace the definition of compute in the third class with one that overrides compute while also making a class to the base class definition of compute. This is where the technique for function definition (defining on the prototype vs. defining on the object) becomes critical. Since the functions are actually defined on the prototypes, we can call back to the base class prototype for that version of the function. Remember that “ThirdClass” derives from “DerivedClass”

// override base function compute, but still use it in this function
ThirdClass.prototype.compute = function() { 
  return DerivedClass.prototype.compute.call(this) * 5; 
}; 

Now we’ll retry the two instance3 calls from earlier:

// overridden method calls base implementation of compute, 
// which returns 5, and multiplies it by 5. output: 25
console.log(instance3.compute()); 

// derived method with overridden method
// (inside, which calls base implementation) 
// and derived field: outputs 31 ((5*5)+6)
console.log(instance3.anotherCompute()); 

We now have a reliable technique for constructing “class”es in JavaScript which fully support inheritance, overriding, and superclass calls in the way conventional from class-based object-oriented languages.

Remember, though, that this is “writing C#/Java in JavaScript” and should be used sparingly. Also, some authors believe implementation inheritance is usually bad even in languages which support it as a primary form of reuse.

Comments are closed.