+5

Inheritance & Prototype Chain in Javascript

JavaScript might be, indeed, a bit confusing and troublesome at first, especially for developers experienced in class-based languages (like Java or C++). In addition to the aspect of its dynamic behaviors, Javascript does not provide a class implementation per se (the class keyword is introduced in ES2015, but syntactical sugar, JavaScript remains prototype-based underneath). It uses the concept of prototypes and prototype chaining for inheritance.

1. Objects Inherit from Objects

If you’ve ever read anything about inheritance in JS before, then you’ve probably heard that objects inherit from other objects. This is correct. And once you already grasp the knowledge of it, it’s a good way to think about things. But please bear in your mind that the explanation in my article alone isn’t really sufficient.

Still, my goal here is to help people who’s never used prototypal inheritance to become comfortable with it. In order achieve such thing, I decided cut down a few small corners here, which may help eliminate a bit of confusion.

2. Function prototypes

In JavaScript, all functions are also objects, which means that they can have properties. And it happens that they all have a property called prototype in common, whose type is also object.

function doSomething() {
}
console.log(typeof doSomething.prototype); // ‘object’

Pretty easy to understand, right? As soon as a function is created, it will automatically have a property called prototype initialized to an empty object.

3. Constructors

In JavaScript, there’s really no difference between a “regular” function and a constructor function. They’re actually all the same. But as a convention, functions that are meant to be used as constructors are generally capitalized.

So let’s say, we wanna make a constructor function named Human:

function Human(name) {
  this.name = name;
}

new keyword Any time you see the new keyword, it means that the following function is being used as a constructor. The new keyword is intended for constructing a new object, or more generally speaking, making an instance of a "class".

this keyword When a function is used as a constructor, this refers to the new object that you’re creating.

In our Human constructor, we’re taking name as an argument and then assigning this value to the name property of our new Humaninstance. Now that we have been able to make an instance of Human through the usage of new keyword:

var engineer = new Human('Harrison Wells');

and ended up having an useless engineer who’s a Human but doesn’t really do anything.

He’s kind of like my Bridge Software Engineer in my currently active project, actually.

3. Methods

It’s about time to make our Human act a little more human-like by adding a new method called greet.

function Human(name) {
  this.name = name;
}
Human.prototype.greet = function() {
 console.log(this.name + ' said Hello!');
};

There’s that this keyword again. Just like in the constructor, this inside of a method refers to the instance.

You should remember from earlier that all functions automatically get initialized with a prototype object. In the example above, we tacked a function onto it called greet.

Now let’s make ourselves a brand-new engineer.

function Human(name) {
  this.name = name;
}
Human.prototype.greet = function() {
  console.log(this.name + ' said Hello!');
};
var engineer = new Human('Harrison Wells');
engineer.greet(); // ‘Harrison Wells said Hello!’

4. Differential Inheritance

JavaScript uses an inheritance model called “differential inheritance”, whose mechanism is explained that methods aren’t copied from parent to child. Instead, children have an “invisible link” back to their parent object.

For example, engineer doesn’t actually have its own method called greet. In other words,

console.log(engineer.hasOwnProperty('greet') === false); // true

What actually happens when I write engineer.greet() is this:

  1. The JS engine looks for a property called greet on our engineer object.
  2. It doesn’t find one, so it looks “up the prototype chain” to engineer’s parent, which is Human.prototype.
  3. It finds Human.prototype.greet and calls it with this bound to engineer.

That part is really important, so I’m going to repeat it:

There’s really no such property as engineer.greet. It doesn’t exist. Instead, engineer has access to the greet() method on Human.prototype because it’s an instance of Human. This is the “invisible link” I mentioned. More commonly, it’s referred to as the “prototype chain”.

5. Object.create()

Okay. We talked a little bit about differential inheritance and the prototype chain. Now it’s time to put that into action.

Since ES5, JavaScript has had a cool little function called Object.create().

Here’s how it works.

var mammal = {
  jump: function() {
    console.log(‘Jumped!);
  }
};
var tomCat = Object.create(mammal);
tomCat.hasOwnProperty(‘jump’); // false
tomCat.jump(); // ‘Jumped!’

So what is it doing?

Essentially, it creates a new, empty object that has parent in its prototype chain. That means that even though tomCat doesn’t have its own jump method, it has access to the jump method from parent.

6. Prototypal Inheritance

What if we want to make a new class of object that inherits from Human?

Let’s say we need a class called MetaHuman (well, in case you have absolutely no idea what MetaHuman is, I suggest you have a quick glace at MetaHuman Wikia. In short, MetaHumans are superhuman individuals who acquired powers and abilities unlike those of normal humans after experiencing mutation of their bodies. In another word, MetaHuman is just a specific type of Human.

6.1. Subclass constructor

We’ll start by creating its constructor.

function MetaHuman(name, alias, power) {
 this.name = name;
 this.alias = alias;
 this.power = power;
}

Although there is no critical problem with this code so far, we can see that there is a repetation in a piece of code (this.name = name, which is exactly like that in the Human constructor). What if the Human constructor receives more than 1 parameters, for example, name, age and location? Are we gonna repeat one line of code for each of these arguments?

In the middle of crisis, call method was born like a savior of the day. The call method is a predefined Javascript function method which can be used to invoke a function with an owner object as the first argument.

Will call, you are able to use a method belonging to another object.

Let's take a look at the example below:

var cat = {
  name: 'Tom',
  run: function() {
    console.log(this.name + ' ran!');
  }
};

var mouse = {
  name: 'Jerry'
};

cat.run(); // Tom ran!
mouse.run(); //ERROR, because run is not a method of mouse

The right way:

cat.run.call(mouse); // Jerry ran!

So let come back to our original problem. We will eliminate the code repetation via using call method inside MetaHuman constructor in order to invoke the Human constructor with corresponding parameters.

function MetaHuman(name, alias, power) {
 Human.call(this, name);
 this.alias = alias;
 this.power = power;
}

6.2. Setup prototype chain for inheritance

So, that’s all well and good, but how do we make MetaHuman inherit from Human? It’s all about setting up the prototype chain.

If you remember from earlier, we can use Object.create() to create an empty object that inherits from another object. In the case of MetaHuman, that means all we need to do is this:

MetaHuman.prototype = Object.create(Human.prototype);

All instances of MetaHuman will automatically have Human.prototype in their prototype chain, and because MetaHuman.prototype has Human.prototype in its prototype chain, every MetaHuman will have access to the methods of Human.

In other words, we can do this:

var flash = new MetaHuman('Barry Allen', 'Flash', 'Superspeed');
flash.greet(); // Barry Allen said Hello!

One point we need to notice is that MetaHuman.prototype.constructor is now Human while it is supposed to be MetaHuman. This is due to the fact that MetaHuman.prototype holds a references to an instance created via Human method. We can fix this by reassign the constructor to MetaHuman:

MetaHuman.prototype = Object.create(Human.prototype);
MetaHuman.prototype.constructor = MetaHuman;

6.3. Override parent method

Well, any meta-human can greet now, so far so good. But we do know that with the same behavior, each person has their own way to carry it out, right? What if a human needs to greet in a human way and a meta-human need to greet in, well, meta-human way? =)) In another word, despite the method bearing the same name, it is achievable that MetaHuman has greet specific to themselve instead of using the abstract parent class's greet:

MetaHuman.prototype.greet = function() {
  // a hero needs to protect their real identity, thus cannot say Hello with their real name, right?
  console.log(this.alias + ' said Hello to citizens!');
};
var vibe = new MetaHuman('Francisco \'Cisco\' Ramon', 'Vibe', 'Vibrational Blast');
vibe.greet(); // Vibe said Hello to citizens!

6.4. Add specific method of subclass

Last but not least, we can also add new methods that are specific to MetaHuman. Remember, MetaHuman.prototype is just an empty object right now (albeit with a link back to Human.prototype).

MetaHuman.prototype.usePower = function() {
  console.log(this.alias + ' used ' + this.power + '!');
};
var killerFrost = new MetaHuman('Caitlin Snow', 'Killer Frost', 'Cryokinesis');
killerFrost.userPower(); // Killer Frost used Cryokinesis!

7. References

https://www.sitepoint.com/simple-inheritance-javascript https://hackernoon.com/inheritance-in-javascript-21d2b82ffa6f https://javascript.info/prototype-inheritance


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí