Introduction
Aaaaah, prototypes... How many blog posts did you read where prototypes are listed as a must-know characteristic of the language? How many times senior developers told you about prototypal inheritance? I've spent quite some time avoiding to learn more deeply about this thing. I got tired of procrastinating, so I wrote this thing.
Simple words please... with examples?
Objects in Javascript have an internal property ( in the specification called [[Prototype]] ). This internal property is a reference to another object. Quick example:
// A simple object
const myObject = {
a: 2,
};
console.log(myObject.a); // 2
// We link newObject to myObject with Object.create
const newObject = Object.create(myObject);
console.log(newObject); // {}
console.log(newObject.a); // 2 ??? Why?
Object.create creates a new object. It takes another object as an argument. The common way to think about what is happening is ( the classical way ) : I made a copy of this object. Well, no.
As you can see, newObject is empty. Object.create takes a prototype as its argument. Which means, we didn't copy, we linked newObject to myObject. myObject becomes a prototype of newObject. To know what is inside the prototype of an object, you can use **proto**.
console.log(newObject.__proto__); // { a: 2 }
console.log(myObject.isPrototypeOf(newObject)); // true
Chains have links, [[Prototype]] is a chain. So how does Javascript uses prototypes to retrieve values?
Up the chain... one link at a time.
const original = {
a: 2,
};
const secondComing = Object.create(original);
const thirdLink = Object.create(secondComing);
console.log(thirdLink); // {}
console.log(secondComing); // {}
console.log(secondComing.isPrototypeOf(thirdLink)); // true
console.log(original.isPrototypeOf(thirdLink)); // true
console.log(thirdLink.isPrototypeOf(original)); // false
console.log(thirdLink.a); // 2
Here is how your favourite language works: it tries to get the property a in the thirdLink object. Can't find it. Does it return undefined or a error? Nope, it looks in the prototype chain for a link. It finds out that secondComing is a prototype of thirdLink. It looks for a, still can't find it. It moves on to another link, called original. Finds a = 2 !!
What if I change something in the bottom of the chain?
- How will it affect the top of the chain? Such a great question.
I decide to change the value a in thirdLink directly:
thirdLink.a = 3;
console.log(thirdLink); //{ a: 3 }
console.log(thirdLink.a); // 3
console.log(original.a); // 2
This is what we call a shadowed property. The new a value shadows the other a values present in the higher prototypes.
What if I want some ice on it?
What if the property in the top link can't be overwritten?
// Freeze the original, properties can't be changed
Object.freeze(original);
original.a = 3;
// a is still equal to 2
console.log(original); // { a: 2 }
// That will NOT change the value, or shadow it.
thirdLink.a = 3;
console.log(thirdLink); // {}
console.log(thirdLink.a); // 2
Nothing changed because the prototype's property a is read-only.
However, if you need to change the property value anyway when it is read-only. You must use Object.defineProperty:
// Freeze the original, properties can't be changed
Object.freeze(original);
// Ok, this will work.
Object.defineProperty(thirdLink, "a", { value: 5 });
console.log(thirdLink.a); // 5
So, whenever you think you are changing a value in a object, you must account for the prototypes up the chain. They may have properties with the same name that cant be overwritten in a certain way.
What does it mean for functions?
In a class oriented language, you can create different instances of a class. You copy the class behavior into an object. And this is done again every time you instantiate a class.
In Javascript, however, there are no classes, just objects. The class keyword is only a syntax thing, it doesn't bring anything class-y to the table. Whatever you can do with the class keyword in ES6, you could do without a problem in ES5.
By default, every function gets a prototype property.
function hello() {
return "Hello World";
}
function goodBye() {
return "Goodbye";
}
console.log(hello.prototype); // hello {}
console.log(goodBye.prototype); // goodBye {}
Ok, so what happens if you don't copy like class-oriented languages? You create multiple objects with a [[Prototype]] link. Like so:
const a = new hello();
const b = new hello();
const c = new goodBye();
const d = new goodBye();
console.log(Object.getPrototypeOf(a) === hello.prototype); // true
console.log(Object.getPrototypeOf(b) === hello.prototype); // true
console.log(Object.getPrototypeOf(c) === goodBye.prototype); // true
console.log(Object.getPrototypeOf(d) === goodBye.prototype); // true
All our objects link to the same hello.prototype or goodBye.prototype origin. So, our objects ( a, b, c and d ) are not completely separated from one another, but linked to the same origin. So, if I add a method in hello.prototype, a and b will have access to it, because Javascript will go up the chain to find it. But, I didn't change anything about a and b:
// I'm not touching a or b
hello.prototype.sayHowDoYouDo = () => {
console.log("How do you do?");
};
a.sayHowDoYouDo(); // How do you do?
b.sayHowDoYouDo(); // How do you do?
By NOT copying objects but linking them, Javascript doesn't need to have the entire object environment carried in each object. It just goes up the chain.
Let's now make the goodBye.prototype a prototype of hello.prototype:
// Objects not linked yet => Errors
c.sayHowDoYouDo(); // Error: not a function
d.sayHowDoYouDo(); // Error: not a function
// This is a ES6 method. First argument will be the link at the bottom of the prototype chain, the second is the top link.
Object.setPrototypeOf(goodBye.prototype, hello.prototype);
// Now, c and d will look up the chain!
c.sayHowDoYouDo(); // How do you do?
d.sayHowDoYouDo(); // How do you do?
Prototypal inheritance
And that, my dear friends, is the concept of prototypal inheritance. Now, I am not a huge fan of the word inheritance here. It would imply some sort of copying, or parent-child relationship, and Javascript doesn't do that. I have seen the word delegation to describe this, I like it better. Again, Javascript doesn't natively copy objects, it links them to one another.
I see you waiting for some examples:
function Mammal(type) {
this.type = type;
this.talk = () => {
console.log("Hello friend");
};
}
Mammal.prototype.myType = function () {
return this.type;
};
function Dog(name, type) {
// This next line makes Mammal a prototype of the Dog object
Mammal.call(this, type);
this.name = name;
this.woof = () => {
console.log("Woof!");
};
}
// Link the Dog prototype to the Mammal prototype
Object.setPrototypeOf(Dog.prototype, Mammal.prototype);
//OR
// Dog.prototype = Object.create(Mammal.prototype)
Dog.prototype.myName = function () {
return this.name;
};
const Joe = new Dog("Joe", "Labrador");
Joe.woof(); // Woof!
// myName() function is in the Dog prototype.
console.log(Joe.myName()); // Joe
// myType is in the Mammal prototype.
// Joe is a Dog instance, and Mammap is a prototype of Dog.
console.log(Joe.myType()); // Labrador
// talk() is a method in the Mammal function, which is a prototype of the Joe object.
Joe.talk(); // Hello friend
It also works with objects, obviously. Quick example:
const SuperHero = {
statement: function () {
return "I am an anonymous superhero";
},
};
// SuperHero becomes a prototype of Batman.
const Batman = Object.create(SuperHero);
Batman.statement(); // 'I am an anonymous superhero'
Conclusion
Classical inheritance is a parent-child relationship. It goes from top to bottom. Javascript has prototypal delegation. Although it resembles the classical inheritance, it's quite different. Objects are linked together, not copied. The references are more from bottom to top.
Prototypes also helps with memory management because you don't need to carry the whole object environment every time you create a new child object. Everything that needs to be in common can exist in a prototype, therefore being referenced only once.
Tell me what you think about this, I hope I've been clear enough.