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.
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.
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); // trueBoth 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
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); // trueHow classes desugar
The class syntax (ES2015) is the same machinery with a cleaner spelling. Compare the two forms side by side:
With class
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 Animalis a function whose body is theconstructor.greetis added toAnimal.prototype— exactly like the manual version.extends AnimalsetsRabbit.prototype's prototype toAnimal.prototypeandRabbit's own prototype toAnimal(so static methods chain too).super(name)isAnimal.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.
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:
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'