Skip to main content
โšก Calmops

Inheritance: Extends and Super in JavaScript

Inheritance: Extends and Super in JavaScript

Introduction

Inheritance is a core concept in object-oriented programming that allows you to create a hierarchy of classes where child classes inherit properties and methods from parent classes. ES6 classes make inheritance straightforward with the extends and super keywords. Understanding how to properly use inheritance is essential for building scalable, maintainable applications.

In this article, you’ll learn how to create class hierarchies, use the extends keyword, call parent methods with super, and implement proper inheritance patterns.

Basic Class Inheritance

The extends Keyword

The extends keyword creates a child class that inherits from a parent class.

// Parent class
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    return `${this.name} makes a sound`;
  }
}

// Child class
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call parent constructor
    this.breed = breed;
  }
  
  speak() {
    return `${this.name} barks`;
  }
}

// Usage
const dog = new Dog('Rex', 'Labrador');
console.log(dog.name); // 'Rex'
console.log(dog.breed); // 'Labrador'
console.log(dog.speak()); // 'Rex barks'
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true

Understanding the Prototype Chain

// Inheritance creates a prototype chain
class Vehicle {
  constructor(make) {
    this.make = make;
  }
}

class Car extends Vehicle {
  constructor(make, model) {
    super(make);
    this.model = model;
  }
}

const car = new Car('Toyota', 'Camry');

// Prototype chain: car -> Car.prototype -> Vehicle.prototype -> Object.prototype
console.log(car instanceof Car); // true
console.log(car instanceof Vehicle); // true
console.log(car instanceof Object); // true

The super Keyword

The super keyword is used to call methods and constructors from the parent class.

Calling Parent Constructor

// Parent class
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

// Child class
class Employee extends Person {
  constructor(name, age, employeeId) {
    super(name, age); // Call parent constructor
    this.employeeId = employeeId;
  }
}

const employee = new Employee('Alice', 30, 'EMP001');
console.log(employee.name); // 'Alice'
console.log(employee.age); // 30
console.log(employee.employeeId); // 'EMP001'

Calling Parent Methods

// Parent class
class Shape {
  constructor(color) {
    this.color = color;
  }
  
  describe() {
    return `This shape is ${this.color}`;
  }
}

// Child class
class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }
  
  describe() {
    // Call parent method and extend it
    const parentDescription = super.describe();
    return `${parentDescription} with radius ${this.radius}`;
  }
  
  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

const circle = new Circle('red', 5);
console.log(circle.describe()); // 'This shape is red with radius 5'
console.log(circle.getArea()); // 78.53981633974483

Inheritance Patterns

Pattern 1: Single Inheritance

// Simple parent-child relationship
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    return `${this.name} is eating`;
  }
}

class Dog extends Animal {
  bark() {
    return `${this.name} is barking`;
  }
}

const dog = new Dog('Buddy');
console.log(dog.eat()); // 'Buddy is eating'
console.log(dog.bark()); // 'Buddy is barking'

Pattern 2: Multi-Level Inheritance

// Grandparent -> Parent -> Child
class LivingBeing {
  constructor(name) {
    this.name = name;
  }
  
  breathe() {
    return `${this.name} is breathing`;
  }
}

class Animal extends LivingBeing {
  eat() {
    return `${this.name} is eating`;
  }
}

class Dog extends Animal {
  bark() {
    return `${this.name} is barking`;
  }
}

const dog = new Dog('Max');
console.log(dog.breathe()); // 'Max is breathing'
console.log(dog.eat()); // 'Max is eating'
console.log(dog.bark()); // 'Max is barking'

Pattern 3: Method Overriding

// Child class overrides parent method
class Vehicle {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }
  
  getInfo() {
    return `${this.make} ${this.model}`;
  }
  
  start() {
    return 'Vehicle starting...';
  }
}

class Car extends Vehicle {
  constructor(make, model, doors) {
    super(make, model);
    this.doors = doors;
  }
  
  // Override parent method
  getInfo() {
    return `${super.getInfo()} with ${this.doors} doors`;
  }
  
  // Override parent method
  start() {
    return `${super.start()} Engine roaring!`;
  }
}

const car = new Car('Honda', 'Civic', 4);
console.log(car.getInfo()); // 'Honda Civic with 4 doors'
console.log(car.start()); // 'Vehicle starting... Engine roaring!'

Pattern 4: Adding Methods to Child Class

// Child class adds new methods
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    return `${this.name} makes a sound`;
  }
}

class Bird extends Animal {
  constructor(name, wingspan) {
    super(name);
    this.wingspan = wingspan;
  }
  
  // New method specific to Bird
  fly() {
    return `${this.name} is flying with ${this.wingspan}cm wingspan`;
  }
  
  // Override parent method
  speak() {
    return `${this.name} chirps`;
  }
}

const bird = new Bird('Tweety', 20);
console.log(bird.speak()); // 'Tweety chirps'
console.log(bird.fly()); // 'Tweety is flying with 20cm wingspan'

Practical Real-World Examples

Example 1: User Management System

// Base user class
class User {
  constructor(username, email) {
    this.username = username;
    this.email = email;
    this.createdAt = new Date();
  }
  
  getProfile() {
    return {
      username: this.username,
      email: this.email,
      createdAt: this.createdAt
    };
  }
}

// Admin user extends User
class Admin extends User {
  constructor(username, email, permissions = []) {
    super(username, email);
    this.permissions = permissions;
  }
  
  addPermission(permission) {
    this.permissions.push(permission);
  }
  
  hasPermission(permission) {
    return this.permissions.includes(permission);
  }
  
  getProfile() {
    return {
      ...super.getProfile(),
      role: 'admin',
      permissions: this.permissions
    };
  }
}

// Moderator user extends User
class Moderator extends User {
  constructor(username, email, moderatedSections = []) {
    super(username, email);
    this.moderatedSections = moderatedSections;
  }
  
  canModerate(section) {
    return this.moderatedSections.includes(section);
  }
  
  getProfile() {
    return {
      ...super.getProfile(),
      role: 'moderator',
      moderatedSections: this.moderatedSections
    };
  }
}

// Usage
const admin = new Admin('admin_user', '[email protected]', ['delete_users', 'ban_users']);
const moderator = new Moderator('mod_user', '[email protected]', ['general', 'support']);

console.log(admin.getProfile());
console.log(moderator.getProfile());
console.log(admin.hasPermission('delete_users')); // true
console.log(moderator.canModerate('general')); // true

Example 2: Shape Hierarchy

// Base shape class
class Shape {
  constructor(color) {
    this.color = color;
  }
  
  describe() {
    return `A ${this.color} shape`;
  }
}

// Rectangle class
class Rectangle extends Shape {
  constructor(color, width, height) {
    super(color);
    this.width = width;
    this.height = height;
  }
  
  getArea() {
    return this.width * this.height;
  }
  
  describe() {
    return `${super.describe()} - Rectangle (${this.width}x${this.height})`;
  }
}

// Circle class
class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }
  
  getArea() {
    return Math.PI * this.radius ** 2;
  }
  
  describe() {
    return `${super.describe()} - Circle (radius: ${this.radius})`;
  }
}

// Triangle class
class Triangle extends Shape {
  constructor(color, base, height) {
    super(color);
    this.base = base;
    this.height = height;
  }
  
  getArea() {
    return (this.base * this.height) / 2;
  }
  
  describe() {
    return `${super.describe()} - Triangle (base: ${this.base}, height: ${this.height})`;
  }
}

// Usage
const shapes = [
  new Rectangle('blue', 10, 5),
  new Circle('red', 7),
  new Triangle('green', 8, 6)
];

shapes.forEach(shape => {
  console.log(shape.describe());
  console.log(`Area: ${shape.getArea().toFixed(2)}\n`);
});

Example 3: Payment Processing System

// Base payment method
class PaymentMethod {
  constructor(accountHolder) {
    this.accountHolder = accountHolder;
  }
  
  validate() {
    throw new Error('validate() must be implemented');
  }
  
  process(amount) {
    throw new Error('process() must be implemented');
  }
}

// Credit card payment
class CreditCard extends PaymentMethod {
  constructor(accountHolder, cardNumber, expiryDate, cvv) {
    super(accountHolder);
    this.cardNumber = cardNumber;
    this.expiryDate = expiryDate;
    this.cvv = cvv;
  }
  
  validate() {
    return this.cardNumber.length === 16 && this.cvv.length === 3;
  }
  
  process(amount) {
    if (!this.validate()) {
      throw new Error('Invalid card details');
    }
    return {
      method: 'Credit Card',
      amount,
      cardLast4: this.cardNumber.slice(-4),
      status: 'processed'
    };
  }
}

// PayPal payment
class PayPal extends PaymentMethod {
  constructor(accountHolder, email) {
    super(accountHolder);
    this.email = email;
  }
  
  validate() {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email);
  }
  
  process(amount) {
    if (!this.validate()) {
      throw new Error('Invalid PayPal email');
    }
    return {
      method: 'PayPal',
      amount,
      email: this.email,
      status: 'processed'
    };
  }
}

// Bank transfer
class BankTransfer extends PaymentMethod {
  constructor(accountHolder, accountNumber, routingNumber) {
    super(accountHolder);
    this.accountNumber = accountNumber;
    this.routingNumber = routingNumber;
  }
  
  validate() {
    return this.accountNumber.length >= 8 && this.routingNumber.length === 9;
  }
  
  process(amount) {
    if (!this.validate()) {
      throw new Error('Invalid bank details');
    }
    return {
      method: 'Bank Transfer',
      amount,
      accountLast4: this.accountNumber.slice(-4),
      status: 'pending'
    };
  }
}

// Usage
const payments = [
  new CreditCard('John Doe', '1234567890123456', '12/25', '123'),
  new PayPal('Jane Smith', '[email protected]'),
  new BankTransfer('Bob Johnson', '123456789', '987654321')
];

payments.forEach(payment => {
  try {
    const result = payment.process(100);
    console.log(result);
  } catch (error) {
    console.error(error.message);
  }
});

Common Mistakes to Avoid

Mistake 1: Forgetting to Call super() in Constructor

// โŒ Wrong - Forgot super()
class Dog extends Animal {
  constructor(name, breed) {
    // Missing super(name)
    this.breed = breed;
  }
}

// โœ… Correct
class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
}

Mistake 2: Not Using super When Calling Parent Methods

// โŒ Wrong - Doesn't call parent method
class Dog extends Animal {
  speak() {
    return `${this.name} barks`; // Doesn't use parent method
  }
}

// โœ… Correct - Uses super to call parent method
class Dog extends Animal {
  speak() {
    const parentSpeak = super.speak();
    return `${parentSpeak} - Actually, ${this.name} barks`;
  }
}

Mistake 3: Trying to Use this Before Calling super()

// โŒ Wrong - Using this before super()
class Dog extends Animal {
  constructor(name, breed) {
    this.breed = breed; // Error: must call super() first
    super(name);
  }
}

// โœ… Correct - Call super() first
class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
}

Mistake 4: Incorrect Method Overriding

// โŒ Wrong - Typo in method name
class Dog extends Animal {
  spek() { // Typo - should be 'speak'
    return `${this.name} barks`;
  }
}

// โœ… Correct - Exact method name
class Dog extends Animal {
  speak() {
    return `${this.name} barks`;
  }
}

Inheritance vs Composition

While inheritance is powerful, composition is often preferred for flexibility.

// Inheritance approach
class Bird extends Animal {
  fly() {
    return `${this.name} is flying`;
  }
}

// Composition approach
class Animal {
  constructor(name) {
    this.name = name;
  }
}

class FlyingAbility {
  fly() {
    return `${this.name} is flying`;
  }
}

class Bird {
  constructor(name) {
    this.animal = new Animal(name);
    this.flyingAbility = new FlyingAbility();
    this.name = name;
  }
  
  fly() {
    return this.flyingAbility.fly.call(this);
  }
}

// Composition is more flexible for complex scenarios

Summary

Class inheritance with extends and super provides:

  • Clear parent-child relationships
  • Method overriding and extension
  • Code reuse through inheritance
  • Proper prototype chain setup
  • Clean syntax compared to prototype-based inheritance
  • Always call super() in child constructors
  • Use super.method() to call parent methods
  • Consider composition for complex scenarios
  • Inheritance works best for “is-a” relationships

Next Steps

Continue your learning journey:

Comments