Polymorphism is a fundamental concept in object-oriented programming (OOP), which allows different objects to be treated in a similar way based on shared behaviors or interfaces.
In JavaScript, polymorphism is often achieved through inheritance and method overriding, where objects of different types respond to the same method in their own specific way.
In this tutorial, you'll learn:
What is polymorphism?
How polymorphism works in JavaScript
Polymorphism through method overriding
Polymorphism using interfaces (duck typing)
Practical examples of polymorphism in JavaScript
1. What is Polymorphism?
Polymorphism allows objects of different types to be accessed through the same interface, usually by calling methods that behave differently depending on the object type.
This makes code more flexible and reusable since you can work with different types of objects using a uniform interface.
Polymorphism can be achieved in different ways:
Method overriding: A subclass provides its own implementation of a method that is already defined in its superclass.
Interfaces or duck typing: Objects are treated polymorphically based on the methods they implement, regardless of their actual class or type.
2. How Polymorphism Works in JavaScript
JavaScript, as a loosely-typed language, implements polymorphism primarily through prototypal inheritance and method overriding.
Since JavaScript is not a class-based language in the traditional sense, polymorphism is implemented through object prototypes or ES6 classes.
Example 1: Basic Polymorphism Using Prototypes
Let’s create a basic example of polymorphism using prototypes.
function Animal() {} Animal.prototype.speak = function() { console.log("The animal makes a sound."); }; function Dog() {} Dog.prototype = Object.create(Animal.prototype); // Inheriting from Animal Dog.prototype.speak = function() { console.log("The dog barks."); }; function Cat() {} Cat.prototype = Object.create(Animal.prototype); // Inheriting from Animal Cat.prototype.speak = function() { console.log("The cat meows."); }; const animals = [new Dog(), new Cat()]; animals.forEach((animal) => { animal.speak(); });
Output:
The dog barks. The cat meows.
In this example:
We define a speak method in the Animal prototype.
Dog and Cat inherit from Animal but override the speak method to provide their specific implementations.
This allows us to treat both Dog and Cat polymorphically when calling speak.
3. Polymorphism Through Method Overriding
One common way to achieve polymorphism in JavaScript is through method overriding. When a subclass provides a specific implementation for a method that is already defined in its superclass, we override the parent method to achieve polymorphism.
Example 2: Polymorphism with ES6 Classes
In ES6 (ECMAScript 2015), JavaScript introduced class syntax that makes object-oriented programming more explicit. Let’s see how to use polymorphism with ES6 classes.
class Animal { speak() { console.log("The 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 animals = [new Dog(), new Cat()]; animals.forEach((animal) => { animal.speak(); // Each object calls its specific version of the speak method });
Output:
The dog barks. The cat meows.
In this example:
The Animal class has a generic speak method.
The Dog and Cat classes inherit from Animal but override the speak method.
When we call speak() on instances of Dog and Cat, JavaScript automatically invokes the correct method, demonstrating polymorphism.
4. Polymorphism Using Interfaces (Duck Typing)
JavaScript does not have interfaces like other languages (e.g., Java or TypeScript), but it supports duck typing, a type of polymorphism. Duck typing allows objects to be treated as instances of a particular class if they implement the same methods or properties, regardless of their actual class.
Duck typing means that an object is considered to be of a certain type if it “walks like a duck and quacks like a duck” (i.e., it has the same methods as the expected type).
Example 3: Duck Typing in JavaScript
class Car { drive() { console.log("The car is driving."); } } class Bicycle { drive() { console.log("The bicycle is being pedaled."); } } function startJourney(vehicle) { vehicle.drive(); // We don't care what type vehicle is, as long as it has a 'drive' method } const car = new Car(); const bicycle = new Bicycle(); startJourney(car); //
Output:
The car is driving. startJourney(bicycle); //
Output:
The bicycle is being pedaled.
In this example:
Both Car and Bicycle have a drive method, but they belong to different classes.
The startJourney function treats both Car and Bicycle polymorphically because both implement the drive method.
This is an example of duck typing in JavaScript, where the actual class of the object doesn’t matter as long as it behaves like the expected type (i.e., has the required method).
5. Practical Examples of Polymorphism in JavaScript
Example 4: Polymorphism in a Payment System
Let’s implement a polymorphic payment system that processes payments through different payment methods (e.g., credit card, PayPal, or cryptocurrency). Each payment method has its own implementation of a processPayment method.
class Payment { processPayment() { console.log("Processing a generic payment."); } } class CreditCardPayment extends Payment { processPayment() { console.log("Processing a credit card payment."); } } class PayPalPayment extends Payment { processPayment() { console.log("Processing a PayPal payment."); } } class CryptoPayment extends Payment { processPayment() { console.log("Processing a cryptocurrency payment."); } } function handlePayment(payment) { payment.processPayment(); } const payments = [new CreditCardPayment(), new PayPalPayment(), new CryptoPayment()]; payments.forEach((payment) => handlePayment(payment));
Output:
Processing a credit card payment. Processing a PayPal payment. Processing a cryptocurrency payment.
In this example:
The Payment class is the base class, and it has a default processPayment method.
The CreditCardPayment, PayPalPayment, and CryptoPayment classes override processPayment to provide specific implementations for each payment method.
The handlePayment function processes payments polymorphically, calling the correct method for each object.
Example 5: Shape Example Using Polymorphism
Let’s create a polymorphic example with different geometric shapes (circle and rectangle), each with its own implementation of a getArea method.
class Shape { getArea() { return 0; // Default area } } class Circle extends Shape { constructor(radius) { super(); this.radius = radius; } getArea() { return Math.PI * this.radius ** 2; } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } getArea() { return this.width * this.height; } } function printArea(shape) { console.log("The area is: " + shape.getArea()); } const circle = new Circle(5); const rectangle = new Rectangle(4, 6); printArea(circle); //
Output:
The area is: 78.53981633974483 printArea(rectangle); //
Output:
The area is: 24
In this example:
The Shape class provides a default getArea method.
The Circle and Rectangle classes override getArea with their specific implementations.
The printArea function treats both Circle and Rectangle polymorphically and calculates the correct area for each shape.
Conclusion
Polymorphism is a powerful concept that allows objects of different types to be treated uniformly based on shared methods or interfaces. Here's a summary of what you've learned:
Method overriding: Subclasses override methods of a superclass to provide specific implementations.
Duck typing: Objects are treated based on the methods they implement, regardless of their class.
Practical examples: You can implement polymorphism in real-world scenarios like payment systems or geometric shapes.
By mastering polymorphism in JavaScript, you can write more flexible and maintainable code that works with a variety of object types in a consistent manner!