3. Prototypes and Inheritance Exercise

After going through first two articles, Prototypes and Inheritance, it's time to do some practice and keep learning through doing.

·

5 min read

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:

    g9

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)

img-1

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.

img-2

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.

img-3

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:

img-4

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

img-5

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 has

  • inheritance 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.