Have you ever inspected an object in your browser's console and stumbled upon the field [[Prototype]]
or __proto__
. Or maybe you have seen someone use the mystical prototype field on classes, such as String.prototype
. Ever wondered what kind of sorcery this prototype is? Great! ️This article will hopefully make things a bit clearer, and make you an advanced prototype wizard. 🧙♂️
Categories of Object-Orientation
For us to understand what prototypes are we are going to have to talk about categories of object-orientation. Hold on! Before you close this article, I promise this isn't going to be one of those articles that go into the nitty-gritty of programming language theory. I intend only to poke at the surface of the rabbit hole, so this should be digestible by anyone who has basic knowledge of object-oriented programming.
Still here? Awesome! Let's briefly look at two different categories of object-orientation, namely class-based and prototype-based object-orientation. 🤓
Class-based vs. Prototype-based Object-Orientation
Class-based and prototype-based object-orientation are categories referring to how inheritance is handled in the language. In class-based languages, such as Java, inheritance is defined in the class definitions through super-/sub-class relations, while in prototypical languages, objects inherit properties directly from other objects.
What does that even mean? Well, instead of JavaScript having classes that contain the objects' shared properties, such as methods, JavaScript instead uses other objects to store this. These objects are what we call prototypes. So all instances of the same "class" in JavaScript will share the same prototype object.
Why does JavaScript use Prototypes when it also has Classes?
Here's the cool part, there aren't really any classes in JavaScript! 🤯
The class
-syntax in JavaScript is just syntactic sugar for creating a prototype and an object-constructor function. Confused? Let's look at what this means.
The following example shows how you could implement a Santa
class in JavaScript. The Santa
class has some property hasEatenCookies
which is set in the constructor of the class. In addition, we also have a method to check if Santa is hungry. 🎅🍪🤰
class Santa {
constructor(hasEatenCookies) {
this.hasEatenCookies = hasEatenCookies;
}
isHungry() {
return this.hasEatenCookies === false;
}
}
Since JavaScript doesn't really have classes, this would be transformed into a prototype object and a function for constructing Santa
objects, like the following:
const santaPrototype = {
isHungry: function() {
return this.hasEatenCookies === false;
}
}
function santaConstructor(hasEatenCookies) {
const santaObject = {
hasEatenCookies: hasEatenCookies
}
Object.setPrototypeOf(santaObject, santaPrototype)
return santaObject
}
Note: This is a somewhat simplified example of a class transformation, if you would like to see a more complete transformation you can check out this Babel REPL.
As we can see, the methods of the Santa
class has been put into a normal object, while the constructing of objects has been made into a standard function. The santaConstructor
function simply creates an object with the hasEatenCookie
property, and the object is then assigned the santaPrototype
.
How does Inheritance work with Prototypes?
It is actually quite simple. Since prototypes themselves are just objects, they can themselves also have prototypes. So if we made a subclass, BadSanta
, of our Santa
class, this would result in our BadSanta
prototype having the Santa prototype as its prototype...
That was a lot of prototype at once. Let's instead look at some code. Below we have implemented our BadSanta
class, which introduces a method to check if BadSanta
cares if you've been naughty or nice.
class BadSanta extends Santa {
constructor(hasEatenCookies) {
super(hasEatenCookies)
}
function caresIfYouveBeenNaughtyOrNice() {
return false;
}
}
We could then create an instance of BadSanta
and call the methods from both our BadSanta
and our Santa
class.
const willie = new BadSanta(false);
willie.caresIfYouveBeenNaughtyOrNice(); // false
willie.isHungry(); // true
What is happening here is that for every time we try to reference an attribute on an object, there's a check performed to see if the attribute exists on the object. If it doesn't exist it will do the same check on its prototype, recursively.
So when we do willie.isHungry()
, JavaScript will check if isHungry
exists on the object willie
. Since willie
doesn't have any attribute with that name it will check its prototype, BadSanta
. BadSanta
doesn't have this attribute either, so it will check its prototype, Santa
. Fortunately, the Santa
prototype has this attribute, and we can use this function for our function call.
That's it!
As we have seen, prototypes aren't all that mystical. They are just objects used to share properties between objects, such as the methods we would define in a class. We also saw how inheritance is handled in JavaScript, where class-based languages has classes with superclasses, JavaScript instead has prototypes with parent prototypes. This is possible since prototypes themselves are just objects.