Prototypes and class-like objects in Javascript
Javascript being prototypal means that it can access methods and properties through what is known as the prototype chain. Everything in Javascript is an object, and every object has a special property known as the ‘prototype’. This post is a continuation of my previous post Constructor chaining & inheritance in javascript. I recommend that you read that post first as I’ll be referencing some of the same code.
The prototype is defined as an “object that provides shared properties for other objects” - Ecma International. What this means is that, by default, any object that inherits from another object automatically has reference to the parents prototypes all the way through the inheritance tree. When you call a method on an object (remember everything in Javascript is an object) it first looks through the local prototype, if it’s not found, it then continues checking prototypes up until it hits Object.prototype to find that method before throwing an error. To illustrate that lets slightly modify our mustang instance and take a look at the instance in the inspector.
function Car() {
this.color = "red";
}
Car.prototype.getColor = function() {
return this.color;
};
var mustang = new Car();
console.log(mustang.getColor()); // Prints `red` to the console
console.log(mustang.toString()); // Prints [object Object] to the console
Stepping through this code you’ll see that we create a new constructor function as we did previously but this time we are creating a new getColor method on Car’s prototype that returns the color of the instance of Car. We then create a new instance of Car and log the output of the getColor method to the console. The following line is a perfect example of walking the prototype chain to try and find a toString method. We clearly didn’t create this method so where is it coming from? This is best illustrated using a screenshot of the browser console log of our mustang instance.
First thing to notice is that we don’t actually have a prototype
property here but __proto__
which is a non-standard label for accessing the prototype of an object. In the prototype of our Car instance we have the getColor method that we created which matches with the above code block. Next, because Car is a function which extends Object, we have the Object prototype which includes, among other things, the toString method that we used.
It is considered bad practice to overwrite the native method prototypes because other code on the page might be relying on those native prototypes to function properly. So what do you do if you want to use your own toString method? By creating a toString method in the prototype of your instance, Javascript will run into that one first and use it instead of the native method.
function Car() {
this.color = "red";
}
Car.prototype.toString = function() {
return this.color;
};
var mustang = new Car();
console.log(mustang.toString()); // Prints `red` to the console
So here we create a toString method on the Car prototype to return the color of Car. This time when we call the toString method, Javascript runs into our own toString method first and executes it instead of the native method printing red instead of [object Object]
Taking a look at the console log of our mustang object, you can now see the two toString methods and where they are in the prototype chain. Note: Although the console shows __proto__
make sure you don’t access it directly because it’s not available on all platforms, always use prototype
.
To do constructor chaining while maintaining the prototype chain requires you to take one more step. If you simply take the lessons from the previous post and this post so far you would assume you could write something like.
function Baddie() {
this.level = 1;
}
Baddie.prototype.levelUp = function() {
this.level++;
};
function Spider() {
Baddie.apply(this);
}
var spider = new Spider();
spider.levelUp(); // Throws TypeError
By running the above script you will see that you get an error in the console “Uncaught TypeError: Object # has no method ‘levelUp’ ”. And that is because when you chain constructors in this method you aren’t maintaining reference to the parents prototype. To make this work as you would like you need to add one more line.
function Baddie() {
this.level = 1;
}
Baddie.prototype.levelUp = function() {
this.level++;
};
function Spider() {
Baddie.apply(this);
}
Spider.prototype = new Baddie(); // Add this line
var spider = new Spider();
spider.levelUp();
console.log(spider.level); // Prints `2` to the console
By assigning the prototype of your new object to a new instance of your parent you maintain the prototype chain giving you access to its methods.
Combining the above lessons allows you to infinitely chain constructors as we did previously but this time, also maintain the prototypes.
function Baddie() {
this.level = 1;
}
Baddie.prototype.levelUp = function() {
this.level++;
};
function Spider() {
Baddie.apply(this);
}
Spider.prototype = new Baddie();
Spider.prototype.boost = function() {
this.level = this.level + 10;
};
function SuperSpider() {
Spider.apply(this);
}
SuperSpider.prototype = new Spider();
var superSpider = new SuperSpider();
superSpider.levelUp();
console.log(superSpider.level); // Prints `2` to the console
superSpider.boost();
console.log(superSpider.level); // Prints `12` to the console
And for reference here is the console output of our superSpider class pointing out the levelUp and boost functions
I’m sure you’re thinking that this is pretty convoluted and you would be right. Next time I’ll be showing you an ES5 convenience method to clean this up, as well as some YUI OOP methods which greatly simplify this task as well as handle all of the edge cases. Thanks for reading and if you have any questions or comments you can comment below, or mention me @fromanegg. Till next time!