The magical world of JavaScript prototypes
| Tags: front-end
How many times have we heard "JavaScript is not an Object-Oriented language, it's Prototype-oriented"? It turns out it's not accurate.
Here there are some objects in JavaScript, each created in a different way:
(({} instanceof Object)(
// => true
[] instanceof Object
));
// => true
function Foo() {}
new Foo() instanceof Object;
// => true
So we do have objects in JavaScript. So, what about prototypes? It's the mechanism by which JavaScript implements its Object Orientation. So yes, JavaScript it is a prototype-based, Object-Oriented language.
With the arrival of ES6 classes, some folks may think that it's not worth it to learn how to deal with prototypes. This is untrue for a few reasons:
-
ES6 classes are basically syntax sugar for prototypes. Instances of ES6 "classes" are still prototype-based.
-
There is a vast ES5 (i.e. with no classes) codebase around the world, and chances are you will have to deal with it sooner or later.
With all of this, let's learn a bit about JavaScript prototypes, shall we?
A prototype is just an "special object" embedded in an Object. In JavaScript we can access it via the property __proto__
:
const witch = { name: 'Hermione' };
witch.__proto__;
// => {} (empty prototype)
What makes this special is that the prototype acts as some kind of "proxy" or "backup", transparently. If we try to access a property that is not present in an Object, but the prototype does have it, JavaScript will return the prototype's. Continuing the previous example:
// add a property to the prototype
witch.__proto__.spells = { leviosa: 'Wingardium leviosa' };
// the property is not defined by the object…
witch;
// => { name: "Hermione" }
// …but we can access it thanks to the prototype!
witch.spells;
// => { leviosa: "Wingardium leviosa" }
What's the practical application of this? To share code among Objects. In Object-Oriented languages which have classes, the class acts a "template" that is shared among all the instances of that class. In JavaScript, there is no "template": what we have is a shared common object, the prototype.
We can easily see this when we instantiate objects using a constructor function. If we have a Wizard
function, each time we create a new object with new Wizard()
, what's defined in the property Wizard.prototype
is established as the prototype of the newly created instances.
function Wizard(name) {
this.name = name || 'Anonymous';
}
Wizard.prototype.spells = {
leviosa: 'Wingardium leviosa',
expelliarmus: 'Expelliarmus',
patronus: 'Expecto patronum',
};
const draco = new Wizard('Draco');
// => Wizard { name: "Draco" }
const hermione = new Wizard('Hermione');
// => Wizard { name: "Hermione" }
draco.spells === hermione.spells;
// => true (both wizards share spells)
draco.__proto__ === hermione.__proto__;
// => true (that's why they share prototypes)
hermione.__proto__ === Wizard.prototype;
// => true (their prototype is defined in Wizard.prototype)
The benefits of sharing this common object –the prototype– are:
- To avoid duplication in memory, since the prototype is shared by all the Objects that need it, instead of each one having a replica of it.
- To be able to modify multiple objects on the fly in a go, by modifying the prototype.
Thanks to this system, we can also modify only specific Objects, by adding properties that only them have. If this property has the same name of a property in the prototype, the one contained directly in the Object will have precedence. For instance, we could have a first-year student in Hogwarts with an empty spellbook:
const newbie = new Wizard('Lorem');
newbie.spells = {}; // bypass what's in the prototype
newbie.spells === hermione.spells;
// => false
And now let's imagine that in the Wizarding World a huge discovery has been made, and they have learned conjure up authentic horchata on demand. We could easily update everyone's spellbook –as long as it has not been previously overridden–, by simply altering the prototype itself.
// add a new spell
Wizard.prototype.spells.horchata = 'Send horchata';
// check Hermione's spellbook
hermione.spells;
// => { leviosa: "Windgardium leviosa",
// expelliarmus: "Expelliarmus",
// patronus: "Expecto patronum",
// horchata: "Send horchata" }
This is a very powerful feature, but thanks to Marvel we all now that with great power comes great responsibility. Even more in JavaScript, since it's too easy to deeply mess with prototypes. How far can we go? Well, we can even alter the prototype of objects that are part of the standard library, like Object
, Date
, Array
… Here's a hacky example, which I have named the "Flamenca Trick":
Date.prototype.toString = () => '💃'`${new Date()}`;
// => 💃
I hope you enjoyed this short intro to JavaScript prototypes. Happy coding!