Shape Shifters: Understanding Polymorphism in JavaScript!

gayatri kumargayatri kumar
11 min read

Imagine a world where beings can take on multiple forms, like a shape shifter that can change its appearance but still retain its core identity. In JavaScript, polymorphism allows objects to behave similarly—taking on different forms (methods) while still being instances of their parent class.

Polymorphism is a key concept in Object-Oriented Programming (OOP). It allows us to design objects that can be treated as instances of their parent class, but also have the flexibility to override or modify behaviors in their child classes. This flexibility enables you to write cleaner, more maintainable code by reusing methods and properties across different objects.


What is Polymorphism in JavaScript?

In simple terms, polymorphism means that a single function or method can behave differently based on which object is calling it. With polymorphism, child classes can override parent methods, or a single function can work with objects from different classes.

Let’s go back to our shape shifter analogy:

  • A parent class represents the core traits of a shape shifter (like the ability to change form).

  • Child classes inherit this ability but change how they look or behave.

  • The shape-shifter can take on multiple forms but is still fundamentally the same entity.

In JavaScript, polymorphism allows different objects (even from different classes) to share method names while behaving differently based on the object calling the method.


Implementing Polymorphism in JavaScript

To demonstrate polymorphism, let’s use a simple example of animals that make sounds. We’ll create a base class Animal, and then create child classes like Dog and Cat, each overriding the method speak() to provide their own sound.

Code Snippet: Simple Polymorphism Example

// Parent class
class Animal {
    speak() {
        console.log("The animal makes a sound.");
    }
}

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

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

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

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

Explanation:

  • Parent Class (Animal): Contains a generic speak() method that simply prints a basic message.

  • Child Classes (Dog and Cat): Override the speak() method to provide specific sounds.

  • Polymorphism: Both the Dog and Cat classes share the same method name (speak()), but they behave differently depending on the instance calling the method.

In this case, the shape shifters are the Dog and Cat, which inherit the basic structure of Animal but override how they behave when they "speak."


How Polymorphism Works Behind the Scenes

When an object calls a method, JavaScript looks for that method in the class of the object. If the method is overridden in the child class, JavaScript uses the child class’s version of the method. If the method doesn’t exist in the child class, JavaScript looks up the prototype chain to find it in the parent class.

Think of this as the shape-shifter’s family tree. If the shape-shifter can’t find a certain form (method) in its own abilities, it checks its parent for guidance.


Flowchart: Polymorphism in Action

Here’s a visual representation of how polymorphism works:

           Animal Class (Parent)
                 |
    -----------------------------
    |                           |
 Dog Class                   Cat Class
 (Overrides speak)         (Overrides speak)
    |                           |
 [ myDog Instance ]         [ myCat Instance ]
     (barks)                    (meows)

Each child class (Dog, Cat) has its own version of the speak() method, but they both inherit the general structure from the parent class (Animal). This is what makes polymorphism so powerful—you can treat different objects the same way (calling speak() on both myDog and myCat), while allowing them to behave differently.


Polymorphism with Method Overriding

In JavaScript, the most common way to achieve polymorphism is through method overriding. When a child class defines a method with the same name as its parent class, it overrides the parent’s method. This is like a shape-shifter changing its form based on the situation—it’s still the same shape-shifter, but it behaves differently.

Code Snippet: Method Overriding in Action

Let’s extend the animal example by adding a Bird class that overrides the speak() method but keeps the behavior of another method from the parent class.

// Parent class
class Animal {
    speak() {
        console.log("The animal makes a sound.");
    }

    move() {
        console.log("The animal moves.");
    }
}

// Child class Bird overrides speak but keeps move
class Bird extends Animal {
    speak() {
        console.log("The bird chirps.");
    }

    // No override for move, so it inherits from Animal
}

const myBird = new Bird();
myBird.speak();  // Output: The bird chirps.
myBird.move();   // Output: The animal moves. (Inherits from Animal)

Explanation:

  • The Bird class overrides the speak() method to provide its own version (chirps), but since it doesn’t override move(), it inherits the parent class’s version of move().

This shows how a shape-shifter can change some forms (like how it speaks) while keeping other behaviors (like how it moves).


Challenge – Create Your Own Shape-Shifting Polymorphic System

Now that you understand the basics of polymorphism, it’s time for you to create your own shape-shifting system. Try creating a system where different objects (like vehicles) inherit from a parent class but override some of the behaviors.

Challenge Instructions:

  1. Create a Vehicle parent class: This class should have a method like move() that prints a generic message about moving.

  2. Create child classes: Create classes like Car, Bike, and Airplane that inherit from Vehicle but override the move() method to specify how each one moves (e.g., cars drive, bikes pedal, airplanes fly).

  3. Add more methods: Include more methods in the parent class that some children override and others inherit.

Example:

// Parent class
class Vehicle {
    move() {
        console.log("The vehicle moves.");
    }

    stop() {
        console.log("The vehicle stops.");
    }
}

// Child class Car
class Car extends Vehicle {
    move() {
        console.log("The car drives.");
    }
}

// Child class Bike
class Bike extends Vehicle {
    move() {
        console.log("The bike pedals.");
    }
}

// Child class Airplane
class Airplane extends Vehicle {
    move() {
        console.log("The airplane flies.");
    }
}

const myCar = new Car();
const myBike = new Bike();
const myPlane = new Airplane();

myCar.move();    // Output: The car drives.
myBike.move();   // Output: The bike pedals.
myPlane.move();  // Output: The airplane flies.
myCar.stop();    // Output: The vehicle stops. (Inherits stop from Vehicle)

Explanation:

  • The Vehicle class acts as the parent class with the generic method move().

  • Each child class (Car, Bike, Airplane) overrides move() to customize how it moves.

  • The stop() method is inherited from Vehicle in all child classes, demonstrating how polymorphism allows flexibility and code reuse.


Shifting Forms with Polymorphism

In this first part, we’ve introduced the concept of polymorphism through the analogy of shape-shifters. You’ve learned how objects in JavaScript can override methods from their parent class while retaining the flexibility to behave differently depending on the object calling the method.

In the next part, we’ll dive deeper into more advanced polymorphism concepts, including working with arrays of polymorphic objects and exploring real-world use cases where polymorphism shines in JavaScript applications.


Working with Arrays of Polymorphic Objects

One of the most powerful uses of polymorphism in JavaScript is the ability to work with arrays of polymorphic objects. This means you can have an array filled with objects from different classes, all sharing a common interface (a set of shared methods), but each behaving according to its specific class’s implementation.

Let’s imagine a shape-shifter convention where shape-shifters from different worlds gather. They can all change form (polymorphism in action), but each does so in its own unique way. Similarly, we can have an array of different objects, each representing a different form, but they all follow the same method structure.


Code Snippet: Using Polymorphic Objects in an Array

We’ll extend our previous example of Animal and create an array of polymorphic objects that represent different animals. Each animal will behave differently when we call their shared method speak().

// Parent class
class Animal {
    speak() {
        console.log("The animal makes a sound.");
    }
}

// Child classes
class Dog extends Animal {
    speak() {
        console.log("The dog barks.");
    }
}

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

class Bird extends Animal {
    speak() {
        console.log("The bird chirps.");
    }
}

// Array of polymorphic objects
const animals = [new Dog(), new Cat(), new Bird()];

// Loop through the array and call the polymorphic method
animals.forEach(animal => {
    animal.speak();
});

Output:

The dog barks.
The cat meows.
The bird chirps.

Explanation:

  • We create an array animals filled with instances of Dog, Cat, and Bird.

  • Even though they are different classes, we can call the speak() method on each object in the array. JavaScript automatically uses the correct version of speak() based on the object’s class.


Polymorphism in Real-World Applications

Polymorphism isn’t just a theoretical concept—it’s incredibly useful in real-world JavaScript applications. It allows you to create flexible, reusable code that can adapt to different situations, making your programs more scalable and easier to maintain.

Here are some common real-world examples of where polymorphism shines in JavaScript:

1. User Interfaces (UI)

In web development, you often deal with different types of UI components (like buttons, input fields, or sliders). These components may look different and behave differently, but they share some common methods, like render() or onClick().

Using polymorphism, you can treat all UI components as instances of a base Component class, but each specific component (button, input, slider) can override certain behaviors while still being treated as a Component.

Code Example: Polymorphism in UI Components

// Base Component class
class Component {
    render() {
        console.log("Rendering a component.");
    }
}

// Button class
class Button extends Component {
    render() {
        console.log("Rendering a button.");
    }
}

// InputField class
class InputField extends Component {
    render() {
        console.log("Rendering an input field.");
    }
}

// Array of polymorphic UI components
const components = [new Button(), new InputField()];

// Loop through components and call their render methods
components.forEach(component => {
    component.render();
});

Output:

Rendering a button.
Rendering an input field.

In this example, Button and InputField both override the render() method to provide their own specific behavior. You can use polymorphism to loop through any type of component without worrying about its specific type, knowing it will behave according to its own class.


2. Game Development

In game development, polymorphism is used to manage different types of game objects, like characters, enemies, and power-ups. Each object type has its own specific behaviors, but you can handle them in a unified way.

For example, all characters in a game may share a method like move() or attack(). A player character might move differently from an enemy, but both will still have a move() method.

Code Example: Polymorphism in Game Characters

// Base Character class
class Character {
    move() {
        console.log("The character moves.");
    }
}

// Player class
class Player extends Character {
    move() {
        console.log("The player moves quickly.");
    }
}

// Enemy class
class Enemy extends Character {
    move() {
        console.log("The enemy moves slowly.");
    }
}

// Array of polymorphic game characters
const characters = [new Player(), new Enemy()];

// Loop through characters and call their move methods
characters.forEach(character => {
    character.move();
});

Output:

The player moves quickly.
The enemy moves slowly.

Here, polymorphism allows the Player and Enemy classes to behave differently when moving, but we can still handle them uniformly in the game logic by calling move() on each.


Advanced Polymorphism – Using Interfaces (Conceptually)

While JavaScript doesn’t have traditional interfaces like some other languages (e.g., Java or TypeScript), you can still design your code to follow the interface-like behavior by ensuring that different classes implement the same methods. This is often used in more advanced cases where you want to ensure that certain objects follow a specific contract.

For example, if you want all shapes in a drawing application to implement a draw() method, you can use polymorphism to ensure each shape (circle, square, triangle) provides its own version of draw() while still adhering to the contract of the base Shape class.


Challenge – Create a Polymorphic Shape Drawing System

Now it’s your turn to apply polymorphism to a new domain. Create a system where different shapes (like Circle, Square, and Triangle) all implement a draw() method, but each one draws itself differently.

Challenge Instructions:

  1. Create a Shape base class: This class should have a draw() method that each child class will override.

  2. Create child classes: Create classes like Circle, Square, and Triangle that each provide their own implementation of the draw() method.

  3. Use polymorphism: Put all the shapes in an array and loop through them, calling draw() on each one.

Example:

// Base Shape class
class Shape {
    draw() {
        console.log("Drawing a shape.");
    }
}

// Circle class
class Circle extends Shape {
    draw() {
        console.log("Drawing a circle.");
    }
}

// Square class
class Square extends Shape {
    draw() {
        console.log("Drawing a square.");
    }
}

// Triangle class
class Triangle extends Shape {
    draw() {
        console.log("Drawing a triangle.");
    }
}

// Array of polymorphic shapes
const shapes = [new Circle(), new Square(), new Triangle()];

// Loop through shapes and call their draw methods
shapes.forEach(shape => {
    shape.draw();
});

Output:

Drawing a circle.
Drawing a square.
Drawing a triangle.

Conclusion – Mastering Shape Shifting with Polymorphism

In this article, we’ve explored the concept of polymorphism through the theme of shape shifters. You’ve learned how polymorphism allows objects to share a common interface (like speak() or move()), while still having the flexibility to behave differently based on their specific class. This gives your code the power to be flexible, reusable, and scalable.

You’ve also seen how polymorphism is applied in real-world scenarios, from user interfaces to game development, and how it can be used to manage arrays of different objects seamlessly.


Encapsulation next!

In the next article, we’ll explore another core concept of Object-Oriented Programming: Encapsulation. You’ll learn how to protect and organize your data, keeping certain details hidden from the outside world while exposing only what’s necessary.

30
Subscribe to my newsletter

Read articles from gayatri kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

gayatri kumar
gayatri kumar