JavaScriptPrototype Inheritance

Prototype Inheritance

Before ES2015 classes, JavaScript already had a full inheritance system — it was just spelled differently. You combined a constructor function with the prototype property on that function to share methods between instances. Learning the classical pattern is worth the time: class is just a thin layer on top of it, and a few of the awkward edges still show through.

Constructor functions

Any regular function can be called with new. When you do, JavaScript creates a fresh object, sets its prototype to the function's prototype property, runs the function body with this bound to that new object, and returns it.

JS
function Animal(name) {
  this.name = name;
  this.alive = true;
}

const cat = new Animal("Mittens");
console.log(cat);   // Animal { name: 'Mittens', alive: true }

By convention, constructor functions are named in PascalCase. That is the only signal to readers that the function is meant to be called with new.

The prototype property

Every function has a prototype property — an empty object by default. Methods added there are shared by every instance created with new.

JS
function Animal(name) {
  this.name = name;
}

Animal.prototype.greet = function () {
  return `Hi, I am ${this.name}`;
};

const a = new Animal("Cat");
const b = new Animal("Dog");

console.log(a.greet());                       // 'Hi, I am Cat'
console.log(a.greet === b.greet);             // true — same function
console.log(Object.getPrototypeOf(a) === Animal.prototype);  // true

Both a and b share one function object — far cheaper than putting greet directly on each instance.

Inheriting from another constructor

To make Rabbit inherit from Animal, you set up two links: Rabbit.prototype must inherit from Animal.prototype, and the Rabbit constructor must call Animal so the parent fields are initialised on this.

Pre-class inheritance pattern

JS
function Animal(name) {
  this.name = name;
}
Animal.prototype.greet = function () {
  return `Hi, I am ${this.name}`;
};

function Rabbit(name, colour) {
  Animal.call(this, name);     // "super" — initialise the parent fields
  this.colour = colour;
}

// Make Rabbit.prototype inherit from Animal.prototype
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;          // fix the back-reference

Rabbit.prototype.hop = function () {
  return `${this.name} hops`;
};

const bunny = new Rabbit("Roger", "white");
console.log(bunny.greet());   // 'Hi, I am Roger' — inherited
console.log(bunny.hop());     // 'Roger hops' — own
console.log(bunny instanceof Rabbit);   // true
console.log(bunny instanceof Animal);   // true
Three lines that are easy to forget
The ritual `Rabbit.prototype = Object.create(Parent.prototype); Rabbit.prototype.constructor = Rabbit; Parent.call(this, ...)` is exactly what `class Rabbit extends Animal` does for you. Forgetting any one of them leads to subtle bugs.
How classes desugar

The class syntax (ES2015) is the same machinery with a cleaner spelling. Compare the two forms side by side:

With class

JS
class Animal {
  constructor(name) { this.name = name; }
  greet() { return `Hi, I am ${this.name}`; }
}

class Rabbit extends Animal {
  constructor(name, colour) {
    super(name);
    this.colour = colour;
  }
  hop() { return `${this.name} hops`; }
}

Behind the scenes:

  • class Animal is a function whose body is the constructor.

  • greet is added to Animal.prototype — exactly like the manual version.

  • extends Animal sets Rabbit.prototype's prototype to Animal.prototype and Rabbit's own prototype to Animal (so static methods chain too).

  • super(name) is Animal.call(this, name) — initialising the parent fields.

instanceof and the chain

a instanceof Ctor walks up a's prototype chain looking for Ctor.prototype. If it finds it anywhere, the answer is true.

JS
console.log(bunny instanceof Rabbit);   // true
console.log(bunny instanceof Animal);   // true
console.log(bunny instanceof Object);   // true — top of the chain
Methods on Object.prototype

Because the chain bottoms out at Object.prototype, every object inherits a small set of methods: toString, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, valueOf. Overriding toString is a common, painless customisation:

JS
function Money(amount, currency) {
  this.amount = amount;
  this.currency = currency;
}
Money.prototype.toString = function () {
  return `${this.amount} ${this.currency}`;
};

const price = new Money(42, "GBP");
console.log(`That'll be ${price}`);   // 'That'll be 42 GBP'
Why know the pre-class pattern?
Most libraries written before 2016 use it, and the language semantics still describe it — including the way `this` and inheritance work in `class`. Reading old code, or debugging an unexpected `Object` chain, is much easier when the underlying mechanics are familiar.
One sentence
A constructor function plus its `prototype` object is JavaScript's inheritance engine. `class` and `extends` are syntactic sugar that wires the prototype chain for you.