Historical Context & Motivation
Before modern programming languages existed, developers often copied and pasted the same code over and over. If you had a program with many similar objects — like different types of vehicles, animals, or user accounts — you would write nearly identical code for each one. This was tedious, error-prone, and hard to maintain. The idea of inheritance emerged as a way to let one piece of code share its structure and behavior with another, eliminating all that duplication.
Inheritance is one of the foundational pillars of object-oriented programming (OOP), a paradigm that organizes code around "objects" — bundles of related data and functions. Languages like Simula, Smalltalk, C++, and Java all used class-based inheritance. JavaScript, however, took a different path at first, using something called prototypal inheritance under the hood. It wasn't until ES6 (ECMAScript 2015) that JavaScript gained the familiar class, extends, and super keywords that made inheritance much easier to read and write.
So here's the core question this lesson addresses: How do you create a new class that automatically gets all the properties and methods of an existing class, while also adding its own unique features? The answer lies in extends and super.
Core Principles & Definitions
Before diving into code, let's nail down the key vocabulary. Inheritance in JavaScript revolves around a few essential ideas that build on each other. Once you understand these concepts, the code will feel much more natural.
Parent (Base) Class
Child (Derived) Class
extends Keyword
class Dog extends Animal tells JavaScript that Dog inherits from Animal.super() Call
Method Overriding
super.methodName().extends), follow its basic steps (calling super()), and then add your own twist — extra spices, a new topping. You didn't rewrite the whole recipe; you just built on what was already there.Visual Explanation — The Inheritance Hierarchy
The diagram below shows how a parent class Animal passes its properties and methods down to two child classes: Dog and Cat. Notice that both children share everything the parent has, but each one also adds its own unique method.
name, age) and methods (speak(), info()). The Dog child adds breed and fetch(), while the Cat child adds isIndoor and purr().Notice the arrow labeled extends pointing from each child to the parent. This represents the inheritance relationship — the child "extends" the parent by getting all of its features. The child's own unique properties and methods appear in the colored accent text. This visual pattern — a parent at the top branching to children below — is called an inheritance hierarchy or class hierarchy.
How extends & super Work Under the Hood
Let's look at exactly how extends and super work in JavaScript. The extends keyword in a class declaration tells JavaScript to set up a prototype chain so that the child class can access the parent's methods. When you create an instance of the child class, JavaScript first looks for a property or method on the child. If it doesn't find it, it walks up the chain to the parent.
The extends Keyword
The syntax is straightforward. You write class ChildName extends ParentName { ... }. This single keyword does two important things behind the scenes. First, it links the child's prototype to the parent's prototype so that inherited methods are available. Second, it sets up the child's constructor to allow calling super().
The super() Function Call
Inside the child class's constructor() method, you must call super() before using this. The super() call runs the parent's constructor, which initializes the inherited properties. You pass any arguments the parent constructor needs. For example, if the parent expects name and age, you write super(name, age) in the child.
super.method() for Overridden Methods
You can also use super as an object — not just a function call — to access parent methods from within the child. If the child overrides speak(), it can still call the parent's version with super.speak(). This lets you extend behavior rather than completely replace it.
constructor(), you must call super() before accessing this. If you forget, JavaScript will throw a ReferenceError. If your child class doesn't define its own constructor, JavaScript automatically calls the parent's constructor for you.Anatomy of an Inheritance Example
Let's look at a complete code example and then trace through every line. The diagram below shows the full structure of a parent Animal class and a child Dog class, with annotations pointing out each key piece of syntax.
Animal class (violet border) and the child Dog class (cyan border). Dashed callout boxes explain the role of extends, super(), and method overriding.Let's trace what happens when you run const rex = new Dog('Rex', 5, 'Labrador');. First, JavaScript enters Dog's constructor with name = 'Rex', age = 5, and breed = 'Labrador'. The super(name, age) call jumps up to Animal's constructor, which sets this.name = 'Rex' and this.age = 5. Control returns to Dog's constructor, which then sets this.breed = 'Labrador'. Now rex has all three properties.
When you call rex.speak(), JavaScript finds the speak() method on the Dog class first (because it overrides the parent's version) and returns 'Rex barks!'. If Dog didn't define speak(), JavaScript would walk up to Animal and use the parent's version, returning 'Rex makes a sound'.
Worked Example — Building a Vehicle Hierarchy
Let's build a practical inheritance hierarchy from scratch. We'll create a Vehicle parent class and an ElectricCar child class, walking through each step.
Vehicle class with a constructor that accepts make, model, and year. We also add a describe() method.class Vehicle { constructor(make, model, year) { this.make = make; this.model = model; this.year = year; } describe() { return `${this.year} ${this.make} ${this.model}`; } }ElectricCar and use extends Vehicle to inherit all of Vehicle's properties and methods. This single keyword establishes the inheritance link.class ElectricCar extends Vehicle { ... }range (in miles). We call super(make, model, year) first to let Vehicle set up the inherited properties, and then we set this.range = range.constructor(make, model, year, range) { super(make, model, year); this.range = range; }describe() to include the range. Instead of rewriting the base description, we call super.describe() to get the parent's version, then append the range info.describe() { return `${super.describe()} — Electric, ${this.range} mi range`; }const myTesla = new ElectricCar('Tesla', 'Model 3', 2024, 358); Then myTesla.describe() returns the full description including both the inherited and added parts.myTesla.describe() → '2024 Tesla Model 3 — Electric, 358 mi range'super.describe() to reuse it, and only added the electric-specific information. If the parent's describe() ever changes, the child automatically gets the updated version. This is the real power of inheritance — you maintain code in one place and all children benefit.Strengths & Common Pitfalls
Inheritance is a powerful tool, but like any tool, it has both strengths and situations where it can trip you up. Understanding both sides will help you use it wisely.
| Aspect | Strengths ✅ | Pitfalls ⚠️ |
|---|---|---|
| Code Reuse | Write shared logic once in the parent. All children get it for free — no copy-pasting. | If you chain too many levels deep (grandparent → parent → child → grandchild), the code becomes hard to trace. |
| Consistency | Fixing a bug in the parent automatically fixes it in all children that rely on that method. | A change in the parent could accidentally break a child class that depends on specific behavior (the fragile base class problem). |
| Organization | Hierarchies model real-world relationships clearly (e.g., Dog is-a Animal). | Not everything fits neatly into is-a relationships. Forcing inheritance where composition is better leads to awkward designs. |
| super() Requirement | Ensures parent initialization always happens, preventing incomplete objects. | Forgetting to call super() before using this in the constructor throws a ReferenceError — a very common beginner mistake. |
Connection to Advanced Concepts
The extends and super keywords you've learned are your entry point into a much larger world of object-oriented design. As you advance, you'll encounter more sophisticated patterns that build on these fundamentals. The table below compares what you know now to what comes next.
| What You Know Now | What Comes Next |
|---|---|
| Single-level inheritance (one parent, one child) | Multi-level chains (grandparent → parent → child) and understanding when to stop going deeper |
| Method overriding with super.method() | Polymorphism — treating objects of different child classes through a common parent interface |
| Using class syntax as syntactic sugar | Understanding JavaScript's prototype chain directly (Object.getPrototypeOf, __proto__) |
| Inheritance (is-a relationships) | Composition (has-a relationships) and mixins — combining behaviors from multiple sources |
| Basic extends with built-in classes | Extending built-in classes like Error, Array, or Map for custom data structures |
Don't worry about mastering all of these right away. The important thing is that extends and super give you a solid foundation. Every advanced OOP concept in JavaScript — polymorphism, the prototype chain, mixins — grows naturally from the inheritance basics you've learned in this lesson.
Practice Problems
extends keyword does when you write class Dog extends Animal. Why is it useful instead of just copying all of Animal's code into Dog?console.log(s.describe()) output?
class Shape { constructor(color) { this.color = color; } describe() { return `A ${this.color} shape`; } }
class Circle extends Shape { constructor(color, radius) { super(color); this.radius = radius; } }
const s = new Circle('blue', 10);class Person { constructor(name) { this.name = name; } greet() { return `Hi, I'm ${this.name}`; } }
class Student extends Person { constructor(name, grade) { super(name); this.grade = grade; } greet() { return `${super.greet()} and I'm in grade ${this.grade}`; } }
What does new Student('Aisha', 10).greet() return? Explain how super.greet() is used in the child's method.Enemy with properties name and health, and a method attack() that returns a string. Then write a child class Dragon that adds an element property (like 'fire') and overrides attack() to include the element.class Animal { constructor(name) { this.name = name; } }
class Cat extends Animal { constructor(name, isIndoor) { this.isIndoor = isIndoor; super(name); } }
Explain what the error is, why it happens, and how to fix it. Then explain: if Cat didn't define a constructor at all, would new Cat('Whiskers') still work? Why or why not?Lesson Summary
In this lesson, you learned how JavaScript's extends keyword creates a child (subclass) that inherits all properties and methods from a parent (superclass). The super() function call inside the child's constructor runs the parent's constructor first, ensuring inherited properties are properly initialized. You must always call super() before using this in a child constructor.
You also explored method overriding, where a child class redefines a parent method, and learned that super.methodName() lets you call the parent's version from within the override. Inheritance works best for is-a relationships (Dog is-a Animal, ElectricCar is-a Vehicle). These foundational skills prepare you for more advanced OOP patterns like polymorphism, prototype chains, and composition.