JavaScript Class Inheritance: Complete Tutorial
A complete guide to JavaScript class inheritance using extends and super. Learn how to create class hierarchies, call parent constructors, override methods, use super.method(), and understand instanceof with multi-level inheritance.
Class inheritance allows one class to build on another, reusing and extending behavior. JavaScript's extends keyword establishes the inheritance relationship, and super provides access to the parent class. Understanding how these work — and the rules around them — is core to writing object-oriented JavaScript effectively.
The extends Keyword
extends creates a child class (subclass) that inherits from a parent class (superclass):
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
// Dog extends Animal — inherits constructor and speak()
class Dog extends Animal {
fetch() {
return `${this.name} fetches the ball!`;
}
}
const rex = new Dog("Rex");
console.log(rex.speak()); // "Rex makes a sound." (inherited)
console.log(rex.fetch()); // "Rex fetches the ball!" (own method)
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // trueDog.prototype is linked to Animal.prototype via the prototype chain. When rex.speak() is called, JavaScript looks on rex (not found), then Dog.prototype (not found), then Animal.prototype (found!).
The super() Call in the Constructor
When a child class defines its own constructor, it MUST call super() before using this. This is enforced by the engine:
class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
}
describe() {
return `${this.make} ${this.model}`;
}
}
class Car extends Vehicle {
constructor(make, model, year) {
super(make, model); // ← MUST be first use of 'this'
this.year = year; // Own property added after super()
}
describe() {
return `${this.year} ${super.describe()}`; // Call parent describe()
}
}
const myCar = new Car("Toyota", "Camry", 2023);
console.log(myCar.describe()); // "2023 Toyota Camry"Rule: If you define a constructor in a derived class, super() must be called before referencing this or before the constructor returns. Violating this throws a ReferenceError.
No constructor in child class: If you omit the constructor entirely, JavaScript inserts this automatically:
constructor(...args) {
super(...args);
}Overriding Methods
A child class redefines a method from the parent by declaring a method with the same name:
class Shape {
constructor(color) { this.color = color; }
area() { return 0; }
toString() { return `Shape(color=${this.color}, area=${this.area().toFixed(2)})`; }
}
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
area() { // Overrides Shape.area()
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(color, w, h) {
super(color);
this.width = w;
this.height = h;
}
area() { // Overrides Shape.area()
return this.width * this.height;
}
}
const shapes = [
new Circle("red", 5),
new Rectangle("blue", 4, 6),
];
shapes.forEach(s => console.log(s.toString()));
// "Shape(color=red, area=78.54)"
// "Shape(color=blue, area=24.00)"Notice toString() is defined once in Shape and calls this.area() — each subclass area() override is picked up automatically because this refers to the actual instance.
super.method() — Calling Parent Methods
Use super.methodName() to call the parent class implementation:
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
class TimestampLogger extends Logger {
log(message) {
const ts = new Date().toISOString();
super.log(`${ts}: ${message}`); // Extends parent behavior
}
}
class PrefixLogger extends TimestampLogger {
constructor(prefix) {
super();
this.prefix = prefix;
}
log(message) {
super.log(`[${this.prefix}] ${message}`); // Chains up through the hierarchy
}
}
const logger = new PrefixLogger("APP");
logger.log("Starting..."); // "[LOG] 2026-...: [APP] Starting..."For more details on the super keyword's mechanics, see using the super keyword in JavaScript classes.
Multi-Level Inheritance
Inheritance can go multiple levels deep:
class LivingThing {
constructor(name) { this.name = name; }
breathe() { return `${this.name} breathes.`; }
}
class Animal extends LivingThing {
constructor(name, legs) {
super(name);
this.legs = legs;
}
move() { return `${this.name} moves on ${this.legs} legs.`; }
}
class Dog extends Animal {
constructor(name) {
super(name, 4);
}
bark() { return `${this.name} barks!`; }
}
const buddy = new Dog("Buddy");
console.log(buddy.breathe()); // "Buddy breathes." (from LivingThing)
console.log(buddy.move()); // "Buddy moves on 4 legs." (from Animal)
console.log(buddy.bark()); // "Buddy barks!" (from Dog)The prototype chain: buddy → Dog.prototype → Animal.prototype → LivingThing.prototype → Object.prototype → null
Class Hierarchy vs Composition
Deep inheritance hierarchies become rigid. JavaScript allows class composition patterns like mixins:
// Mixin — a function that extends a class
const Serializable = (Base) => class extends Base {
serialize() { return JSON.stringify(this); }
static deserialize(json) { return Object.assign(new this(), JSON.parse(json)); }
};
const Timestamped = (Base) => class extends Base {
constructor(...args) {
super(...args);
this.createdAt = new Date().toISOString();
}
};
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
// Compose behaviors with mixins
class FullUser extends Timestamped(Serializable(User)) {}
const u = new FullUser("Alice", "alice@example.com");
console.log(u.serialize()); // JSON string with name, email, createdAtinstanceof in Class Hierarchies
| Check | Dog extends Animal |
|---|---|
dog instanceof Dog | true |
dog instanceof Animal | true |
dog instanceof Object | true |
animal instanceof Dog | false (parent is not an instance of child) |
Object.getPrototypeOf(Dog.prototype) === Animal.prototype | true |
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
const d = new Dog();
const c = new Cat();
console.log(d instanceof Animal); // true
console.log(c instanceof Dog); // false
console.log(d instanceof Cat); // falsePreventing Inheritance With final-Like Patterns
JavaScript has no final keyword, but you can throw in the constructor to prevent direct instantiation:
class AbstractShape {
constructor() {
if (new.target === AbstractShape) {
throw new Error("AbstractShape cannot be instantiated directly");
}
}
area() { throw new Error("area() must be implemented"); }
}
class Square extends AbstractShape {
constructor(side) { super(); this.side = side; }
area() { return this.side ** 2; }
}
// new AbstractShape() → Error
const s = new Square(5);
console.log(s.area()); // 25new.target refers to the constructor called with new. In a direct new AbstractShape() call, new.target === AbstractShape.
Rune AI
Key Insights
- extends sets up prototype chaining: Dog.prototype's [[Prototype]] is Animal.prototype — method lookup traverses the chain automatically
- super() is mandatory in child constructors: Must be called before any reference to this; the parent constructor allocates and initializes the this object
- Method overriding is automatic: Define a method with the same name in the child class and it shadows the parent's version for that instance
- super.method() calls the parent implementation: Useful when you want to extend rather than replace parent behavior
- instanceof traverses the full chain: An instance of Dog is also an instance of Animal and Object; the check is not just the immediate prototype
Frequently Asked Questions
Why must super() come before this in a derived constructor?
Can I extend a constructor function (not a class) with class extends?
Can I extend a built-in like Array or Error?
What is the difference between override and overwrite?
Conclusion
JavaScript class inheritance with extends and super provides a clean, readable way to build object hierarchies. The child class acquires all parent methods via prototype chaining. The super() call in the constructor is mandatory in derived classes before this is accessible. Method overriding lets child classes specialize behavior while super.method() allows extending it. instanceof traces the full prototype chain, so instances of child classes are also instances of their ancestors.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.