JavaScript Classes & Inheritance class constructor extends super

This documentation covers everything you need to know about JavaScript classes, inheritance, constructors, the extends keyword, and the super keyword. It includes detailed explanations and practical examples to help you understand and apply these concepts effectively.

Introduction to Classes in JavaScript

What are Classes?

Imagine you have a blueprint for a house. This blueprint tells you how to construct a house with specific features like the number of windows, doors, and rooms. In object-oriented programming, a class serves a similar purpose. A class is a blueprint for creating objects that share similar properties and methods. Just as a blueprint can be used to build many houses, a class can be used to create many objects that follow the same structure.

In JavaScript, classes were introduced in ECMAScript 6 (ES6) to provide a more organized and systematic way of defining objects, emphasizing reusability and structure.

Classes are particularly useful for creating objects that have similar behavior or properties. For example, if you are building a game, you might have multiple enemies that share common behaviors like moving or attacking. A class can define the common behaviors of these enemies, and then you can create different enemy objects based on this class.

Why Use Classes?

Using classes in JavaScript offers several advantages:

  1. Organization: Classes help organize code into logical structures, making it easier to manage and understand.
  2. Reusability: Once a class is defined, it can be used to create multiple objects, promoting code reuse and reducing duplication.
  3. Prototypal Inheritance: Classes in JavaScript use prototypal inheritance, making it easier to share functionality between classes.
  4. Encapsulation: Classes can encapsulate data and methods, promoting better data hiding and security.
  5. Maintainability: Code that is well-structured and organized with classes is easier to maintain and update over time.

Defining a Class

The Class Declaration

To define a class in JavaScript, you can use the class keyword followed by the class name. Here’s a simple example:

class Car {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
  }

  displayDetails() {
    return `This car is a ${this.year} ${this.make} ${this.model}.`;
  }
}

In this example:

  • We define a class named Car.
  • The constructor method is used to initialize new objects created from the class.
  • The displayDetails method is an instance method that returns a string describing the car.

You can create an instance of the Car class using the new keyword:

const myCar = new Car('Toyota', 'Corolla', 2020);
console.log(myCar.displayDetails()); // Output: This car is a 2020 Toyota Corolla.

The Class Expression

In addition to the class declaration, JavaScript also supports class expressions. Class expressions can be named or unnamed, and they are often used when defining a class inline. Here’s an example of an unnamed class expression:

const Animal = class {
  constructor(species) {
    this.species = species;
  }

  makeSound() {
    console.log(`This ${this.species} makes a sound.`);
  }
};

const lion = new Animal('lion');
lion.makeSound(); // Output: This lion makes a sound.

In this example:

  • We define an unnamed class expression and assign it to the variable Animal.
  • We create an instance of the Animal class using the new keyword and call the makeSound method to display a message.

Creating Instances of a Class

Using the new Keyword

To create an instance of a class, you use the new keyword followed by the class name and any required parameters. Here’s an example:

class Dog {
  constructor(name, breed) {
    this.name = name;
    this.breed = breed;
  }

  bark() {
    console.log(`${this.name} is barking.`);
  }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.bark(); // Output: Buddy is barking.

In this example:

  • We define a Dog class with a constructor that initializes the name and breed properties.
  • We create an instance of the Dog class using the new keyword and assign it to the variable myDog.
  • We call the bark method on the myDog object.

Constructor Method

What is a Constructor?

The constructor is a special method for creating and initializing an object created with a class. A class can have only one constructor method. It is called automatically when a new instance of the class is created.

Syntax and Usage

The syntax for a constructor is straightforward:

class ClassName {
  constructor(param1, param2) {
    this.param1 = param1;
    this.param2 = param2;
  }
}

Here's an example with a Book class:

class Book {
  constructor(title, author, year) {
    this.title = title;
    this.author = author;
    this.year = year;
  }

  getSummary() {
    return `${this.title} by ${this.author}, published in ${this.year}.`;
  }
}

const book1 = new Book('1984', 'George Orwell', 1949);
console.log(book1.getSummary()); // Output: 1984 by George Orwell, published in 1949.

In this example:

  • The Book class has a constructor that initializes three properties: title, author, and year.
  • The getSummary method returns a string summarizing the book.
  • We create an instance of the Book class using new Book('1984', 'George Orwell', 1949) and call the getSummary method on the book1 object.

Class Methods

Defining Methods

Methods are functions that are defined inside a class. Methods are used to define the behavior of objects created from the class. Here’s a simple example:

class Vehicle {
  constructor(type, color) {
    this.type = type;
    this.color = color;
  }

  startEngine() {
    console.log(`The ${this.color} ${this.type} starts its engine.`);
  }

  stopEngine() {
    console.log(`The ${this.color} ${this.type} stops its engine.`);
  }
}

const car = new Vehicle('car', 'red');
car.startEngine(); // Output: The red car starts its engine.
car.stopEngine();  // Output: The red car stops its engine.

In this example:

  • The Vehicle class has two methods: startEngine and stopEngine.
  • We create an instance of the Vehicle class and call the startEngine and stopEngine methods.

Instance Methods

Instance methods are methods that act on instances of the class. They can access and modify the state of the object through the this keyword. Here’s an example:

class Monitor {
  constructor(brand, size) {
    this.brand = brand;
    this.size = size;
    this.isPowerOn = false;
  }

  turnOn() {
    this.isPowerOn = true;
    console.log(`${this.brand} ${this.size} inch monitor turned on.`);
  }

  turnOff() {
    this.isPowerOn = false;
    console.log(`${this.brand} ${this.size} inch monitor turned off.`);
  }

  displayPowerStatus() {
    if (this.isPowerOn) {
      console.log('Monitor is on.');
    } else {
      console.log('Monitor is off.');
    }
  }
}

const myMonitor = new Monitor('Samsung', 27);
myMonitor.turnOn();            // Output: Samsung 27 inch monitor turned on.
myMonitor.displayPowerStatus();  // Output: Monitor is on.
myMonitor.turnOff();           // Output: Samsung 27 inch monitor turned off.
myMonitor.displayPowerStatus();  // Output: Monitor is off.

In this example:

  • The Monitor class has three instance methods: turnOn, turnOff, and displayPowerStatus.
  • We create an instance of the Monitor class and call its methods to control and check the power status of the monitor.

Static Methods

What are Static Methods?

Static methods are methods that belong to the class itself, rather than to instances of the class. They are called on the class directly, rather than on any specific object. Static methods are often used for utility functions that are related to the class but do not need access to instance-specific data.

Syntax and Usage

The syntax for a static method is as follows:

class ClassName {
  static methodName() {
    // method body
  }
}

Here’s an example with a MathUtils class that has a static method:

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

  static multiply(a, b) {
    return a * b;
  }
}

console.log(MathUtils.add(5, 3));        // Output: 8
console.log(MathUtils.multiply(5, 3));   // Output: 15

In this example:

  • The MathUtils class has two static methods: add and multiply.
  • We call these methods directly on the MathUtils class without creating an instance.

Encapsulation

Understanding Encapsulation

Encapsulation is one of the fundamental principles of object-oriented programming. It refers to bundling the data (properties) and methods that operate on the data into a single unit (a class) and restricting access to some of the object’s components. This helps in protecting the data inside the class from being accessed directly from outside the class, leading to better security and maintainability.

Implementing Private Fields and Methods

In JavaScript, private fields and methods were introduced in ECMAScript 2022. Private class fields and methods are only accessible within the class body and not from outside.

Private Fields

Private fields are denoted by the # symbol and can only be accessed or modified from within the class. Here’s an example:

class BankAccount {
  #balance;

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

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`Deposited $${amount}. New balance: $${this.#balance}.`);
    } else {
      console.log('Invalid deposit amount.');
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      console.log(`Withdrew $${amount}. New balance: $${this.#balance}.`);
    } else {
      console.log('Invalid withdrawal amount.');
    }
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount(100);
account.deposit(200);    // Output: Deposited $200. New balance: $300.
account.withdraw(150);  // Output: Withdrew $150. New balance: $150.
console.log(account.getBalance()); // Output: 150

In this example:

  • The #balance field is private and can only be accessed or modified from within the BankAccount class.
  • The deposit and withdraw methods modify the private #balance field.
  • The getBalance method returns the current balance.

Private Methods

Private methods, similar to private fields, are denoted by the # symbol and can only be called from within the class. Here’s an example:

class Calculator {
  #sum(a, b) {
    return a + b;
  }

  #subtract(a, b) {
    return a - b;
  }

  add(a, b) {
    return this.#sum(a, b);
  }

  subtract(a, b) {
    return this.#subtract(a, b);
  }
}

const calculator = new Calculator();
console.log(calculator.add(5, 3));        // Output: 8
console.log(calculator.subtract(5, 3)); // Output: 2

In this example:

  • The #sum and #subtract methods are private and can only be called from within the Calculator class.
  • The add and subtract methods are public and can be called from outside the class, but they internally call the private methods.

Inheritance in JavaScript

What is Inheritance?

Inheritance is a mechanism that allows a new class to inherit properties and methods from an existing class. This promotes code reuse and establishes a hierarchical relationship between classes. The new class, known as a derived or child class, inherits from the base or parent class.

The extends Keyword

The extends keyword is used to create a new class as a child of an existing class. Here’s a basic example:

class Animal {
  constructor(species) {
    this.species = species;
  }

  makeSound() {
    console.log(`This ${this.species} makes a sound.`);
  }
}

class Dog extends Animal {
  constructor(species, name) {
    super(species);
    this.name = name;
  }

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

const myDog = new Dog('dog', 'Buddy');
myDog.makeSound(); // Output: Buddy barks.

In this example:

  • The Dog class extends the Animal class.
  • The Dog class inherits the makeSound method from the Animal class.
  • The Dog class overrides the makeSound method to provide specific behavior for dogs.

Extending Built-in Objects

You can also extend built-in objects to add new features or modify existing behavior. Here’s an example of extending the Array class:

class CustomArray extends Array {
  sum() {
    return this.reduce((acc, val) => acc + val, 0);
  }
}

const myArray = new CustomArray(1, 2, 3, 4, 5);
console.log(myArray.sum()); // Output: 15

In this example:

  • The CustomArray class extends the built-in Array class.
  • The CustomArray class adds a sum method to calculate the sum of the array elements.
  • We create an instance of CustomArray and call the sum method.

The super Keyword

The super keyword is used to call functions defined on a parent class. This includes calling the constructor of the parent class and calling methods on the parent class.

Using super in Constructors

When you use the extends keyword to create a subclass, you must call the parent class's constructor using super(). Here’s an example:

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

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

class Dog extends Pet {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

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

const myPet = new Dog('Buddy', 'Golden Retriever');
myPet.speak(); // Output: Buddy barks.
console.log(myPet.name); // Output: Buddy
console.log(myPet.breed); // Output: Golden Retriever

In this example:

  • The Dog class extends the Pet class.
  • The Dog class constructor calls super(name) to initialize the name property defined in the Pet class.
  • The Dog class overrides the speak method to provide specific behavior for dogs.

Using super for Calling Parent Methods

You can use super.methodName() to call methods on the parent class. This is useful when you want to extend or modify the behavior of a method defined in the parent class.

class Vehicle {
  constructor(type) {
    this.type = type;
  }

  startEngine() {
    console.log(`The ${this.type} vehicle starts its engine.`);
  }

  stopEngine() {
    console.log(`The ${this.type} vehicle stops its engine.`);
  }
}

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

  startEngine() {
    super.startEngine();
    console.log(`The ${this.model} ${this.type} car starts its engine.`);
  }

  stopEngine() {
    super.stopEngine();
    console.log(`The ${this.model} ${this.type} car stops its engine.`);
  }
}

const myCar = new Car('sedan', 'Toyota Camry');
myCar.startEngine(); // Output: The sedan vehicle starts its engine.
                     //         The Toyota Camry sedan car starts its engine.
myCar.stopEngine();  // Output: The sedan vehicle stops its engine.
                     //         The Toyota Camry sedan car stops its engine.

In this example:

  • The Car class extends the Vehicle class.
  • The Car class overrides both the startEngine and stopEngine methods but uses super.methodName() to call the parent class methods and extend their behavior.

Inheritance Chain

Understanding Prototype Chain

In JavaScript, every object has an internal property called __proto__ (or [[Prototype]]). When a class extends another class, the child class's prototype is set to an instance of the parent class. This creates a prototype chain, allowing the child class to inherit methods and properties from the parent class.

The instanceof Operator

The instanceof operator is used to test whether an object is an instance of a specific class. Here’s an example:

class Animal {}
class Dog extends Animal {}

const myDog = new Dog();

console.log(myDog instanceof Dog);    // Output: true
console.log(myDog instanceof Animal); // Output: true
console.log(myDog instanceof Object); // Output: true

In this example:

  • We define an Animal class and a Dog class that extends the Animal class.
  • We create an instance of the Dog class.
  • The instanceof operator checks if myDog is an instance of Dog, Animal, and Object.

The __proto__ Property

The __proto__ property points to the parent class's prototype. Here’s an example:

class Vehicle {}
class Car extends Vehicle {}

const myCar = new Car();

console.log(myCar.__proto__ === Car.prototype);         // Output: true
console.log(Car.prototype.__proto__ === Vehicle.prototype); // Output: true
console.log(Vehicle.prototype.__proto__ === Object.prototype); // Output: true

In this example:

  • We define a Vehicle class and a Car class that extends the Vehicle class.
  • We create an instance of the Car class.
  • We use the __proto__ property to verify the prototype chain: myCar.__proto__ points to Car.prototype, Car.prototype.__proto__ points to Vehicle.prototype, and Vehicle.prototype.__proto__ points to Object.prototype.

Overriding Methods

What is Method Overriding?

Method overriding occurs when a subclass provides a specific implementation of a method that is already defined in its parent class. This allows you to change or extend the behavior of the method in the subclass.

How to Override Methods

To override a method, simply define a method in the subclass with the same name as the method in the parent class. Here’s an example:

class Animal {
  speak() {
    console.log('This animal makes a sound.');
  }
}

class Dog extends Animal {
  speak() {
    console.log('The dog barks.');
  }
}

class Cat extends Animal {
  speak() {
    console.log('The cat meows.');
  }
}

const myDog = new Dog();
const myCat = new Cat();

myDog.speak(); // Output: The dog barks.
myCat.speak(); // Output: The cat meows.

In this example:

  • The Dog class and the Cat class both override the speak method defined in the Animal class.
  • When we call the speak method on instances of Dog and Cat, the overridden methods are executed.

Accessing Parent Class Properties

Using super to Access Parent Properties

To access properties from the parent class, you should use the super keyword. Here’s an example:

class Vehicle {
  constructor(type) {
    this.type = type;
  }
}

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

  displayInfo() {
    console.log(`This is a ${this.type} ${this.model}.`);
  }
}

const myCar = new Car('sedan', 'Toyota Camry');
myCar.displayInfo(); // Output: This is a sedan Toyota Camry.

In this example:

  • The Car class extends the Vehicle class.
  • The Car class constructor calls super(type) to initialize the type property of the Vehicle class.
  • The displayInfo method accesses the type property defined in the Vehicle class.

Accessing Parent Methods Directly

You can also use super.methodName() to call methods defined in the parent class. Here’s an example:

class Vehicle {
  startEngine() {
    console.log('The vehicle starts its engine.');
  }
}

class Car extends Vehicle {
  startEngine() {
    super.startEngine();
    console.log('Car-specific start sequence.');
  }
}

const myCar = new Car();
myCar.startEngine(); // Output: The vehicle starts its engine.
                     //         Car-specific start sequence.

In this example:

  • The Car class extends the Vehicle class.
  • The Car class overrides the startEngine method but also calls the startEngine method of the Vehicle class using super.startEngine().

Abstract Classes (Simulating)

What are Abstract Classes?

An abstract class is a class that cannot be instantiated on its own and is designed to be subclassed. It may contain abstract methods that must be implemented by its subclasses. JavaScript does not have built-in support for abstract classes, but you can simulate them using constructors and throwing errors.

Simulating Abstract Classes in JavaScript

To simulate an abstract class, you can throw an error in the constructor if the class is instantiated directly. Here’s an example:

class Animal {
  constructor() {
    if (new.target === Animal) {
      throw new TypeError('Cannot construct Animal instances directly.');
    }
  }

  makeSound() {
    throw new Error('Subclasses must implement makeSound method.');
  }
}

class Dog extends Animal {
  makeSound() {
    console.log('The dog barks.');
  }
}

// Trying to create an instance of the abstract class
try {
  const animal = new Animal();
} catch (error) {
  console.error(error.message); // Output: Cannot construct Animal instances directly.
}

// Creating an instance of the subclass
const myDog = new Dog();
myDog.makeSound(); // Output: The dog barks.

In this example:

  • The Animal class is intended to be abstract and throws an error if its constructor is called directly.
  • The Dog class extends the Animal class and provides an implementation for the makeSound method.

Summary of Key Concepts

Recapping Classes

  • Class Declaration: Defines a new class using the class keyword.
  • Class Expression: Defines a class using a function-like expression.
  • Constructor: A special method for creating and initializing objects.
  • Methods: Functions defined inside a class to define object behavior.
  • Static Methods: Methods that belong to the class, not to any object.
  • Private Fields and Methods: Fields and methods that are only accessible within the class.
  • Encapsulation: Bundling data and methods that operate on the data into a single unit and restricting access to some of the components.

Recapping Inheritance

  • Inheritance: A mechanism to create new classes based on existing classes.
  • extends Keyword: Used to define a subclass of another class.
  • super Keyword: Used to call functions defined on the parent class.
  • Prototype Chain: The chain of prototypes that allows method and property lookup.
  • instanceof Operator: Used to test if an object is an instance of a specific class.
  • __proto__ Property: Points to the parent class's prototype.

Exercises and Challenges

Practice Problems on Classes

  1. Create a Student class with properties like name, age, and grade. Add methods to display the student's details and update the student's grade.

    class Student {
      constructor(name, age, grade) {
        this.name = name;
        this.age = age;
        this.grade = grade;
      }
    
      displayDetails() {
        return `Student Name: ${this.name}, Age: ${this.age}, Grade: ${this.grade}`;
      }
    
      updateGrade(newGrade) {
        this.grade = newGrade;
        console.log(`Grade updated to ${this.grade}.`);
      }
    }
    
    const student = new Student('Alice', 16, 'A');
    console.log(student.displayDetails()); // Output: Student Name: Alice, Age: 16, Grade: A
    student.updateGrade('B');            // Output: Grade updated to B.
    console.log(student.displayDetails()); // Output: Student Name: Alice, Age: 16, Grade: B
    
  2. Define a Rectangle class with length and width properties. Add methods to calculate the area and perimeter.

    class Rectangle {
      constructor(length, width) {
        this.length = length;
        this.width = width;
      }
    
      getArea() {
        return this.length * this.width;
      }
    
      getPerimeter() {
        return 2 * (this.length + this.width);
      }
    }
    
    const rectangle = new Rectangle(5, 3);
    console.log(`Area: ${rectangle.getArea()}`);          // Output: Area: 15
    console.log(`Perimeter: ${rectangle.getPerimeter()}`); // Output: Perimeter: 16
    

Practice Problems on Inheritance

  1. Create a Vehicle class and a Car subclass. The Vehicle class should have properties and methods for starting and stopping the engine. The Car class should override these methods to include car-specific behavior.

    class Vehicle {
      startEngine() {
        console.log('The vehicle starts its engine.');
      }
    
      stopEngine() {
        console.log('The vehicle stops its engine.');
      }
    }
    
    class Car extends Vehicle {
      startEngine() {
        super.startEngine();
        console.log('Car-specific start sequence.');
      }
    
      stopEngine() {
        super.stopEngine();
        console.log('Car-specific stop sequence.');
      }
    }
    
    const myCar = new Car();
    myCar.startEngine(); // Output: The vehicle starts its engine.
                       //         Car-specific start sequence.
    myCar.stopEngine();  // Output: The vehicle stops its engine.
                       //         Car-specific stop sequence.
    
  2. Define a Shape class and a Circle subclass. The Shape class should have a method for calculating the area. The Circle class should override the method to calculate the area of a circle.

    class Shape {
      calculateArea() {
        throw new Error('This method must be implemented by subclasses.');
      }
    }
    
    class Circle extends Shape {
      constructor(radius) {
        super();
        this.radius = radius;
      }
    
      calculateArea() {
        return Math.PI * this.radius ** 2;
      }
    }
    
    const circle = new Circle(5);
    console.log(`Area: ${circle.calculateArea()}`); // Output: Area: 78.53981633974483
    

These exercises and challenges help you practice defining classes, creating instances, and using inheritance in JavaScript. By working through these exercises, you gain a deeper understanding of how to use classes and inheritance effectively in your JavaScript programs.