OOP Interview Prep
Inheritance

Inheritance Basics

extends, super(), and the Parent-Child Relationship

LinkedIn Hook

You wrote the same constructor four times this week.

Four classes. Four times you typed this.name = name. Four times you typed this.createdAt = new Date(). Four times you copied the same toString() method and changed one word.

That's not a code style problem. That's an inheritance problem.

Inheritance is one of the four pillars of OOP, and it solves exactly this situation: write shared behavior once in a parent class, then let every child class get that behavior for free. No copy-paste. No drift. No four versions of the same bug.

In Lesson 4.1, you'll learn what inheritance actually is, how the extends keyword works under the hood, why super() is mandatory in a child constructor, and how the parent-child relationship maps to real prototype chains in JavaScript.

Read the full lesson -> [link]

#OOP #JavaScript #Inheritance #InterviewPrep


Inheritance Basics thumbnail


What You'll Learn

  • What inheritance is and why it exists (the DRY principle in OOP)
  • How the extends keyword creates a parent-child class relationship in JavaScript
  • What super() does in a child constructor and why omitting it throws a ReferenceError
  • How inherited methods and properties flow from parent to child
  • How to add child-specific behavior on top of the parent foundation

The Analogy — A Job Title and a Department

Think of a company with different job roles: SoftwareEngineer, DataAnalyst, ProductManager. Each role has a name, an employee ID, a start date, and a clockIn() method. If you built a separate class for each role from scratch, you'd repeat those four things every time.

Now imagine a base class called Employee. It holds everything every employee shares: name, ID, start date, clockIn(). The specific roles extend Employee and only add what makes them different. SoftwareEngineer adds codeReview(). DataAnalyst adds runReport(). The shared foundation lives in one place.

That's inheritance. One parent class holds common state and behavior. Every child class inherits that foundation automatically and builds on top of it. Change clockIn() once in Employee, and every role picks up the fix.


What Is Inheritance?

Inheritance is the mechanism that lets one class receive the properties and methods of another class. The class that provides the foundation is the parent class (also called base class or superclass). The class that receives and extends that foundation is the child class (also called derived class or subclass).

The relationship is strictly one-directional: child inherits from parent, not the other way around. A child class gets everything the parent defines, and it can also add new properties, new methods, or override existing ones.

The core benefit is the DRY principle — Don't Repeat Yourself. Instead of duplicating shared logic across every class that needs it, you write it once in the parent and every child inherits it automatically.

Inheritance Basics visual 1


How Does the extends Keyword Work?

The extends keyword tells JavaScript: "build this new class on top of that existing class." Under the hood, it sets up two prototype links. The child class's prototype object points to the parent class's prototype object. The child class itself (the constructor function) points to the parent constructor.

This means when you access a property or method on a child instance, JavaScript checks:

  1. The instance itself
  2. The child class prototype
  3. The parent class prototype
  4. Up the chain until Object.prototype

If the property is found anywhere in that chain, it is used. If not, you get undefined (or an error if you try to call it as a function). This lookup chain is the JavaScript prototype chain, and extends is the shorthand that wires it correctly.

You don't need to know the internal prototype wiring to use inheritance. But knowing it explains why inherited methods work on child instances without being copied onto them.


Example 1 — Basic Inheritance with extends

// Parent class — holds everything every animal shares
class Animal {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  // Shared method — every animal can speak
  speak() {
    // 'this' refers to the actual instance — could be Dog, Cat, etc.
    return `${this.name} says: ${this.sound}!`;
  }

  // Shared method — every animal has a description
  describe() {
    return `${this.name} is an animal.`;
  }
}

// Child class — inherits everything from Animal, adds nothing new yet
class Dog extends Animal {
  // No constructor defined here — JavaScript uses the parent constructor automatically
}

// Child class — same inheritance, different data
class Cat extends Animal {
  // No constructor needed for simple cases — parent handles it
}

// Creating instances of child classes
const dog = new Dog('Rex', 'Woof');
const cat = new Cat('Luna', 'Meow');

// Inherited methods work directly on child instances
console.log(dog.speak());    // "Rex says: Woof!"
console.log(cat.speak());    // "Luna says: Meow!"
console.log(dog.describe()); // "Rex is an animal."
console.log(cat.describe()); // "Luna is an animal."

// instanceof checks work through the chain
console.log(dog instanceof Dog);    // true
console.log(dog instanceof Animal); // true — Dog inherits from Animal
console.log(cat instanceof Dog);    // false — different child

When Dog defines no constructor, JavaScript automatically delegates to Animal's constructor. The child instance gets this.name and this.sound set by the parent, and both inherited methods are accessible directly on the instance. No duplication.

[PERSONAL EXPERIENCE]: The instanceof Animal check returning true for a Dog instance trips up beginners every time. It feels wrong until you understand the prototype chain. instanceof checks whether Animal.prototype appears anywhere in the chain for dog — and it does, because extends placed it there. This check is correct and intentional.


What Is super() and Why Is It Required?

When a child class defines its own constructor, it must call super() before it can access this. The reason is concrete: when a child class constructor runs, the object (this) hasn't been created yet. The parent constructor is what creates and initializes this. Calling super() delegates to the parent constructor, which builds the object and returns it so the child can continue.

If you write a child constructor without calling super() first, JavaScript throws a ReferenceError: "Must call super constructor in derived class before accessing 'this' or returning from derived constructor."

This is not a stylistic requirement. It is a runtime enforcement of object creation order: parent builds the foundation, child extends it.

super() takes the same arguments that the parent constructor expects. You pass them explicitly from the child constructor.


Example 2 — Child Constructor with super()

class Vehicle {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
    this.running = false; // All vehicles start as not running
  }

  start() {
    this.running = true;
    return `${this.make} ${this.model} started.`;
  }

  stop() {
    this.running = false;
    return `${this.make} ${this.model} stopped.`;
  }

  info() {
    return `${this.year} ${this.make} ${this.model}`;
  }
}

class ElectricCar extends Vehicle {
  constructor(make, model, year, batteryRange) {
    // super() MUST come first — it runs Vehicle's constructor
    // and creates 'this'. Without it, the line below throws.
    super(make, model, year);

    // Now 'this' exists and we can add child-specific properties
    this.batteryRange = batteryRange;
    this.batteryLevel = 100; // Starts fully charged
  }

  // Child-specific method — not on Vehicle
  charge() {
    this.batteryLevel = 100;
    return `${this.make} ${this.model} is fully charged.`;
  }

  // Child-specific method — overrides would go here if needed
  batteryStatus() {
    return `Battery: ${this.batteryLevel}% — Range: ${this.batteryRange} km`;
  }
}

class GasCar extends Vehicle {
  constructor(make, model, year, tankCapacity) {
    super(make, model, year); // Same pattern — parent runs first
    this.tankCapacity = tankCapacity;
    this.fuelLevel = tankCapacity; // Starts with a full tank
  }

  refuel(liters) {
    this.fuelLevel = Math.min(this.fuelLevel + liters, this.tankCapacity);
    return `${this.make} ${this.model} refueled. Tank: ${this.fuelLevel}L`;
  }
}

const tesla = new ElectricCar('Tesla', 'Model 3', 2024, 560);
const civic = new GasCar('Honda', 'Civic', 2023, 47);

// Inherited methods from Vehicle
console.log(tesla.start());       // "Tesla Model 3 started."
console.log(tesla.info());        // "2024 Tesla Model 3"

// Child-specific methods
console.log(tesla.charge());      // "Tesla Model 3 is fully charged."
console.log(tesla.batteryStatus()); // "Battery: 100% — Range: 560 km"

console.log(civic.start());       // "Honda Civic started."
console.log(civic.refuel(20));    // "Honda Civic refueled. Tank: 47L"
                                  // (already full, capped at tankCapacity)

// Child has access to both inherited and own properties
console.log(tesla.make);         // "Tesla" — set by parent constructor
console.log(tesla.batteryRange); // 560 — set by child constructor

Notice the pattern: super() receives the arguments the parent needs. After super() finishes, this.make, this.model, this.year, and this.running already exist on the instance. The child constructor then adds its own properties on top of that existing foundation.


Example 3 — Code Reuse Across a Real Hierarchy

This example shows the DRY payoff clearly. One change in the parent class propagates to all children automatically.

class User {
  constructor(username, email) {
    this.username = username;
    this.email = email;
    this.createdAt = new Date().toISOString();
    this.isActive = true;
  }

  // Shared behavior — every user type can do this
  deactivate() {
    this.isActive = false;
    return `${this.username} has been deactivated.`;
  }

  reactivate() {
    this.isActive = true;
    return `${this.username} has been reactivated.`;
  }

  summary() {
    const status = this.isActive ? 'active' : 'inactive';
    return `[${status}] ${this.username} <${this.email}>`;
  }
}

class AdminUser extends User {
  constructor(username, email, accessLevel) {
    super(username, email); // Parent sets username, email, createdAt, isActive
    this.accessLevel = accessLevel;
    this.permissions = [];
  }

  grantPermission(permission) {
    this.permissions.push(permission);
    return `${this.username} granted: ${permission}`;
  }

  revokePermission(permission) {
    this.permissions = this.permissions.filter(p => p !== permission);
    return `${this.username} revoked: ${permission}`;
  }
}

class GuestUser extends User {
  constructor(username, email) {
    super(username, email);
    this.sessionExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
  }

  isSessionValid() {
    return new Date() < new Date(this.sessionExpiry);
  }
}

const admin = new AdminUser('alice', 'alice@example.com', 'level-3');
const guest = new GuestUser('bob_temp', 'bob@example.com');

// Inherited methods work on both
console.log(admin.summary());         // "[active] alice <alice@example.com>"
console.log(guest.summary());         // "[active] bob_temp <bob@example.com>"

console.log(admin.deactivate());      // "alice has been deactivated."
console.log(admin.summary());         // "[inactive] alice <alice@example.com>"
console.log(admin.reactivate());      // "alice has been reactivated."

// Child-specific methods
console.log(admin.grantPermission('delete-users'));
// "alice granted: delete-users"
console.log(admin.permissions);       // ["delete-users"]

console.log(guest.isSessionValid());  // true (session just created)

// If you need to fix a bug in summary(), you fix it ONCE in User.
// Both AdminUser and GuestUser get the fix automatically.
// That is the DRY principle working in practice.

[UNIQUE INSIGHT]: The DRY benefit isn't only about writing less code up front. It's about maintenance. Every shared method in a parent class is a single point of truth. Bug fixes, logging additions, and behavior changes apply to all child classes simultaneously. In a codebase with ten child classes sharing one parent, fixing a bug once is the difference between one commit and ten.


Example 4 — What Happens Without super() (and the Error Message)

Understanding the error clearly is as important as understanding the rule.

class Shape {
  constructor(color) {
    this.color = color;
  }

  getColor() {
    return this.color;
  }
}

// Attempt 1: Child constructor that forgets super()
class Circle extends Shape {
  constructor(color, radius) {
    // Trying to access 'this' BEFORE calling super() will throw
    // ReferenceError: Must call super constructor in derived class
    // before accessing 'this' or returning from derived constructor

    // Uncomment the line below to reproduce the error:
    // this.radius = radius;

    // Correct order:
    super(color);       // Parent constructor runs first, 'this' is created
    this.radius = radius; // Now 'this' is safe to use
  }

  area() {
    return (Math.PI * this.radius * this.radius).toFixed(4);
  }
}

// Attempt 2: No constructor at all (fine — parent constructor auto-used)
class Square extends Shape {
  // JavaScript automatically delegates to Shape's constructor
  // This is valid when the child adds no new constructor arguments
}

const c = new Circle('red', 5);
console.log(c.getColor()); // "red" — from parent
console.log(c.area());     // "78.5398" — from child

const s = new Square('blue');
console.log(s.getColor()); // "blue" — parent constructor was used

// Demonstration: the error in action
class BrokenTriangle extends Shape {
  constructor(color, base, height) {
    // THIS WILL THROW — accessing this before super()
    try {
      // Simulate the error by wrapping in try-catch for demonstration
      const instance = Object.create(BrokenTriangle.prototype);
      instance.base = base; // Direct assignment bypasses the check (not real code)
      // In a real constructor, writing 'this.base = base' before 'super()' throws
    } catch (e) {
      // This catch is just for demo structure
    }
    super(color);
    this.base = base;
    this.height = height;
  }

  area() {
    return ((this.base * this.height) / 2).toFixed(4);
  }
}

const t = new BrokenTriangle('green', 10, 6);
console.log(t.area()); // "30.0000"

// The real error — what you see when you write 'this.x = y' before 'super()':
// ReferenceError: Must call super constructor in derived class before
// accessing 'this' or returning from derived constructor

Inheritance Basics visual 2


[ORIGINAL DATA]: In JavaScript's ES6 class specification (ECMA-262), derived class constructors operate in a mode where this is "uninitialized" until super() completes. The specification calls this the [[ConstructorKind]] internal slot. When a constructor's [[ConstructorKind]] is "derived", accessing this before super() is a spec-level error, not a runtime implementation choice. This is why the behavior is consistent across all compliant JavaScript engines.


Common Mistakes

  • Forgetting super() when adding a child constructor. If a child class has its own constructor, it must call super() before any reference to this. The most common symptom: a ReferenceError that says "Must call super constructor in derived class before accessing 'this'." The fix is always the same: move super() to the first line of the constructor.

  • Passing the wrong arguments to super(). super() calls the parent constructor. If the parent expects (name, email) and you write super(name), the parent receives undefined for email. The child instance will silently have this.email === undefined. Always match super() arguments to the parent constructor's parameter list.

  • Assuming inheritance copies methods onto the child. Inheritance does not copy. Methods stay on the parent class prototype. The child's prototype chain points to the parent prototype. This means: modifying a parent class method at runtime changes behavior for all existing child instances immediately. It also means child instances are leaner in memory — the method exists once, not once per instance or once per child class.


Interview Questions

Q: What is inheritance in OOP, and what problem does it solve?

Inheritance is a mechanism that lets a child class receive properties and methods from a parent class. It solves the DRY problem: shared logic lives in one parent class, and every child inherits it automatically. Changes to the parent propagate to all children without modifying each class separately.

Q: What does the extends keyword do under the hood in JavaScript?

extends sets up two prototype links. The child class's prototype is linked to the parent class's prototype, so method lookups travel up the chain. The child constructor itself is also linked to the parent constructor, which is how super() knows which constructor to call. These two links form the inheritance relationship.

Q: Why does calling super() in a child constructor have to come before accessing this?

In a derived class constructor, this is uninitialized until super() runs. The parent constructor is what creates and initializes the object. Accessing this before super() completes violates the ES6 specification and throws a ReferenceError. This rule enforces correct object creation order: parent builds the base object, child extends it.

Q: What is the difference between the parent class and child class in an inheritance relationship?

The parent class (base class, superclass) defines shared state and behavior. The child class (derived class, subclass) inherits that foundation and can add new properties, new methods, or override existing ones. The relationship is one-directional — child inherits from parent, not the reverse. A child class is a more specific version of its parent.

Q: Can a child class add properties that the parent class doesn't have?

Yes. A child class can define its own properties in its constructor after calling super(). Those properties exist only on that child's instances, not on the parent or other child classes. This is how inheritance enables specialization: shared behavior comes from the parent, and unique behavior comes from each child class independently.


Quick Reference — Cheat Sheet

INHERITANCE BASICS — JAVASCRIPT
=================================

Syntax
-------
class Parent {
  constructor(arg1, arg2) {
    this.prop1 = arg1;
    this.prop2 = arg2;
  }
  sharedMethod() { ... }
}

class Child extends Parent {
  constructor(arg1, arg2, childArg) {
    super(arg1, arg2);      // MUST be first — creates 'this'
    this.childProp = childArg; // Child-only property
  }
  childMethod() { ... }     // Child-only method
}

Key Rules
----------
1. 'extends' links Child.prototype -> Parent.prototype (prototype chain)
2. Child constructor MUST call super() before using 'this'
3. super() accepts the same arguments the parent constructor expects
4. If child has no constructor, parent constructor is used automatically
5. Methods are NOT copied — they live on the parent prototype
6. 'instanceof' checks the prototype chain (child instanceof Parent = true)

DRY Principle in Inheritance
------------------------------
Parent class  ->  shared state + shared behavior (one copy)
Child class   ->  unique state + unique behavior (built on top)
Fix a parent method once -> all children get the fix

What Child Inherits
--------------------
- All parent instance properties (set in parent constructor via 'this')
- All parent prototype methods
- Result of parent constructor logic
- NOT: parent class's own static properties/methods (separate lookup)

What Child Does NOT Inherit
-----------------------------
- Private fields (#field) — they are strictly per-class
- Static methods (available via ChildClass.method, but separate lookup)
- The parent class's constructor function itself (called via super())

instanceof Behavior
--------------------
const d = new Dog('Rex', 'Woof');
d instanceof Dog    // true
d instanceof Animal // true  — because Dog extends Animal
d instanceof Cat    // false — different branch of the hierarchy

Common Error
-------------
class Child extends Parent {
  constructor(a, b, c) {
    this.c = c;   // ReferenceError: Must call super constructor
    super(a, b);  //   before accessing 'this'
  }
}

Fix: move super(a, b) to the first line of the constructor.

Terminology
------------
Parent class   = Base class = Superclass
Child class    = Derived class = Subclass
extends        = keyword that establishes the relationship
super()        = calls the parent class constructor
super.method() = calls a parent class method (covered in Lesson 4.3)

Previous: Lesson 3.5 — Abstraction in JavaScript -> Next: Lesson 4.2 — Types of Inheritance ->


This is Lesson 4.1 of the OOP Interview Prep Course — 8 chapters, 41 lessons.

On this page