Skip to main content

JavaScript Polymorphism

Introduction

Polymorphism is one of the four fundamental principles of Object-Oriented Programming (OOP), alongside encapsulation, inheritance, and abstraction. The word "polymorphism" comes from Greek, meaning "many forms." In programming, polymorphism allows objects of different classes to be treated as objects of a common superclass. More simply, it's the ability of different objects to respond to the same method call in different ways.

In this tutorial, we'll explore how polymorphism works in JavaScript and why it's such a powerful concept for building flexible and maintainable code.

Understanding Polymorphism in JavaScript

JavaScript implements polymorphism in several ways:

  1. Method Overriding: A subclass can provide a specific implementation of a method that is already defined in its superclass
  2. Interface-like Behavior: Objects can implement the same methods but with different behaviors
  3. Duck Typing: If it looks like a duck and quacks like a duck, it's a duck!

Let's explore each of these concepts with examples.

Method Overriding

Method overriding is perhaps the most common form of polymorphism. It occurs when a child class provides a specific implementation for a method that is already defined in its parent class.

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

makeSound() {
return "Some generic animal sound";
}

introduce() {
console.log(`Hi, I'm ${this.name} and I sound like: ${this.makeSound()}`);
}
}

// Child classes
class Dog extends Animal {
makeSound() {
return "Woof! Woof!";
}
}

class Cat extends Animal {
makeSound() {
return "Meow!";
}
}

class Duck extends Animal {
makeSound() {
return "Quack!";
}
}

// Usage
const animal = new Animal("Unknown Animal");
const dog = new Dog("Buddy");
const cat = new Cat("Whiskers");
const duck = new Duck("Donald");

animal.introduce(); // "Hi, I'm Unknown Animal and I sound like: Some generic animal sound"
dog.introduce(); // "Hi, I'm Buddy and I sound like: Woof! Woof!"
cat.introduce(); // "Hi, I'm Whiskers and I sound like: Meow!"
duck.introduce(); // "Hi, I'm Donald and I sound like: Quack!"

In this example, the makeSound() method is overridden in each subclass. The introduce() method calls makeSound(), but the specific implementation that runs depends on the actual object type, demonstrating polymorphism in action.

Interface-like Behavior in JavaScript

JavaScript doesn't have formal interfaces like some other languages (Java, TypeScript), but we can achieve similar behavior by ensuring objects implement the same methods.

javascript
// Different objects with the same method interface
const circle = {
name: "Circle",
calculateArea: function(radius) {
return Math.PI * radius * radius;
}
};

const square = {
name: "Square",
calculateArea: function(side) {
return side * side;
}
};

const rectangle = {
name: "Rectangle",
calculateArea: function(width, height) {
return width * height;
}
};

// Function that works with any shape that has a calculateArea method
function printArea(shape, ...dimensions) {
console.log(`The area of this ${shape.name} is: ${shape.calculateArea(...dimensions)}`);
}

printArea(circle, 5); // "The area of this Circle is: 78.53981633974483"
printArea(square, 4); // "The area of this Square is: 16"
printArea(rectangle, 3, 5); // "The area of this Rectangle is: 15"

Here, each shape object has a calculateArea() method, but each implementation is different. The printArea() function works with any object that has this method, regardless of how it's implemented.

Duck Typing

Duck typing is a programming concept that focuses on what an object can do (its methods and properties) rather than what it is (its type). JavaScript, being a dynamically typed language, naturally supports this form of polymorphism.

javascript
function makeItFly(entity) {
if (typeof entity.fly === 'function') {
console.log(`${entity.name} is flying:`);
entity.fly();
} else {
console.log(`${entity.name} cannot fly!`);
}
}

const bird = {
name: "Sparrow",
fly: function() {
console.log("Flapping wings and soaring through the air!");
}
};

const airplane = {
name: "Boeing 747",
fly: function() {
console.log("Starting engines, accelerating down the runway, and taking off!");
}
};

const fish = {
name: "Goldfish",
swim: function() {
console.log("Swimming in the water");
}
};

makeItFly(bird);
// "Sparrow is flying:"
// "Flapping wings and soaring through the air!"

makeItFly(airplane);
// "Boeing 747 is flying:"
// "Starting engines, accelerating down the runway, and taking off!"

makeItFly(fish);
// "Goldfish cannot fly!"

In this example, the makeItFly() function doesn't care what type the entity parameter is — it only cares whether the entity has a fly() method. This is duck typing: "If it has a fly method, then it can fly."

Practical Example: Building a Payment Processing System

Let's see polymorphism in a more practical example. Imagine we're building a simple e-commerce system with different payment methods:

javascript
// Base Payment class
class Payment {
constructor(amount) {
this.amount = amount;
}

process() {
throw new Error("The process method must be implemented by subclasses");
}
}

// Specific payment methods
class CreditCardPayment extends Payment {
constructor(amount, cardNumber, cvv) {
super(amount);
this.cardNumber = cardNumber;
this.cvv = cvv;
}

process() {
console.log(`Processing $${this.amount} payment via Credit Card (${this.cardNumber.substr(-4)})...`);
// Credit card processing logic would go here
return `Payment of $${this.amount} processed successfully via Credit Card`;
}
}

class PayPalPayment extends Payment {
constructor(amount, email) {
super(amount);
this.email = email;
}

process() {
console.log(`Processing $${this.amount} payment via PayPal (${this.email})...`);
// PayPal processing logic would go here
return `Payment of $${this.amount} processed successfully via PayPal`;
}
}

class BankTransferPayment extends Payment {
constructor(amount, accountNumber) {
super(amount);
this.accountNumber = accountNumber;
}

process() {
console.log(`Processing $${this.amount} payment via Bank Transfer (${this.accountNumber})...`);
// Bank transfer logic would go here
return `Payment of $${this.amount} processed successfully via Bank Transfer`;
}
}

// Checkout system that works with any payment method
function processCheckout(cart, paymentMethod) {
const totalAmount = cart.reduce((total, item) => total + item.price, 0);
console.log(`Checking out ${cart.length} items totaling $${totalAmount}`);

// The magic of polymorphism: we don't need to know what type of payment is used
const result = paymentMethod.process();
console.log(result);

console.log("Thank you for your purchase!");
}

// Usage
const shoppingCart = [
{ name: "JavaScript Book", price: 39.99 },
{ name: "Mechanical Keyboard", price: 89.99 }
];

const creditCardPayment = new CreditCardPayment(129.98, "4111111111111111", "123");
const payPalPayment = new PayPalPayment(129.98, "[email protected]");
const bankTransferPayment = new BankTransferPayment(129.98, "GB29NWBK60161331926819");

// The checkout works with any type of payment
processCheckout(shoppingCart, creditCardPayment);
// Output:
// Checking out 2 items totaling $129.98
// Processing $129.98 payment via Credit Card (1111)...
// Payment of $129.98 processed successfully via Credit Card
// Thank you for your purchase!

// We could easily switch to a different payment method
processCheckout(shoppingCart, payPalPayment);
// Output:
// Checking out 2 items totaling $129.98
// Processing $129.98 payment via PayPal ([email protected])...
// Payment of $129.98 processed successfully via PayPal
// Thank you for your purchase!

In this example, the processCheckout() function works with any payment method that implements the process() method, and each payment class implements this method differently. This demonstrates polymorphism in a real-world scenario, making our code more modular and easier to extend.

Benefits of Polymorphism

  1. Code Reusability: Polymorphism allows you to reuse code by implementing methods that work with objects of multiple types.

  2. Flexibility and Extensibility: You can add new classes that implement the same interface without modifying existing code.

  3. Cleaner Code Structure: Your code becomes more organized and readable when you can group objects by common behavior rather than specific types.

  4. Reduced Conditional Logic: Instead of using lengthy if-else chains to check object types, you can rely on polymorphic behavior.

Summary

Polymorphism is a powerful concept in JavaScript that allows objects of different types to be treated as instances of a common type. Through method overriding and duck typing, JavaScript enables flexible, reusable, and maintainable code structures.

In this tutorial, we've explored:

  • Method overriding, where subclasses provide their own implementations of parent class methods
  • Interface-like behavior in JavaScript, where different objects implement the same method signatures
  • Duck typing, where we focus on what an object can do rather than what it is
  • A practical example of polymorphism in a payment processing system

By leveraging polymorphism in your JavaScript applications, you'll write code that's more flexible, modular, and easier to maintain.

Exercises

  1. Create a Shape class hierarchy with different shapes that all implement an area() method, but each with a different calculation.

  2. Design a notification system with different classes (EmailNotification, SMSNotification, PushNotification) that all implement a send() method.

  3. Create a simple game with different character types, each implementing attack() and defend() methods in unique ways.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)