Inheritance & Prototype Chain in Javascript
Bài đăng này đã không được cập nhật trong 3 năm
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 Human
instance. 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:
- The JS engine looks for a property called
greet
on ourengineer
object. - It doesn’t find one, so it looks “up the prototype chain” to engineer’s parent, which is
Human.prototype
. - It finds
Human.prototype.greet
and calls it with this bound toengineer
.
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