JavaScript Object-Oriented Programming: Polymorphism

JavaScript Object-Oriented Programming: Polymorphism

Polymorphism is one of the core concepts in object-oriented programming. The word “polymorphism” means “many forms,” and in programming, it allows different objects to be treated through a common interface while exhibiting different behaviors. This concept enables programmers to write more flexible and reusable code because the exact type of the object can be decided at runtime. In JavaScript, polymorphism plays a vital role despite the language’s dynamic and prototype-based nature.

In simple terms, polymorphism means that you can call the same method on different objects, and each object responds in its own way. This is often achieved by having a method with the same name implemented differently in each class or object. JavaScript supports polymorphism mainly through method overriding in classes or prototype chains and through what is known as duck typing, where objects share method names without formal inheritance. This article will guide you step-by-step through how to implement polymorphism in JavaScript, using fun and practical examples.

Polymorphism Using Method Overriding in ES6 Classes

The most common way to achieve polymorphism in JavaScript today is by using ES6 classes. Method overriding happens when a subclass provides its own version of a method defined in the parent class. When you call this method on an instance of the subclass, the subclass’s version is executed.

Let’s explore this with a simple example of animals speaking differently. We define a base class Animal with a method speak(). Then, subclasses like Dog and Cat will override this method to make their own sounds.

class Animal {

  speak() {
    console.log("The animal makes a sound.");
  }

}

class Dog extends Animal {

  speak() {
    console.log("Woof! Woof!");
  }

}

class Cat extends Animal {

  speak() {
    console.log("Meow! Meow!");
  }

}

const animals = [new Animal(), new Dog(), new Cat()];

animals.forEach(animal => animal.speak());

In this code, the base class Animal has a generic speak() method. The Dog and Cat classes override speak() to produce their unique sounds. When we create an array of different animals and call speak() on each, JavaScript dynamically invokes the correct method based on the object’s actual class. This is polymorphism in action: the same method call behaves differently depending on the object.

Polymorphism Using Prototypes and Constructor Functions

Before ES6 classes, JavaScript programmers used constructor functions and prototypes to create objects and enable inheritance. Polymorphism can be implemented in this style by overriding prototype methods.

Consider a base constructor function Vehicle with a method move(). Subclasses like Car and Bike override the move() method on their prototypes.

function Vehicle() {}

Vehicle.prototype.move = function() {
  console.log("The vehicle moves forward.");
};

function Car() {}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;

Car.prototype.move = function() {
  console.log("The car drives on the road.");
};

function Bike() {}
Bike.prototype = Object.create(Vehicle.prototype);
Bike.prototype.constructor = Bike;

Bike.prototype.move = function() {
  console.log("The bike pedals forward.");
};

const vehicles = [new Vehicle(), new Car(), new Bike()];

vehicles.forEach(vehicle => vehicle.move());

Here, the Vehicle prototype provides a default move() method. The Car and Bike prototypes override this method to customize movement. When we call move() on each vehicle instance, JavaScript uses the closest method on the prototype chain, showing polymorphic behavior.

Polymorphism via Interface-like Patterns (Duck Typing)

JavaScript does not have formal interfaces like some other languages, but polymorphism is still possible using duck typing. This means if different objects have the same method name, you can use them interchangeably without needing to share the same prototype.

Imagine three devices: a Printer, a Scanner, and a FaxMachine. Each implements a performTask() method, but their tasks differ.

const Printer = {

  performTask() {
    console.log("Printing document...");
  }

};

const Scanner = {

  performTask() {
    console.log("Scanning document...");
  }

};

const FaxMachine = {

  performTask() {
    console.log("Sending fax...");
  }

};

const devices = [Printer, Scanner, FaxMachine];

devices.forEach(device => device.performTask());

In this example, each device object has a performTask() method. Even though they don’t share a common prototype or class, you can treat them uniformly. This pattern highlights polymorphism through shared method names and behavior.

Polymorphism with Function Overloading Simulation

JavaScript does not support traditional function overloading, where multiple functions have the same name but different parameters. However, polymorphism can be simulated inside a single function by handling different argument types or counts.

Let’s create a Calculator object with a polymorphic calculate() method that behaves differently based on arguments passed.

const Calculator = {

  calculate: function(...args) {

    if (args.length === 1) {

      console.log(`Calculating square of ${args[0]}:`, args[0] * args[0]);

    } else if (args.length === 2) {

      console.log(`Calculating sum of ${args[0]} and ${args[1]}:`, args[0] + args[1]);

    } else {
      console.log("Invalid number of arguments.");
    }

  }

};

Calculator.calculate(5);
Calculator.calculate(3, 7);
Calculator.calculate();

This function behaves polymorphically by changing its operation based on the number of arguments. When called with one argument, it squares it; with two, it sums them; with none or more than two, it outputs an error message. This flexible method is another example of polymorphism in JavaScript.

Fun Example: Polymorphic Game Characters

To bring all ideas together, imagine a game with different characters: Wizard, Warrior, and Archer. Each character has an attack() method, but each attacks differently.

class Character {

  attack() {
    console.log("The character attacks.");
  }

}

class Wizard extends Character {

  attack() {
    console.log("Wizard casts a fireball!");
  }

}

class Warrior extends Character {

  attack() {
    console.log("Warrior swings a sword!");
  }

}

class Archer extends Character {

  attack() {
    console.log("Archer shoots an arrow!");
  }

}

const characters = [new Wizard(), new Warrior(), new Archer()];

characters.forEach(character => character.attack());

This example shows polymorphism clearly. Each character overrides attack() to perform a unique attack. When the game processes characters, it calls attack() without needing to know which character it is. The correct attack happens thanks to polymorphism.

Conclusion

Polymorphism in JavaScript allows different objects to be treated the same way through shared method names but produce different results depending on their specific types. Whether using ES6 class inheritance and method overriding, prototype-based inheritance, duck typing, or flexible function arguments, polymorphism helps keep code clean, reusable, and dynamic. The examples in this article demonstrate how to implement polymorphism in enjoyable, real-world ways.

References

If you want to learn more about polymorphism and related JavaScript topics, these resources are excellent starting points:

Scroll to Top