3. Prototypes and Inheritance Exercise
The example I came up with for this article is for learning purposes. It's not related to real-world use cases.
Thinking of a good example turned out to be tricky. One of the reasons for that is a difficulty of coming up with an example that has some kind of meaningful logic and hierarchy. Hopefully my solution will do the job.
Explaining Our Case
The idea is the following:
we need to create some kind of task items (as in todo projects). Let's say, they represent HTML
li
elements in a way.we'll assume that there are two types of these items based on their priority: primary and secondary.
further, let's assume that all task items have the same parent element (
ul
) and the same css class. One possible way of providing this informations to every task instance is by prototypal inheritance. Additionally, in case we need to change the parent element or the class name, every task item will detect the change.we can see that one of the inheritance advantages is sharing the code and also making it more flexible.
our hierarchy for this examples looks like this:
Starting with Example
TaskParent() Constructor
First, we'll create TaskParent()
constructor. We'll assume that every final task item belongs to this parent and therefore we'll have some informations that every final item should be 'aware' of.
We'll put those informations into the prototype
object of TaskParent()
constructor. Why prototype
? Because those informations are the same for every final instance, therefore there is no need to clone those informations into every item instance as its own.
function TaskParent() {}
TaskParent.prototype = {
parentElement: "ul",
parentID: "task-list",
getParentInfo: function () {
return `The parent of this task item is ${this.parentElement} element with id: ${this.parentID}`;
},
};
Now, we're going to log TaskParent()
to the console to inspect it and to understand what's happened so far.
console.log(TaskParent)
We see the prototype
object of the TaskParent()
constructor containing features we have defined ( parentElement
, parentID
and getParentInfo
).
As we know, actually every object in JavaScript has an internal <prototype>
object. Let's now observe the internal <prototype>
object of this prototype
object.
We have learned, the internal <prototype>
object points to the object from which it directly inherits. In this case that object is the prototype
object of the Object()
constructor.
By now, we should be able to understand yet another internal <prototype>
object. The one of the TaskParent()
constructor itself.
TaskItem() Constructor
The TaskItem()
will have informations that relate to every task item. That being the case, we'll also put those informations into its prototype
object.
function TaskItem() {}
// take care of inheritance
TaskItem.prototype = TaskParent.prototype;
TaskItem.prototype.constructor = TaskItem;
// augment the prototype with additional features
TaskItem.prototype.TaskItemElement = "li";
TaskItem.prototype.className = "task-item";
TaskItem.prototype.getItemInfo = function () {
return `This item is ${this.TaskItemElement} element with class: ${this.className}`;
};
Now, the interesting part in the above code is that we actually inherited the prototype
from the TaskParent()
constructor without creating a new one for TaskItem()
.
Let's not forget that objects in JavaScript are copied by reference! This is more efficient way, because JavaScript engine will have less work to do when searching, let's say, for getParentInfo()
method. We'll see that later.
console.log(TaskItem.prototype === TaskParent.prototype) // true
Let's do another check:
console.log(TaskParent.prototype.hasOwnProperty("getParentInfo")); // true
console.log(TaskItem.prototype.hasOwnProperty("getParentInfo")); // true
ItemPrimary() and ItemSecondary() Constructors
We've already mentioned that we'll have two types of task items, based on their priority. The primary and the secondary ones. For this purpose we'll create two constructors: ItemPrimary()
and ItemSecondary()
.
The instances of ItemPrimary()
will have priority
property with value of 1
and the instances of ItemSecondary()
will have priority
with value of 2
. Every task item, however, should inherit features from both, the TaskParent()
and the TaskItem()
. Again, we'll only inherit the prototype
object.
function ItemPrimary(title, description) {
this.title = title;
this.description = description;
this.priority = 1;
}
// take care of inheritance
ItemPrimary.prototype = TaskItem.prototype;
ItemPrimary.prototype.constructor = ItemPrimary;
function ItemSecondary(title, description) {
this.title = title;
this.description = description;
this.priority = 2;
}
// take care of inheritance
ItemSecondary.prototype = TaskItem.prototype;
ItemSecondary.prototype.constructor = ItemSecondary;
Creating Our Final Instances
const myFirstPrimaryTask = new ItemPrimary(
"title of the first primary task item",
"additional info for the first primary task item"
);
const myFirstSecondaryTask = new ItemSecondary(
"title of the first secondary task item",
"additional info for the first secondary task item"
)
console.log(myFirstPrimaryTask, myFirstSecondaryTask);
We can easily recognize own properties of these two instances:
If we're going to call, let's say, getParentInfo()
method, we'll see how easily JavaScript is going to find it because of inheriting a single prototype throughout our example.
First, it will check direct properties. Then, it will recognize the internal <prototype>
object and find the method. Only two steps involved in the process.
console.log(myFirstPrimaryTask)
console.log(myFirstPrimaryTask.getParentInfo()) // The parent of this task item is ul element with id: task-list
Learning Through Mistakes
My first code for ItemPrimary()
and ItemSecondary()
constructors looked like this:
function ItemPrimary(title, description) {
this.title = title;
this.description = description;
}
// take care of inheritance
ItemPrimary.prototype = TaskItem.prototype;
ItemPrimary.prototype.constructor = ItemPrimary;
// augment the prototype with additional features
ItemPrimary.prototype.priority = 1;
function ItemSecondary(title, description) {
this.title = title;
this.description = description;
}
// take care of inheritance
ItemSecondary.prototype = TaskItem.prototype;
ItemSecondary.prototype.constructor = ItemSecondary;
// augment the prototype with additional features
ItemSecondary.prototype.priority = 2;
What I didn't realize is that every instance created using either of these two constructors will actually have a priority
property with value of 2
. This happens because priority
property at the end is overwritten and set to value of 2
. And let's not forget that in this entire example we're actually working with only one prototype
object.
This is more efficient way of inheriting features, but we need to know what features should be inherited through this chain to avoid mistakes like this one.
Conclusion
The main goal of this article was to keep learning and to understand:
prototype
object of functions,internal
<prototype>
object that every object hasinheritance process
This article is not about best practices or best possible solutions.
Disclaimer
By no means am I a JavaScript expert, so feel free to comment and point to the potential mistakes. I write articles as my learning experience and with hope it can be useful for others. Any feedback is welcome.
Subscribe to my newsletter
Read articles from Amer directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by