Introduction
Polymorphism
is a term used with constructor function instantiations to give multiple functions a tree of sorts, each sharing the previous's properties and methods.
It's primarily used to cut down on code in Object-Oriented Programming to make sure the working experience is streamlined, giving a "write less, do more" attitude if you will.
While it's a simple concept on the surface, it isn't uncommon to see a newer coder get stuck on the "coding magic" that is polymorphism, and even instantiation as a whole. This blog will, by the end of it, help you on your way to making all kinds of constructors for all kinds of things.
How does it work?
When we instantiate a constructor function, we primarily have two choices in syntax in JavaScript1: ES5
and ES6
. ES5
is more familiar to most coders, as it doesn't take away any of the syntax that has been since JavaScript's creation. ES6
is functionally identical, but it adds a lot of syntactic sugar to make it much more convenient to look at.
For the examples, we will be using ES5
syntax.
For ES5
, when we want to call what's called the superclass
, or its "parent" class, We do this with the conveniently-named .call(this)
. this
is called for the context, as we want the constructor itself to be instantiated with the parent class. Also don't forget to pass in any relevant arguments your subclass needs defaulted by the superclass!
Also keep in mind that because we call the superclass on the subclass itself, that means the prototype
is also copied. Make sure to copy that with Object.create(<superclass>.prototype)
to the proper constructor name before you move on. Same goes with the prototype.constructor
specifically. Remember, you copied the proto, so you should make sure all names are relevant to their context.
// Superclass declaration
const Auto = function(owner) {
this.owner = owner;
};
Auto.prototype.drive = function() {
/* {...} */
};
// Subclass declaration
const Car = function(make, model, owner) {
// Calling the superclass, Auto, with .call(this), also passing in the owner param.
Auto.call(this, owner);
this.make = make;
this.model = model;
};
// Copying the proto...
Car.prototype = Object.create(Auto.prototype);
// Changing the constructor function. This is important for when the call stack needs
// to refer back to something. As with everything, you should ALWAYS keep information
// relevant.
Car.prototype.constructor = Car;
ES6
however, doesn't need to do that whole Object.create()
thing after the base function. In fact, because ES6
has completely different syntax, you do things just as differently. When you define your constructor() {}
, you start by calling the superclass with the aptly named super()
function, once again passing in the relevant parameters.
On top of that, instead of doing <superclass>.call(this, ...args)
, to define what the superclass is, you use yet another keyword that ES6
added in, that being extends
. You place it after your class name, but before the code block.
// Superclass
class Auto {
constructor(owner) {
this.owner = owner;
}
drive() {
/* {...} */
}
}
// Subclass
// Notice how we add "extends Auto" after the normal naming.
class Car extends Auto {
constructor(make, model, owner) {
// super(owner) is basically <superclass>.call(this, owner). In this case,
// <superclass> is Auto.
super(owner);
}
// And we don't need anything else. "extends" does that jumble of mess below the
// base for us.
}
And if you're feeling extra brave, know that subclasses can also have their own subclasses, same rules applied as before. This makes a "tree" of call chaining, calling the more and more general parent classes to get back all of the properties that should be owned by ALL subclasses, or to hardcode certain parameters, depending on what you're trying to do.
class Car extends Auto {
constructor(make, model, owner) {
super(owner);
this.make = make;
this.model = model;
}
}
class FordCar extends Car {
// Notice how the parameters for the constructor get shorter the more hardcoded things
// you enter.
constructor(model, owner) {
super('Ford', model, owner);
this.model = model;
}
}
class FordFocus extends FordCar {
constructor(owner) {
super('Focus', owner);
}
}
// This could go on for a while, but you get the idea.
/*
And in case you need a bit more of a visual...
FordFocus('James') is calling
FordCar('Focus', 'James') which is calling
Car('Ford', 'Focus', 'James').
*/
Conclusion
Polymorphism
is a fairly simple concept primarily in Object-Oriented Programming used to create a "tree" of constructors, to cut down on the code required to write, which seems menial in small examples like these, but can be a lifesaver in much bigger projects. And understanding this concept thoroughly allows you to make your code cleaner, shorter, and with much less hassle than if you were to do it separately.
Superscript References
- JavaScript is not a primarily Object-Oriented Programming language, and similarly, the term
polymorphism
is not reserved to it. It's a concept that's found in languages such as Python, C/#/++, and Java, who focus more heavily on OOP given their structure.
Top comments (1)
Hey Corbin -- appreciate you touching on this topic. Some of the ideas are there, but polymorphism is typically a means to an end in object-oriented programming. "To what end", you might ask? The end is unifying an interface -- a set of behaviors, if you will. Your example has a base class of
Auto
and its interface is simply made up of a.drive()
method. Any subclass that inherits, by extension, should "behave" the same as its base/parent class.It really has nothing to do with inheritance or tree at all. And furthermore, it's best (if possible) to avoid sub-classing beyond a level deep. Chains of inheritance many levels deep will be a pain in the ass later. Inheritance and sub-classing should NOT be used for shared behavior.
But diving back to polymorphism -- which means, "many forms". Too vague in the context of the object-oriented programming paradigm. Let's extend that definition a bit to: "many forms, but same behavior" (same interface). Why? Let's elucidate with an example:
Notice how the
.stop()
method inCarDriver
is littered with type-checking conditions? If only all the automobiles conformed, and made their.stop()
identical in name, then we could simply do what theCarDriver.drive()
method does by callingcar.drive()
without ever having to inspect the type. Right? This is known to be a good idea -- "depend on behaviors" or abstractions rather than "concretions" (concrete/rigid names with too much specificity). For anything that's an "automobile" we should be able to reasonably expect that it conforms to the same behavior (interface) with identical methods -- the automobile takes many forms, but behaves similarly... as it drives and stops (perhaps in subtle ways, like a different velocity for each).A signal as to for when to reach for polymorphism is the presence of many
if
orcase
statements that perform type-checking -- it looks at what this object is and then makes a decision, because those objects don't have the same methods (interface) even though we're saying in our heads, "if only they had the same method name for the same behavior/operation", then it may be a time to refactor!I've said too much. But if you want some really good examples, here's an amazing GitHub repository. I'm linking to the specific section on polymorphism here: github.com/ryanmcdermott/clean-cod..., but I recommend ⭐️ing it and coming back for reference in your journey when something just doesn't seem right or if your code is constantly breaking due to new requirements...