Understanding Polymorphism in JavaScript
What Is Polymorphism?
Polymorphism is a fundamental concept in object-oriented programming that refers to the ability of a single interface or method to handle different underlying forms (data types, classes, or behavior). Essentially, it means "many forms." In simpler terms, polymorphism allows one piece of code to work with different objects in a consistent way.
Types of Polymorphism
There are two main forms of polymorphism often discussed in OOP:
In languages like Java or C#, method overloading is supported by the language at compile time. JavaScript, however, does not support method overloading in the same manner. Instead, JavaScript is dynamically typed and does not have function signatures in the same way. To achieve overloading-like behavior, we rely on checking arguments.length or types inside a single function body.
Method Overriding in JavaScript
How Method Overriding Works
Method overriding in JavaScript typically involves inheritance. When using ES6 classes:
Example Using Classes:
class Animal {
speak() {
console.log("The animal makes a sound.");
}
}
class Dog extends Animal {
speak() {
console.log("The dog barks.");
}
}
const animal = new Animal();
animal.speak(); // "The animal makes a sound."
const dog = new Dog();
dog.speak(); // "The dog barks."
Here, Dog overrides the speak() method from Animal. This is polymorphism: the speak() method acts differently depending on the object's type.
Method Overloading in JavaScript
Why JavaScript Doesn’t Natively Support Overloading
In strongly typed languages, the compiler can distinguish methods by their parameter types and counts. JavaScript, being dynamically typed, does not create distinct method signatures based on parameter count or type. If you define two methods with the same name in a class, the latter definition overwrites the former.
Simulating Method Overloading
To simulate overloading, you can write a single method that checks the number or types of arguments and performs different actions:
Example:
function greet() {
if (arguments.length === 0) {
console.log("Hello!");
} else if (arguments.length === 1) {
console.log("Hello, " + arguments[0] + "!");
} else {
console.log("Hello everyone!");
}
}
greet(); // "Hello!"
greet("Alice"); // "Hello, Alice!"
greet("Alice", "Bob"); // "Hello everyone!"
You could also use rest parameters and type checks:
function add(...args) {
if (args.length === 1) {
return args[0] + 10;
} else if (args.length === 2) {
return args[0] + args[1];
} else {
return args.reduce((sum, num) => sum + num, 0);
}
}
console.log(add(5)); // 15 (treat single arg differently)
console.log(add(2,3)); // 5 (two args add)
console.log(add(1,2,3,4)); // 10 (multiple args sum)
Overloading Methods in Classes
You can apply a similar logic in class methods by having one method do multiple jobs based on arguments:
class Calculator {
calculate(...args) {
if (args.length === 1) {
return args[0] * 2;
} else if (args.length === 2) {
return args[0] + args[1];
} else {
return args.reduce((a, b) => a + b, 0);
}
}
}
const calc = new Calculator();
console.log(calc.calculate(5)); // 10
console.log(calc.calculate(2,3)); // 5
console.log(calc.calculate(1,2,3,4)); // 10
Multiple Choice Questions
Given: class Vehicle {
move() { console.log("Vehicle moves"); }
}
class Car extends Vehicle {
move() { console.log("Car drives"); }
}
let v = new Car();
v.move();
10 Coding Exercises with Solutions and Explanations
Exercise 1: Task: Create a base class Animal with a speak() method. Create a subclass Cat that overrides speak() and prints "Meow" instead of the base message.
Solution:
class Animal {
speak() {
console.log("The animal speaks.");
}
}
class Cat extends Animal {
speak() {
console.log("Meow");
}
}
const genericAnimal = new Animal();
genericAnimal.speak(); // "The animal speaks."
const kitty = new Cat();
kitty.speak(); // "Meow"
Explanation: Cat overrides the speak() method from Animal.
Exercise 2: Task: Using a single function displayInfo(), handle different numbers of arguments:
Solution:
function displayInfo() {
if (arguments.length === 0) {
console.log("No info");
} else if (arguments.length === 1) {
console.log("Info: " + arguments[0]);
} else if (arguments.length === 2) {
console.log("Detailed info: " + arguments[0] + ", " + arguments[1]);
}
}
displayInfo(); // "No info"
displayInfo("Data"); // "Info: Data"
displayInfo("Data", 123); // "Detailed info: Data, 123"
Explanation: This simulates overloading by checking arguments.length.
Exercise 3: Task: Create a Shape class with a draw() method. Create Circle and Square classes that extend Shape and override draw(). Call draw() on instances of both subclasses.
Solution:
class Shape {
draw() {
console.log("Drawing a generic shape.");
}
}
class Circle extends Shape {
draw() {
console.log("Drawing a circle.");
}
}
class Square extends Shape {
draw() {
Recommended by LinkedIn
console.log("Drawing a square.");
}
}
const c = new Circle();
c.draw(); // "Drawing a circle."
const s = new Square();
s.draw(); // "Drawing a square."
Explanation: Each subclass provides its own version of draw().
Exercise 4: Task: In a single calculate() function, if called with one number, return its double, if with two numbers, return their product, else sum all arguments.
Solution:
function calculate(...args) {
if (args.length === 1) {
return args[0] * 2;
} else if (args.length === 2) {
return args[0] * args[1];
} else {
return args.reduce((sum, val) => sum + val, 0);
}
}
console.log(calculate(10)); // 20
console.log(calculate(2, 3)); // 6
console.log(calculate(1,2,3,4)) // 10
Explanation: Polymorphic behavior based on argument count.
Exercise 5: Task: Create a Logger class with a log() method. Override log() in a subclass FileLogger that prints "Logging to file: ..." instead of the base log message.
Solution:
class Logger {
log(message) {
console.log("Default logger: " + message);
}
}
class FileLogger extends Logger {
log(message) {
console.log("Logging to file: " + message);
}
}
const logger = new Logger();
logger.log("Hello"); // "Default logger: Hello"
const fileLogger = new FileLogger();
fileLogger.log("Hello"); // "Logging to file: Hello"
Explanation: FileLogger overrides log().
Exercise 6: Task: Create a function formatDate() that:
Solution:
function formatDate(...args) {
if (args.length === 1 && args[0] instanceof Date) {
const d = args[0];
return ${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')};
} else if (args.length === 2 && typeof args[0] === 'number' && typeof args[1] === 'number') {
return ${String(args[0]).padStart(2,'0')}/${String(args[1]).padStart(2,'0')};
} else {
return "Invalid Format";
}
}
console.log(formatDate(new Date(2020, 0, 5))); // "2020-01-05"
console.log(formatDate(5, 1)); // "05/01"
console.log(formatDate()); // "Invalid Format"
Explanation: This is a contrived example to show argument-based logic.
Exercise 7: Task: Create a base class Player with method play(). A subclass VideoPlayer overrides play() to print "Playing video" and AudioPlayer overrides play() to print "Playing audio".
Solution:
class Player {
play() {
console.log("Playing media.");
}
}
class VideoPlayer extends Player {
play() {
console.log("Playing video.");
}
}
class AudioPlayer extends Player {
play() {
console.log("Playing audio.");
}
}
const v = new VideoPlayer();
v.play(); // "Playing video."
const a = new AudioPlayer();
a.play(); // "Playing audio."
Explanation: Method overriding for polymorphic behavior.
Exercise 8: Task: Implement a function processData() that:
Solution:
function processData(...args) {
if (args.length === 1 && typeof args[0] === 'string') {
return args[0].toUpperCase();
} else if (args.length === 2 && typeof args[0] === 'string' && typeof args[1] === 'string') {
return args[0] + args[1];
} else {
return null;
}
}
console.log(processData("hello")); // "HELLO"
console.log(processData("hello", "world")); // "helloworld"
console.log(processData()); // null
Explanation: Emulated overloading via argument checks.
Exercise 9: Task: Create a class BasePrinter with a print() method. Create a subclass ColorPrinter that overrides print() to add "in color:" before the message.
Solution:
class BasePrinter {
print(message) {
console.log("Printing: " + message);
}
}
class ColorPrinter extends BasePrinter {
print(message) {
console.log("Printing in color: " + message);
}
}
const bp = new BasePrinter();
bp.print("Test"); // "Printing: Test"
const cp = new ColorPrinter();
cp.print("Test"); // "Printing in color: Test"
Explanation: Simple overriding example.
Exercise 10: Task: Write a mathOperation() function that:
Solution:
function mathOperation(...args) {
if (args.length === 1) {
return args[0] * args[0];
} else if (args.length === 2) {
return args[0] + args[1];
} else {
return args.reduce((sum, val) => sum + val, 0);
}
}
console.log(mathOperation(5)); // 25
console.log(mathOperation(2,3)); // 5
console.log(mathOperation(1,2,3,4)); // 10
Explanation: Another example of simulated overloading.
Summary
Polymorphism in JavaScript is achieved primarily through method overriding—subclasses providing specialized implementations of methods defined in their superclasses. True method overloading (as seen in statically typed languages) is not natively supported. Instead, developers simulate overloading by inspecting the arguments within a single function or method body and altering the behavior based on argument count and types.
Polymorphism increases code flexibility, making it easier to write generic code that can work with different objects that share a common interface but have distinct implementations.
By understanding and applying these concepts, you can write more maintainable, scalable, and clean object-oriented code in JavaScript.