Skip to content

ES6 Extends Inheritance: Modern Class Inheritance Mechanism

Inheritance is one of the core concepts in object-oriented programming. It allows us to build new classes based on existing ones, thereby reusing code and extending functionality. Before ES6, implementing inheritance in JavaScript required writing complex prototype chain code that was error-prone and difficult to understand. The emergence of the extends keyword completely changed this situation, making class inheritance simple and elegant.

ES6 introduced the extends keyword, which completely changed this landscape by providing a concise, intuitive syntax for implementing inheritance relationships between classes. This makes JavaScript's object-oriented programming more aligned with the conventions of other mainstream programming languages, greatly lowering the learning barrier.

Basic Syntax of Extends Inheritance

The extends keyword is used to create a subclass of a class. This subclass will inherit all properties and methods from the parent class. The basic syntax structure is very clear:

javascript
// Parent class (base class)
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

// Child class (derived class)
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call parent class constructor
    this.breed = breed;
  }

  speak() {
    console.log(`${this.name} barks`);
  }

  fetch() {
    console.log(`${this.name} goes to fetch the ball`);
  }
}

const myDog = new Dog("Buddy", "Golden Retriever");
myDog.speak(); // "Buddy barks"
myDog.fetch(); // "Buddy goes to fetch the ball"

In this example, the Dog class inherits all the characteristics of the Animal class through extends Animal. The subclass automatically obtains the parent class's speak method and can also add its own specific methods like fetch.

Core Mechanisms of Inheritance

Automatic Prototype Chain Establishment

When you use extends, the JavaScript engine automatically establishes the correct prototype chain relationships:

javascript
class Animal {}
class Dog extends Animal {}

console.log(Object.getPrototypeOf(Dog) === Animal); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true

This automatically established prototype chain relationship ensures that inheritance works properly. The subclass's prototype points to the parent class, and the subclass instance's prototype chain is also correctly set up.

Constructor Inheritance Rules

In inheritance relationships, constructors have some important rules to follow:

javascript
class Vehicle {
  constructor(brand) {
    this.brand = brand;
    console.log("Vehicle constructor called");
  }
}

class Car extends Vehicle {
  constructor(brand, model) {
    // Must call super() before using this
    super(brand);
    this.model = model;
    console.log("Car constructor called");
  }
}

const myCar = new Car("Toyota", "Camry");
// Output:
// "Vehicle constructor called"
// "Car constructor called"

Important Rule: In a subclass constructor, you must call super() before using the this keyword. This is because the subclass doesn't have its own this object; it inherits the this object from the parent class.

Method Overriding and Extension

Subclasses can not only inherit methods from the parent class but also override (override) these methods while calling the parent class's implementation through the super keyword:

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

  introduce() {
    return `My name is ${this.name}, I'm ${this.age} years old`;
  }

  celebrateBirthday() {
    this.age++;
    console.log(`Happy birthday! Now I'm ${this.age} years old`);
  }
}

class Employee extends Person {
  constructor(name, age, position, salary) {
    super(name, age);
    this.position = position;
    this.salary = salary;
  }

  // Override parent method
  introduce() {
    const basicIntro = super.introduce(); // Call parent method
    return `${basicIntro}, and I'm a ${this.position}`;
  }

  // Extend parent method
  celebrateBirthday() {
    super.celebrateBirthday(); // Call parent method
    // Add employee-specific birthday celebration logic
    this.salary *= 1.05; // 5% raise
    console.log(`Birthday raise! New salary is $${this.salary}`);
  }

  // Subclass-specific method
  promote(newPosition) {
    this.position = newPosition;
    this.salary *= 1.15; // 15% raise
    console.log(`Congratulations on promotion to ${newPosition}! New salary $${this.salary}`);
  }
}

const employee = new Employee("Alice", 28, "Frontend Engineer", 80000);
console.log(employee.introduce());
// "My name is Alice, I'm 28 years old, and I'm a Frontend Engineer"

employee.celebrateBirthday();
// "Happy birthday! Now I'm 29 years old"
// "Birthday raise! New salary is $84000"

employee.promote("Senior Frontend Engineer");
// "Congratulations on promotion to Senior Frontend Engineer! New salary $96600"

Inheritance of Static Members

Static methods and static properties are also inherited:

javascript
class MathHelper {
  static PI = 3.14159;

  static circleArea(radius) {
    return this.PI * radius * radius;
  }

  static add(a, b) {
    return a + b;
  }
}

class AdvancedMath extends MathHelper {
  static E = 2.71828;

  // Override static method
  static add(a, b) {
    const result = super.add(a, b);
    console.log(`${a} + ${b} = ${result}`);
    return result;
  }

  // Add new static method
  static sphereVolume(radius) {
    return (4 / 3) * this.PI * Math.pow(radius, 3);
  }
}

console.log(AdvancedMath.PI); // 3.14159 (inherited)
console.log(AdvancedMath.E); // 2.71828 (own)

AdvancedMath.add(5, 3); // "5 + 3 = 8" (overridden method)
console.log(AdvancedMath.sphereVolume(5)); // 523.59833 (added method)

Inheritance of Private Fields

Private fields (starting with #) introduced in ES2022 can also be used in inheritance:

javascript
class BankAccount {
  #balance = 0;

  constructor(initialBalance) {
    if (initialBalance > 0) {
      this.#balance = initialBalance;
    }
  }

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      return true;
    }
    return false;
  }

  getBalance() {
    return this.#balance;
  }

  #validateAmount(amount) {
    return amount > 0 && Number.isFinite(amount);
  }
}

class SavingsAccount extends BankAccount {
  #interestRate = 0.02;

  constructor(initialBalance, interestRate) {
    super(initialBalance);
    if (interestRate > 0) {
      this.#interestRate = interestRate;
    }
  }

  applyInterest() {
    const balance = this.getBalance();
    const interest = balance * this.#interestRate;
    this.deposit(interest);
    console.log(`Interest $${interest.toFixed(2)} has been deposited`);
  }

  // Subclass cannot directly access parent class private fields
  // getPrivateBalance() {
  //   return this.#balance; // Error: Cannot access parent class private fields
  // }
}

const savings = new SavingsAccount(1000, 0.05);
savings.applyInterest(); // "Interest $50.00 has been deposited"
console.log(savings.getBalance()); // 1050

Practical Application Scenarios of Inheritance

1. UI Component Inheritance

In frontend development, component inheritance is a very common application scenario:

javascript
class Component {
  constructor(element, options = {}) {
    this.element = element;
    this.options = { ...this.getDefaultOptions(), ...options };
    this.events = {};
    this.init();
  }

  getDefaultOptions() {
    return {
      visible: true,
      disabled: false,
    };
  }

  init() {
    this.render();
    this.bindEvents();
  }

  render() {
    this.element.style.display = this.options.visible ? "block" : "none";
    this.element.disabled = this.options.disabled;
  }

  bindEvents() {}

  show() {
    this.options.visible = true;
    this.render();
  }

  hide() {
    this.options.visible = false;
    this.render();
  }

  disable() {
    this.options.disabled = true;
    this.render();
  }

  enable() {
    this.options.disabled = false;
    this.render();
  }
}

class Button extends Component {
  getDefaultOptions() {
    return {
      ...super.getDefaultOptions(),
      text: "Button",
      type: "button",
      size: "medium",
    };
  }

  render() {
    super.render();
    this.element.textContent = this.options.text;
    this.element.className = `btn btn-${this.options.size}`;
    this.element.type = this.options.type;
  }

  bindEvents() {
    this.element.addEventListener("click", () => {
      if (!this.options.disabled) {
        this.onClick();
      }
    });
  }

  onClick() {
    console.log("Button was clicked");
  }

  setText(text) {
    this.options.text = text;
    this.element.textContent = text;
  }
}

class IconButton extends Button {
  getDefaultOptions() {
    return {
      ...super.getDefaultOptions(),
      icon: "",
      iconPosition: "left",
    };
  }

  render() {
    super.render();
    const icon = this.options.icon;
    if (icon) {
      const iconElement = document.createElement("span");
      iconElement.className = "icon";
      iconElement.textContent = icon;

      if (this.options.iconPosition === "left") {
        this.element.insertBefore(iconElement, this.element.firstChild);
      } else {
        this.element.appendChild(iconElement);
      }
    }
  }
}

// Usage examples
const button = new Button(document.getElementById("myButton"), {
  text: "Click Me",
  size: "large",
});

const iconButton = new IconButton(document.getElementById("myIconButton"), {
  text: "Delete",
  icon: "🗑️",
  iconPosition: "right",
});

2. Data Model Inheritance

javascript
class Model {
  constructor(data = {}) {
    this.id = data.id || this.generateId();
    this.createdAt = data.createdAt || new Date();
    this.updatedAt = data.updatedAt || new Date();

    // Batch assignment
    Object.assign(this, data);
  }

  generateId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }

  update(data) {
    Object.assign(this, data);
    this.updatedAt = new Date();
    return this.save();
  }

  save() {
    // Simulate saving to database
    console.log("Save data:", this.toJSON());
    return Promise.resolve(this);
  }

  delete() {
    console.log("Delete data:", this.id);
    return Promise.resolve(true);
  }

  toJSON() {
    const { id, createdAt, updatedAt, ...data } = this;
    return { id, createdAt, updatedAt, ...data };
  }

  static findAll() {
    // Simulate finding all records from database
    return Promise.resolve([]);
  }

  static findById(id) {
    // Simulate finding specific record from database
    return Promise.resolve(null);
  }
}

class User extends Model {
  constructor(data = {}) {
    super(data);
    this.email = data.email;
    this.username = data.username;
    this.role = data.role || "user";
  }

  toJSON() {
    const baseJSON = super.toJSON();
    return {
      ...baseJSON,
      email: this.email,
      username: this.username,
      role: this.role,
    };
  }

  changeRole(newRole) {
    this.role = newRole;
    return this.update({ role: newRole });
  }

  static findByEmail(email) {
    // Simulate finding user by email
    return Promise.resolve(null);
  }

  static authenticate(email, password) {
    // Simulate user authentication
    return Promise.resolve(null);
  }
}

class Admin extends User {
  constructor(data = {}) {
    super(data);
    this.role = "admin";
    this.permissions = data.permissions || [];
  }

  hasPermission(permission) {
    return this.permissions.includes(permission);
  }

  grantPermission(permission) {
    if (!this.hasPermission(permission)) {
      this.permissions.push(permission);
      return this.update({ permissions: this.permissions });
    }
    return Promise.resolve(this);
  }

  revokePermission(permission) {
    const index = this.permissions.indexOf(permission);
    if (index > -1) {
      this.permissions.splice(index, 1);
      return this.update({ permissions: this.permissions });
    }
    return Promise.resolve(this);
  }
}

// Usage examples
const user = new User({
  email: "[email protected]",
  username: "john_doe",
});

const admin = new Admin({
  email: "[email protected]",
  username: "admin",
  permissions: ["read", "write"],
});

admin.grantPermission("delete").then(() => {
  console.log(admin.hasPermission("delete")); // true
});

Considerations for Inheritance

1. Avoid Deep Inheritance Hierarchies

Overly deep inheritance hierarchies can make code difficult to understand and maintain:

javascript
// ❌ Not recommended: Overly deep inheritance hierarchy
class Animal {}
class Mammal extends Animal {}
class Carnivore extends Mammal {}
class Feline extends Carnivore {}
class Cat extends Feline {}
class DomesticCat extends Cat {}

// ✅ Recommended: Reasonable inheritance depth
class Animal {}
class Cat extends Animal {}
class DomesticCat extends Cat {}

2. Use Inheritance Judiciously

Only use inheritance when there's a clear "is-a" relationship:

javascript
// ✅ Correct: A car is a vehicle
class Car extends Vehicle {}

// ✅ Correct: A dog is an animal
class Dog extends Animal {}

// ❌ Incorrect: An employee is not a person, should be composition
class Employee extends Person {}

// ✅ Better: Composition relationship
class Employee {
  constructor(person, position, salary) {
    this.person = person;
    this.position = position;
    this.salary = salary;
  }
}

3. Maintain Method Override Compatibility

When overriding parent class methods, try to maintain interface compatibility:

javascript
class Parent {
  process(data) {
    return data.map((item) => item.value);
  }
}

class Child extends Parent {
  // ✅ Maintain parameter and return value type compatibility
  process(data) {
    const processed = super.process(data);
    return processed.filter((item) => item > 0);
  }

  // ❌ Changing method signature can cause problems
  // process(data, options) {
  //   // Completely different parameter structure
  // }
}

Alternatives to Inheritance

While extends provides a powerful inheritance mechanism, other patterns may be more appropriate in certain scenarios:

1. Composition Pattern

javascript
// Composition over inheritance example
class Engine {
  start() {
    console.log("Engine starts");
  }

  stop() {
    console.log("Engine stops");
  }
}

class GPS {
  navigate(destination) {
    console.log(`Navigate to ${destination}`);
  }
}

class Car {
  constructor() {
    this.engine = new Engine();
    this.gps = new GPS();
  }

  start() {
    this.engine.start();
  }

  navigate(destination) {
    this.gps.navigate(destination);
  }
}

2. Mixin Pattern

javascript
const Flyable = {
  fly() {
    console.log(`${this.name} is flying`);
  },
};

const Swimmable = {
  swim() {
    console.log(`${this.name} is swimming`);
  },
};

class Duck {
  constructor(name) {
    this.name = name;
  }
}

Object.assign(Duck.prototype, Flyable, Swimmable);

const duck = new Duck("Donald");
duck.fly(); // "Donald is flying"
duck.swim(); // "Donald is swimming"

Summary

ES6's extends inheritance provides JavaScript with clear, concise class inheritance syntax. By mastering these concepts and techniques, you can:

  1. Establish clear class hierarchies: Use extends to create meaningful inheritance relationships
  2. Override and extend methods: Use super to call parent class implementations and add new functionality
  3. Handle complex inheritance scenarios: Including static members, private fields, etc.
  4. Choose appropriate design patterns: Make the right choice between inheritance, composition, and Mixin patterns

Inheritance is a powerful tool in object-oriented programming, but use it cautiously. Proper use of inheritance can make code more structured and maintainable, while overuse can lead to increased complexity. In actual development, always follow the "is-a" relationship principle and consider using other patterns like composition to achieve more flexible designs.