JavaScript Inheritance
Inheritance lets one object or class reuse and extend the behavior of another, so you don’t repeat the same properties and methods everywhere. In JavaScript, inheritance is not a copy-paste of code between objects — it’s a live link called the prototype chain that the engine walks every time you access a property. Understanding that chain is the key to understanding almost everything else in JavaScript OOP, including how the modern class/extends/super syntax actually works underneath.
Overview / How It Works
Every object in JavaScript has an internal, hidden link to another object (or to null) called its prototype. This internal link is officially named [[Prototype]], and you can read or write it through Object.getPrototypeOf(obj) and Object.setPrototypeOf(obj, proto), or informally through the non-standard-but-widely-supported __proto__ accessor. When you access a property on an object, the JavaScript engine first looks for an own property directly on that object. If it isn’t found, the engine follows the [[Prototype]] link to the next object up the chain, and keeps climbing until it either finds the property or reaches null, at which point it returns undefined. This chain of linked objects is called the prototype chain, and it is the entire mechanism behind inheritance in JavaScript — there is no separate “class” system at the bytecode level, even when you write class syntax.
Before ES6, inheritance was built manually with constructor functions: every function has a .prototype property (an ordinary object), and objects created with new SomeFunction() get their internal [[Prototype]] set to SomeFunction.prototype. To inherit from another constructor, developers set Child.prototype = Object.create(Parent.prototype) and called Parent.call(this, ...) inside the child constructor to reuse the parent’s setup logic. ES6 introduced class, extends, and super as syntax sugar over exactly this mechanism. A class declaration is still a function under the hood, its methods still live on .prototype, and extends still wires up the same [[Prototype]] links — but the syntax is clearer, enforces calling the parent constructor before using this, and adds features like super.method() to call an overridden parent method.
It’s important to separate two different links that both matter for inheritance. First, every instance has a [[Prototype]] pointing to its constructor’s .prototype object — this is how instances share methods. Second, when you use class Child extends Parent, the constructor functions themselves are also linked: Object.getPrototypeOf(Child) === Parent. This second link is what makes static methods and properties inherited too — Child.someStaticMethod() can find a method defined as static on Parent even though no instance is involved.
Syntax
class Child extends Parent {
constructor(args) {
super(parentArgs); // must run before using `this`
this.ownProp = value;
}
method() {
super.method(); // calls Parent's version of method()
}
}
| Piece | Meaning |
|---|---|
extends |
Declares that Child.prototype‘s internal [[Prototype]] is Parent.prototype, and links the constructors for static inheritance. |
super(...) |
Calls the parent class’s constructor. Required before any use of this in a derived class constructor. |
super.method() |
Calls the parent’s version of an overridden method, from inside a method. |
Object.create(proto) |
The lower-level, pre-class way to create an object with a specific prototype — still used for pure prototypal inheritance. |
Object.getPrototypeOf(obj) |
Reads an object’s [[Prototype]] link. |
instanceof |
Checks whether a constructor’s .prototype appears anywhere in an object’s prototype chain. |
Examples
Example 1: Class-based inheritance with extends and super
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
super.speak();
console.log(`${this.name} barks.`);
}
}
const d = new Dog('Rex', 'Labrador');
d.speak();
console.log(d instanceof Animal);
console.log(d instanceof Dog);
Output:
Rex makes a sound.
Rex barks.
true
true
Dog extends Animal, so Dog.prototype‘s [[Prototype]] is Animal.prototype. Calling super(name) runs Animal‘s constructor with this already bound to the new Dog instance, setting this.name. Inside the overridden speak(), super.speak() explicitly calls Animal.prototype.speak, then the method adds its own behavior. Because the prototype chain includes both Dog.prototype and Animal.prototype, both instanceof checks succeed.
Example 2: Pure prototypal inheritance with constructor functions
function Vehicle(wheels) {
this.wheels = wheels;
}
Vehicle.prototype.describe = function () {
return `This vehicle has ${this.wheels} wheels.`;
};
function Car(brand) {
Vehicle.call(this, 4);
this.brand = brand;
}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.prototype.describe = function () {
const base = Vehicle.prototype.describe.call(this);
return `${base} It is a ${this.brand}.`;
};
const c = new Car('Toyota');
console.log(c.describe());
console.log(Object.getPrototypeOf(c) === Car.prototype);
Output:
This vehicle has 4 wheels. It is a Toyota.
true
This is exactly what class/extends does behind the scenes. Vehicle.call(this, 4) borrows the parent constructor’s logic (the equivalent of super(4)). Object.create(Vehicle.prototype) builds a fresh object whose [[Prototype]] is Vehicle.prototype and assigns it as Car.prototype, wiring up the chain. Resetting Car.prototype.constructor = Car is necessary because replacing Car.prototype wholesale also replaced its default constructor property — an easy step to forget when doing this manually.
Example 3: Overriding getters and inheriting static members
class Shape {
static count = 0;
constructor(name) {
this.name = name;
Shape.count++;
}
get label() {
return `Shape: ${this.name}`;
}
static describe() {
return `${this.count} shapes created`;
}
}
class Circle extends Shape {
constructor(radius) {
super('circle');
this.radius = radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
get label() {
return `${super.label} (radius ${this.radius})`;
}
}
const circ = new Circle(2);
console.log(circ.label);
console.log(circ.area.toFixed(2));
console.log(Circle.describe());
Output:
Shape: circle (radius 2)
12.57
1 shapes created
The label getter is overridden in Circle, but it calls super.label to reuse Shape‘s version rather than duplicating the string logic. Circle.describe() is never defined on Circle itself; because classes also link their constructors (Object.getPrototypeOf(Circle) === Shape), the static method — and the static count field it reads through this — is found by walking up to Shape.
Under the Hood: Step by Step
When you write d.speak() from Example 1, the engine performs roughly these steps:
- Look for an own property called
speakdirectly on the objectd. It isn’t there —donly hasnameandbreedas own properties. - Follow
d‘s[[Prototype]]link toDog.prototype. Look forspeakthere. It is found, because the class body’sspeak() {...}was installed onDog.prototypewhen the class was defined. - Call that function with
thisbound tod. - Inside it,
super.speak()does a different lookup: it starts searching not fromd, but from the prototype ofDog.prototype— i.e.Animal.prototype— using a special internal binding called[[HomeObject]]that every method created inside a class body carries. This is whysuperworks correctly even thoughthisis still theDoginstance. - If neither
Dog.prototypenorAnimal.prototypehadspeak, the engine would keep climbing toObject.prototype, and finally tonull, returningundefinedfor a property lookup or throwing aTypeErrorfor a call.
This walk happens on every single property access, every time — prototypes are not “copied” onto instances, so if you later add a method to Animal.prototype, every existing Dog instance instantly gains it too, because the lookup happens live at access time.
Common Mistakes
1. Using this before calling super(). In a derived class constructor, JavaScript requires super() to run first, because the parent constructor is responsible for actually creating the this binding for the instance. Code like class Cat extends Animal { constructor(name) { this.name = name; super(name); } } throws a ReferenceError: Must call super constructor before accessing 'this'. The fix is simply to call super(...) as the first statement in the constructor before touching this in any way.
2. Defining methods inside the constructor instead of on the prototype. This wastes memory and breaks the shared-behavior benefit of prototypes, since every instance gets its own separate copy of the function.
class Counter {
constructor() {
this.count = 0;
this.increment = function () {
this.count++;
};
}
}
const a = new Counter();
const b = new Counter();
console.log(a.increment === b.increment);
Output:
false
Defining the method normally inside the class body puts it on the shared prototype instead:
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
}
}
const a = new Counter();
const b = new Counter();
console.log(a.increment === b.increment);
Output:
true
3. Putting mutable objects or arrays directly on a shared prototype. Because all instances see the same prototype object through the chain, mutating a reference-type property on the prototype affects every instance at once.
function Team() {}
Team.prototype.members = [];
const teamA = new Team();
const teamB = new Team();
teamA.members.push('Alice');
console.log(teamB.members);
Output:
[ 'Alice' ]
Neither instance has its own members property, so teamA.members and teamB.members both resolve to the exact same array on Team.prototype. The fix is to assign such properties inside the constructor (this.members = []) so each instance gets its own independent array as an own property.
Best Practices
- Prefer
class/extends/supersyntax over manual prototype wiring for readability — it compiles down to the same mechanism but is far less error-prone. - Always call
super(...)as the very first line of a derived constructor, before referencingthis. - Keep inheritance hierarchies shallow. Deep chains (more than 2-3 levels) make property lookups slower and code harder to reason about — favor composition (“has-a”) over inheritance (“is-a”) when a relationship doesn’t clearly model a true subtype.
- Put instance-specific mutable state (arrays, objects, counters) in the constructor as own properties, not on the shared prototype.
- Use
super.method()to extend, not blindly replace, inherited behavior when overriding — it keeps the parent’s logic in one place. - Use
instanceofor duck-typing (checking for a method’s existence) rather than manually inspecting[[Prototype]]chains in application code. - Avoid mutating built-in prototypes like
Array.prototypeorObject.prototype— it’s a classic source of hard-to-debug conflicts in larger codebases.
Practice Exercises
Exercise 1: Create a base class Employee with a constructor that takes name and salary, and a method describe() that logs ". Create a subclass Manager that adds a teamSize property and overrides describe() to call the parent’s version with super.describe() and then also log the team size.
Exercise 2: Using plain constructor functions and Object.create (no class keyword), build a Shape “parent” with an area() method returning 0, and a Square “child” whose own area() overrides it to return side * side. Verify with instanceof-style checks using Object.getPrototypeOf.
Exercise 3: Predict the output before running: define a class A with a static property version = 1 and a class B extends A with no static members of its own. What does B.version log, and why does that work without B redeclaring it?
Summary
- Inheritance in JavaScript is implemented through the prototype chain — a live linked list of objects connected via the internal
[[Prototype]]slot. - Property lookups walk up the chain from the instance to its prototype, and further up, until found or
nullis reached. class/extends/superis syntax sugar over the same mechanism used by constructor functions andObject.create.super()must be called beforethisis used in a derived constructor;super.method()calls the parent’s version of an overridden method.- Static members are inherited too, because
extendsalso links the constructor functions themselves, not just their.prototypeobjects. - Keep instance-owned mutable state off shared prototypes to avoid accidental cross-instance mutation bugs.
