Advanced JavaScript (ES6+)

Class Inheritance

13 min Lesson 27 of 40

Class Inheritance

Inheritance is a fundamental concept in object-oriented programming that allows you to create new classes based on existing ones. ES6 makes inheritance in JavaScript cleaner and more intuitive with the extends and super keywords.

What is Inheritance?

Inheritance allows a class (child/subclass) to inherit properties and methods from another class (parent/superclass). This promotes code reuse and establishes relationships between classes.

Key Concept: Inheritance creates an "is-a" relationship. For example, a Dog is an Animal, a Car is a Vehicle, and a Manager is an Employee.

The extends Keyword

The extends keyword is used to create a class that inherits from another class:

// Parent class (superclass) class Animal { constructor(name, age) { this.name = name; this.age = age; } makeSound() { return "Some generic animal sound"; } info() { return `${this.name} is ${this.age} years old`; } } // Child class (subclass) class Dog extends Animal { constructor(name, age, breed) { super(name, age); // Call parent constructor this.breed = breed; } makeSound() { return "Woof! Woof!"; } fetch() { return `${this.name} is fetching the ball!`; } } const myDog = new Dog("Max", 3, "Golden Retriever"); console.log(myDog.info()); // "Max is 3 years old" (inherited) console.log(myDog.makeSound()); // "Woof! Woof!" (overridden) console.log(myDog.fetch()); // "Max is fetching the ball!" (new method) console.log(myDog.breed); // "Golden Retriever"

The super Keyword

The super keyword is used to call methods from the parent class:

class Vehicle { constructor(brand, model) { this.brand = brand; this.model = model; this.speed = 0; } accelerate(amount) { this.speed += amount; return `Speed: ${this.speed} mph`; } describe() { return `${this.brand} ${this.model}`; } } class Car extends Vehicle { constructor(brand, model, doors) { super(brand, model); // Call parent constructor this.doors = doors; } // Override parent method accelerate(amount) { // Call parent method super.accelerate(amount); return `${this.describe()} accelerating... ${this.speed} mph`; } // Add new method honk() { return "Beep beep!"; } } const myCar = new Car("Toyota", "Camry", 4); console.log(myCar.accelerate(30)); // "Toyota Camry accelerating... 30 mph" console.log(myCar.accelerate(20)); // "Toyota Camry accelerating... 50 mph" console.log(myCar.honk()); // "Beep beep!"
Important: If you define a constructor in a child class, you MUST call super() before accessing this. Otherwise, you'll get a ReferenceError.

Method Overriding

Child classes can override parent methods to provide specialized behavior:

class Shape { constructor(color) { this.color = color; } area() { return 0; } describe() { return `A ${this.color} shape with area ${this.area()}`; } } class Rectangle extends Shape { constructor(color, width, height) { super(color); this.width = width; this.height = height; } // Override area method area() { return this.width * this.height; } } class Circle extends Shape { constructor(color, radius) { super(color); this.radius = radius; } // Override area method area() { return Math.PI * this.radius * this.radius; } } const rect = new Rectangle("blue", 10, 5); const circle = new Circle("red", 7); console.log(rect.describe()); // "A blue shape with area 50" console.log(circle.describe()); // "A red shape with area 153.938..."

Inheritance Chains

Classes can form inheritance chains where a child class can itself be a parent to another class:

class LivingBeing { constructor(name) { this.name = name; this.isAlive = true; } breathe() { return `${this.name} is breathing`; } } class Animal extends LivingBeing { constructor(name, species) { super(name); this.species = species; } move() { return `${this.name} is moving`; } } class Mammal extends Animal { constructor(name, species, furColor) { super(name, species); this.furColor = furColor; } nurse() { return `${this.name} is nursing its young`; } } class Dog extends Mammal { constructor(name, breed, furColor) { super(name, "Canine", furColor); this.breed = breed; } bark() { return `${this.name} says: Woof!`; } } const myDog = new Dog("Buddy", "Labrador", "Golden"); console.log(myDog.breathe()); // From LivingBeing console.log(myDog.move()); // From Animal console.log(myDog.nurse()); // From Mammal console.log(myDog.bark()); // From Dog console.log(myDog.species); // "Canine"
Caution: Deep inheritance chains can make code harder to understand and maintain. Generally, avoid going more than 2-3 levels deep. Consider composition as an alternative.

The instanceof Operator

Use instanceof to check if an object is an instance of a class (or its parent classes):

class Animal { constructor(name) { this.name = name; } } class Dog extends Animal { bark() { return "Woof!"; } } class Cat extends Animal { meow() { return "Meow!"; } } const dog = new Dog("Max"); const cat = new Cat("Whiskers"); console.log(dog instanceof Dog); // true console.log(dog instanceof Animal); // true console.log(dog instanceof Cat); // false console.log(cat instanceof Cat); // true console.log(cat instanceof Animal); // true console.log(cat instanceof Dog); // false // Useful for type checking function makeAnimalSound(animal) { if (animal instanceof Dog) { return animal.bark(); } else if (animal instanceof Cat) { return animal.meow(); } return "Unknown animal"; } console.log(makeAnimalSound(dog)); // "Woof!" console.log(makeAnimalSound(cat)); // "Meow!"

Protecting Methods with super

You can use super to extend parent functionality rather than completely replacing it:

class BankAccount { constructor(accountNumber, balance) { this.accountNumber = accountNumber; this.balance = balance; this.transactions = []; } deposit(amount) { this.balance += amount; this.transactions.push({ type: "deposit", amount, date: new Date() }); return `Deposited $${amount}. New balance: $${this.balance}`; } withdraw(amount) { if (amount > this.balance) { return "Insufficient funds"; } this.balance -= amount; this.transactions.push({ type: "withdrawal", amount, date: new Date() }); return `Withdrew $${amount}. New balance: $${this.balance}`; } } class SavingsAccount extends BankAccount { constructor(accountNumber, balance, interestRate) { super(accountNumber, balance); this.interestRate = interestRate; this.withdrawalLimit = 6; // Federal regulation this.withdrawalCount = 0; } withdraw(amount) { if (this.withdrawalCount >= this.withdrawalLimit) { return "Monthly withdrawal limit reached"; } // Call parent withdraw method const result = super.withdraw(amount); if (result.includes("Withdrew")) { this.withdrawalCount++; } return result; } addInterest() { const interest = this.balance * (this.interestRate / 100); this.deposit(interest); return `Interest added: $${interest.toFixed(2)}`; } resetWithdrawals() { this.withdrawalCount = 0; return "Monthly withdrawal count reset"; } } const savings = new SavingsAccount("SAV-001", 1000, 2.5); console.log(savings.deposit(500)); // "Deposited $500. New balance: $1500" console.log(savings.withdraw(100)); // "Withdrew $100. New balance: $1400" console.log(savings.addInterest()); // "Interest added: $35.00"

Composition vs Inheritance

While inheritance is powerful, composition (combining objects) is often a better choice:

// Inheritance approach (can get messy) class FlyingAnimal extends Animal { fly() { return "Flying"; } } class SwimmingAnimal extends Animal { swim() { return "Swimming"; } } // What about animals that both fly and swim? // Composition approach (more flexible) class Animal { constructor(name, abilities = []) { this.name = name; this.abilities = abilities; } can(ability) { return this.abilities.includes(ability); } perform(action) { if (this.can(action)) { return `${this.name} is ${action}`; } return `${this.name} cannot ${action}`; } } // Create ability objects const flying = { fly() { return "flying high"; } }; const swimming = { swim() { return "swimming gracefully"; } }; const walking = { walk() { return "walking on land"; } }; // Compose abilities const duck = new Animal("Duck", ["flying", "swimming", "walking"]); const fish = new Animal("Fish", ["swimming"]); const bird = new Animal("Eagle", ["flying", "walking"]); console.log(duck.perform("flying")); // "Duck is flying" console.log(duck.perform("swimming")); // "Duck is swimming" console.log(fish.perform("flying")); // "Fish cannot flying"
Best Practice: Prefer composition over inheritance when possible. Use inheritance for true "is-a" relationships and composition for "has-a" or "can-do" relationships.

Static Methods and Inheritance

Static methods are also inherited by child classes:

class MathOperations { static add(a, b) { return a + b; } static multiply(a, b) { return a * b; } } class AdvancedMath extends MathOperations { static power(base, exponent) { return Math.pow(base, exponent); } static squareRoot(num) { return Math.sqrt(num); } } // Child class has access to parent static methods console.log(AdvancedMath.add(5, 3)); // 8 (inherited) console.log(AdvancedMath.multiply(4, 6)); // 24 (inherited) console.log(AdvancedMath.power(2, 3)); // 8 (own method) console.log(AdvancedMath.squareRoot(16)); // 4 (own method)

Practice Exercise:

Challenge: Create an employee management system with the following requirements:

  • Employee class: name, id, salary, and calculateBonus() method (10% of salary)
  • Manager extends Employee: add teamSize, override calculateBonus() (15% + $100 per team member)
  • Developer extends Employee: add programmingLanguages array, add method to add languages
  • Designer extends Employee: add designTools array, override calculateBonus() (12% of salary)

Solution:

class Employee { constructor(name, id, salary) { this.name = name; this.id = id; this.salary = salary; } calculateBonus() { return this.salary * 0.10; } getInfo() { return `${this.name} (ID: ${this.id}) - Salary: $${this.salary}`; } } class Manager extends Employee { constructor(name, id, salary, teamSize) { super(name, id, salary); this.teamSize = teamSize; } calculateBonus() { return (this.salary * 0.15) + (this.teamSize * 100); } } class Developer extends Employee { constructor(name, id, salary, programmingLanguages = []) { super(name, id, salary); this.programmingLanguages = programmingLanguages; } addLanguage(language) { if (!this.programmingLanguages.includes(language)) { this.programmingLanguages.push(language); } return `${language} added. Known languages: ${this.programmingLanguages.join(", ")}`; } } class Designer extends Employee { constructor(name, id, salary, designTools = []) { super(name, id, salary); this.designTools = designTools; } calculateBonus() { return this.salary * 0.12; } } // Test the system const manager = new Manager("Alice", "M001", 80000, 5); console.log(manager.getInfo()); // "Alice (ID: M001) - Salary: $80000" console.log("Bonus: $" + manager.calculateBonus()); // Bonus: $12500 const dev = new Developer("Bob", "D001", 70000, ["JavaScript"]); console.log(dev.addLanguage("Python")); // "Python added. Known languages: JavaScript, Python" console.log("Bonus: $" + dev.calculateBonus()); // Bonus: $7000 const designer = new Designer("Carol", "DS001", 65000, ["Figma", "Photoshop"]); console.log("Bonus: $" + designer.calculateBonus()); // Bonus: $7800

Summary

In this lesson, you learned:

  • The extends keyword creates inheritance relationships
  • The super keyword calls parent class methods and constructors
  • Child classes can override parent methods for specialized behavior
  • Inheritance chains allow multi-level class hierarchies
  • The instanceof operator checks class membership
  • Composition is often preferable to deep inheritance
  • Static methods are inherited by child classes
Next Up: In the next lesson, we'll dive deep into prototypes and the prototype chain to understand what's happening behind the scenes with classes and inheritance!