This is post # 26 of the series, dedicated to exploring JavaScript and its building components. In the process of identifying and…
This is post # 26 of the series, dedicated to exploring JavaScript and its building components. In the process of identifying and describing the core elements, we also share some rules of thumb we use when building SessionStack, a JavaScript application that needs to be robust and high-performing to help companies optimize the digital experience of their users.
In real life, a woman can have different roles. She can be a mother, an employee, and a wife at the same time. The same woman will perform different functions depending on her particular role at the moment. This is the concept of polymorphism, being able to take on different forms.
Although numerous people don’t know what polymorphism is, how it works in JavaScript and why it is needed generally, polymorphism is important in programming; it is a core feature for object-oriented programming. For instance, while the woman is at work as an employee, she’s very unlikely going to take on the characteristics of a mother and vice versa when she’s at home as a mother.
For programming languages that are data-oriented like Rust, polymorphism is also an important feature that is achieved with Entity Component System (ECS). A programming language can present the same interface for several different underlying data types. Programs written in JavaScript can take on different forms. In this article, we’ll explore what polymorphism is, how it applies to JavaScript, the different types of polymorphism, and things to have in mind when dealing with polymorphism.
Let’s imagine that we’ll need to write a program that’ll calculate the area and perimeter of shapes. To do this, we’ll need to define methods to calculate the area and perimeter of our shapes; area()
and perimeter()
. However, basic knowledge of geometry tells us that we can’t solve for the area and perimeter of different shapes in the same way. For instance, the perimeter of a circle is 2 x Pi x radius
and the perimeter of a square is 4 x Length
.
So, we’ll need to define the different shapes as sub-classes or derived classes of the base class shapes. Therefore, we’ll have a subclass circle, square, trapezium, polygon which will all have their methods and different parameters. What we have done above is polymorphism. Our class shapes now has different forms and characteristics like circle, square, trapezium, and polygon as sub-classes.
Polymorphism is derived from two words; poly and morphism which means many and variance of form. In programming, polymorphism is defined as the ability of an object to take on different forms. In the next section, we’ll get a deeper understanding of how JavaScript handles polymorphism.
The way programming languages implement polymorphism differs. As an example, Java and JavaScript are both object-oriented programming languages and they don’t handle polymorphism in the same manner. Although polymorphism is a core feature for object-oriented programming languages, some data-oriented languages achieve polymorphism with Entity Component System (ECS) pattern. We’ll also look at how polymorphism works with inheritance and encapsulation.
To understand how polymorphism works in object-oriented programming, it’ll be best if we understand the concept of the object-oriented model. The object-oriented model relies on the concept of objects and classes. This type of model can mutate its data fields with the notion of this
or self
.
Although not in the scope of this article, I’ll explain how polymorphism in JavaScript works by comparing polymorphism in the object-oriented model as interface and polymorphism in data-oriented programming as Entity Component System (ECS).
In object-oriented programming, we can create a class that calculates the area and perimeter of different shapes with the block of code below:
We can recreate the program from our code above with ECS in pseudocode denoting a data-oriented language.
The difference between both examples is that in object-oriented programming from our JavaScript code, we made use of deep inheritance due to interface/abstraction. However, in data-oriented programming from our pseudocode, we used the ECS model to decouple entities into components for easy retrieval of data.
Let’s get a better understanding of what inheritance is in JavaScript and how it relates to polymorphism.
Inheritance is an important feature in polymorphism in object-oriented programming. Let’s look at an example of a car
object:
Now, our car
object will have different sub-classes for different car makes like BMW, Toyota, Bentley, etc., and different properties like color and speed:
Next, we can input the color, speed, and make for our different cars.
From our example, a child class can take up a property from the superclass, to define it. Inheritance can grow from present family to grandparents or even great-grandparents.
Inheritance is a very wide topic in JavaScript, as there are different ways to implement it. For instance, there’s the prototypal, pseudoclassical and functional inheritance. Let’s look briefly at how these inheritance types differ and how they work with polymorphism in JavaScript.
1. Prototypal Inheritance
Prototypal inheritance is a type of inheritance that adds new properties and methods to an existing object. This inheritance makes use of prototype objects i.e. object.prototype
2. Pseudoclassical Inheritance
Psuedoclassical inheritance is similar to prototypal inheritance. This type of inheritance emulates classical inheritance using prototypal inheritance. However, if you’re programming with ES6, the use of psuedoclassical inheritance isn’t encouraged since you can use conventional classes (the class keyword). In psuedoclassical inheritance, we try to create a class with a function that is intended to be called with the new keyword. To understand this better, we’ll be using our car example. Let’s imagine we have a car object as shown in the code below:
We can create sub-types of different car makes
objects with prototype using the new
keyword:
With prototype, we have created new objects with the different car makes
. Next, we’ll understand how to pass the prototype as inheritance and how it affects polymorphism.
First, we’ll create a function called dialogue
that we’ll want our cars to inherit:
After that, we’ll allow our cars to inherit the dialogue
function with prototype:
The program above should output “I am a red Toyota with 100mph speed”, “I am a green BMW with 90mph speed”, and “I am a white Bentley with 120mph speed” accordingly in your console.
3. Functional Inheritance
Functional inheritance involves inheriting features with the use of an augmenting function to an object instance:
Since we have gotten a better understanding of how inheritance in polymorphism work, it’ll be easier to understand how encapsulation works in polymorphism. While programming, there’ll be a need to bundle or put data together in such a way that users can’t access state values of the variables of a particular object from outside.
From the example below, we’re validating the marks of a student by bundling their data together and inheriting them using prototype-based polymorphism.
This is a very good example to understand encapsulation and polymorphism in JavaScript.
A lot of people don’t understand that there’s a difference between abstraction and encapsulation. In abstraction, only certain information is shown while the rest is hidden. Whereas in encapsulation, data is bundled into a single entity and hidden from outside reach. The main reason for encapsulation is to control and validate data. Just as in our example above, validation of student score is done in such a way that the student/public can’t interfere.
There are different types of polymorphism in JavaScript. In this section, we’ll be discussing the three major types which are ad-hoc, Sub-type, and parametric polymorphism.
Ad-hoc polymorphism is a type of polymorphism that allows a value to exhibit different behaviors when “viewed” at different types. Ad-hoc polymorphism can involve a function taking on different forms but bearing the same name.
This type of polymorphism is oftentimes called overloading. Let’s look at a type of ad-hoc polymorphism called operator overloading.
Operator Overloading
From the example above, the operator +
is taking up different forms of adding numbers and concatenating Strings.
Parametric polymorphism is a type of polymorphism that deals with generic functions and generic data types while still maintaining static type safety. Generic functions and data types are functions or data types that can be written in a way so that they can handle values similarly without classifying them based on their type.
For example, objects can store different data types. It doesn’t distinguish its values based on their types:
From the code above, we have an object named after a person Ann. The object contains Ann’s first and last name which are Strings, Ann’s age which is a number, and a Boolean which states that Ann isn’t an adult. Although there are different data types in our object Ann, the object handles them similarly.
Another quick example is an array. In JavaScript, an array can take up different data types as its element.
const Ann = [‘Ann’, ‘Kim’, 4, false];
Just as in our object example, our array example consists of various data types all treated similarly. If you run console.log(Ann)
for our object or array, you should get a list of the elements.
Let’s look at another example below,
In the example above, id
doesn’t distinguish the values 1
and foo
according to their types. So, id
can be in different data types; strings or numbers etc.
Subtype polymorphism involves a subtype and a supertype data type. This type of polymorphism shouldn’t be confused with inheritance as it doesn’t involve the creation of new objects from existing objects — even though inheritance is used most times with this type of polymorphism in JavaScript. Rather, this type of polymorphism is about implementing an interface and substituting different implementations.
For example, if a relative unfortunately died and left you his bookstore. You can read all the books there, sell off the books if you like, you can look at the deceased accounts, the deceased customer list, etc. This is inheritance; you have everything the relative had. Inheritance is a form of code reuse.
If your relative doesn’t leave the bookstore for you in his will, you can reopen the bookstore yourself, taking on all of the relative’s roles and responsibilities, add even some changes of your own — this is subtyping; you are now a bookstore owner, just like your relative used to be.
Let’s look at the example below;
Cat, dog and goat are subtypes of animals. An animal can either be a cat, dog or goat etc. You can substitute different implementations for our different animals.
We’ve explored polymorphism briefly in this article, I’ll point out some things to have in mind when dealing with polymorphism in JavaScript.
1. Polymorphic functions affect the performance of your code, i.e. how fast your program runs. For instance, a monomorphic function will run faster compared to a polymorphic function. In some cases, this difference in performance may be insignificant if they’re frequently run.
2. Sometimes, polymorphism reduces the readability of a program. To solve this problem, it is important to comment on your code so that people can identify the runtime behavior of the program.
3. To implement polymorphism easily in JavaScript, one needs to understand inheritance. This is because polymorphism in JavaScript is centered around inheritance.
For numerous JavaScript developers, there’s a need for code reusability. This is one of the features of polymorphism. Code that has been previously written, can be reused. For instance, the superclass person
can be inherited by a subclass employee
. Another reason why polymorphism is an integral part of JavaScript developers is the ability to store values of different data types together. For example, a lot of JavaScript developers are familiar with creating an array. You can create an array with values of different data types put together. A quick example is an array below which has integers, strings, and booleans.
const Ann = [‘Ann’, ‘Kim’, 4, false];
Also, with polymorphism comes the ability to compose powerful abstractions from simpler ones.
In projects where high performance is not crucial, the design decision whether to use polymorphism or not depends mainly on the approach which will make the code more extensible and maintainable.
In the cases where performance is top priority, it becomes the main driving force to design and architectural decisions.
For example, one of the components of SessionStack is a library that is integrated into websites and web apps. The library consumes data from the browser such as DOM changes, user interactions, JavaScript exceptions, stack traces, network requests, and debug messages. Afterward, the SessionStack platform allows teams to replay user journeys as videos in order to optimize product workflows, reproduce bugs, or see where users are stuck.
Since the library is integrated into other products, it needs to be highly optimized not to cause any negative performance impact during runtime. This means that polymorphism and high levels of abstractions are avoided while efficiency and performance are a top priority.
The platform, on the other hand, can utilize abstractions and other methodologies to make the code easily scalable.
There is a free trial if you’d like to give SessionStack a try.
If you missed the previous chapters of the series, you can find them here:
I'd say inheritance is NOT a good use-case for Person / Employee
A person can play many roles, at the same time. Composition is better in this case. A person has many roles
Inheritance is better for types of legal parties though (see The Party Model). A person is a legal party. An company is an organisation is a legal party
I'd say that inheritance, with method overriding, is the worst anti-pattern of mainstream OOP, which contains a number of otherwise sound ideas, like encapsulation.
I like the Go's approach to that (taken from Oberon), and the Rust's system of traits.
In one large codebase I saw there was a rule to make classes either abstract (without implementation of key parts), or final. Everything else is done via interfaces and composition.
See https://en.m.wikipedia.org/wiki/Composition_over_inheritance
Oberon is fully inheritance based, with interfaces only later introduced in Oberon.NET (for compatibility purposes with .NET) and Active Oberon.
Go only took the method syntax from Oberon-2, package initialization and unsafe instead of SYSTEM.
Hooray Traits! Love them, they are even in PHP so I can use them in these older apps I maintain.
> I'd say inheritance is NOT a good use-case for Person / Employee
Good call. If I wanted Person distinct from Employee, how I'd probably start, in an analysis model, is have Employment as an association/relationship between Person and Company.
In my meta-model, associations like Employment can have attributes, and that might be where things like "employee number" and "salary" come in.
And, taking that a little further, we can represent the history of how things like salary change over time.
(Meanwhile, a project that went with only an Employee record/object might not have saved any coding time initially, and they're quickly piling on expensive, hard-to-maintain kludges, just to meet evolving business needs. Even the first time an employee got a raise, they might've already been hitting incorrectness cases, just trying to get two parts of the system to use the right salary for the right time.)
I personally think composition is better in almost every imaginable case. But I suppose that's an argument for another thread.
Love to see this line of thinking taking over in the industry. Languages like Go make this type of object relationship super easy.
I can't remember the last time I even considered using inheritance in javascript ... except to consider it unnecessary and strewn with mines.
This seems unlikely. Almost every modern Javascript framework uses it. ie) class MyComponent extends React.Component {}
Even React is de-emphasizing class inheritance in favor of more function components.
With one caveat: you still need class components for error boundaries. https://reactjs.org/docs/error-boundaries.html
As someone quite in love with functional programming (background in Haskell and Scala), one thing that seems quite obviously missing here and in my Javascript experience so far is the use of typeclasses as a form of ad-hoc polymorphism.
Is there a nice (i.e. not too much boilerplate) way of using them in the land of Javascript/Typescript?
See https://github.com/gcanti/fp-ts. Heavily haskell-inspired and includes a TS implementation of higher-kinded types.
This is definitely the best option in the TS ecosystem but it's not meaningfully implementing typeclasses from the perspective of the end user. There's no single `fmap` for example, instead you'll be using dedicated `Array.map`, `Option.map`, etc.
As far as i remember there's functions that take explicit typeclass dictionaries (that you can compose via contramap). So I guess they sort of do typeclasses, but without implicits, which is pretty painful IME. Example: https://gcanti.github.io/fp-ts/modules/Functor.ts.html#map