Do you really understand how prototyping works in JS? Check yourself!
Diving deep in prototypal inheritance in Javascript.
Introduction
Recently I started to notice that many of my colleagues don’t clearly understand how prototyping works in JS. That’s why I wrote this article to get rid of any confusions in this question. I propose you to go through this test and check yourself. After that I’m going to explain why it works this way. And at the end I will provide correct answers. So, lets begin!
console.log({}.__proto__ === {}.prototype);
console.log({}.__proto__ === Object.prototype);
console.log([].__proto__ === Object.prototype);
function f1() {}
function f2() {}
const arrowFunc = () => {};
console.log(f1.__proto__ === f2.__proto__);
console.log(f1.prototype === f2.prototype);
console.log(f1.__proto__ === f1.prototype);
console.log(f1.__proto__ === Function.prototype);
console.log(arrowFunc.__proto__ === arrowFunc.prototype);
console.log(arrowFunc.__proto__ === Function.prototype);
console.log(arrowFunc.prototype === Object.prototype);
console.log((10).__proto__ === (10).prototype);
console.log((10).__proto__ === Number.prototype);
console.log((10).prototype === Number.prototype);
class Animal {
constructor() {}
walk() {}
}
class Cat extends Animal {
meow() {};
}
class Dog extends Animal {
bark() {};
}
const catInstance = new Cat();
const dogInstance = new Dog();
console.log(catInstance.__proto__ === Cat.prototype);
console.log(catInstance.__proto__ === dogInstance.__proto);
console.log(catInstance.prototype === dogInstance.prototype);
console.log(catInstance.__proto__.__proto__ === Animal.prototype);
console.log(Array.__proto__ === Array.prototype);
console.log(Object.__proto__ === Object.prototype);
console.log(Function.__proto__ === Function.prototype);
console.log(Function === Function.__proto__.constructor);
console.log(Function.__proto__.__proto__ === Function.prototype.__proto__);
console.log(Boolean.call === Function.prototype.call);
Not quite-obvious at first glance, isn’t it? Lets crack this nut.
Diving deep
Before we dive in, I highly recommend to refresh the basics of prototyping, without it understanding the below explanations would not be easy.
[[Prototype]] vs .prototype vs __proto__
First things first. We have 3 definitions, which are supposed to relate to prototyping inheritance, but how exactly?
Check the next code example and the related image to it.
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
}
getName() {
return `${this.make} ${this.model}`;
}
}
const fordInstance = new Car('Ford', 'Mustang');
console.log(fordInstance.getName()); // Ford Mustang
Now, the visual easy-to-understand presentation:
The algorithm is next: when we define a class or function in our code the JS engine creates a new object (prototype object, empty be default) simultaneously and allocates memory for it. You can check this by just typing this line in the console:
console.dir(function func() {})
When the JS interpreter reaches the moment when we call our class through the keyword ‘new’ (the same for regular funciton with ‘new’ as well) the JS engine manages this behaviour not as a usual function. I remind, classes in JS are a syntactic sugar of Function, the logic is pretty much the same. There are a few differences, and I won’t describe them here since they don’t matter. So, after the class Car was invoked, it creates a new object with certain fields in it. We place that object in the fordInstance variable. Also, the engine sets a connection between instance and Car.prototype via the internal [[Prototype]] mechanism. The developer doesn’t have any access to it at all. But, we are able to manipulate prototypes with __proto__.
__proto__ is a getter/setter for [[Prototype]]. And where can we get it? Simply: using the same prototype chain! Going through it, we can get a Object.prototype at the end, which has getter/setter __proto__. We are able to call it straight from fordInstance and get our Car.prototype. Check the code below.
console.dir(fordInstance.__proto__) // Car.prototype
console.dir(Object.getPrototypeOf(fordInstance)) // The same, just modern way
Since this is not only getter but setter, we can change our prototype. And I recommend to do it rarely and wisely (You may brake a prototype chain!).
And guess the most important thing is that each object in JS (everything in JS is an object, huh) has __proto__! And always refers to the prototype of the class with which it was created. Remember this rule in ages. I will describe it in details on the next paragraph.
Okay, but what about .prototype?
The main thing about .prototype, is that it might be only in either functions or class. Try to remember this as well.
As I said before, its another object, which was attached to the fordInstance after it was created. That object contains all methods of our car, and it’s a really convenient way to organize them, otherwise we would have all of these methods inside each instance, and this is the worst memory usage.
Quick recap: [[Prototype]] is an internal mechanism of prototype inheritance. You may treat it as a regular reference, but without access to it. On the other hand, browsers give us the ability to adjust prototype chain via __proto__. Almost in all cases, you may think that [[Prototype]] and __proto__ are equal. These are only links to the prototype! And prototype is an object referenced by __proto__. If there is no method within instance, engine will check it in prototypes till the end using prototype inheritance.
Each object has __proto__.
Each function contains .prototype and __proto__ as well.
Moving to examples
console.log({}.__proto__ === {}.prototype); ?
It’s false. When we use an object literal its the same as new Object(). Do you remember what’s going on when we call a function with new keyword. Right, it attaches instance ({} in our case) to the prototype (which is Object.prototype, thanks Captain Obvious)! And as I mentioned, you can call .prototype only in functions, so the right part of the assignment is undefined. Hence, answer is falsy.
Next condition returns true:
console.log({}.__proto__ === Object.prototype); // true
What about this example:
console.log([].__proto__ === Object.prototype); ?
As you might guess, its false. Because [].__proto__ is equal to new Array().__proto__. And new Array().__proto__ === Array.prototype because [] is instance of Array. On other hand, Array.prototype.__proto__ === Object.prototype, because Array.prototype is a regular object, and as a usual object it has a reference to the Object.prototype.
These conditions are both true:
console.log([].__proto__ === Array.prototype);
console.log([].__proto__.__proto__ === Object.prototype);
The same behaviour here:
console.log((10).__proto__ === Number.prototype); // Well, its true
We created 10 in a straightforward way, but under the hood JS will use new Number(10). number is a primitive value, but when we call it through the “dot”, JS converts it in object ( it’s called autoboxing). Thats why a prototype chain works with primitive types as well. I provided some example below:
console.log((20).toString()) // 20 as a string type
console.log('string'.repeat(2)) // 'stringstring'
Functions and Classes
Functions in JS are tricky. They may have __proto__ and prototype at the same time. If you call a function without ‘new’ keyword, you DON’T need to think about prototype at all. Simply because you don’t create a new instance, you treat that function as a usual instruction to do something. And all functions, except arrow functions, have its own prototype.
Lets talk about __proto__ in functions. Functions in JS are objects too, therefore they can get a __proto__. Here’s some code and 2 images for better understanding:
function usualFunction() {}
console.log(usualFunction.__proto__ === Function.prototype); // true
console.log(usualFunction.call === Function.call); // true
console.log(usualFunction.valueOf === Object.prototype.valueOf); // true
Alright, we talked enough about prototype inheritance. I believe you got the idea. Let’s spend a few minutes and discuss about __proto__ in built-in classes.
There are a lot of built-in classes in JS, such as String, Number, Promise, Function, Object, Array, Date etc. All of these classes are functions. And they may have a __proto__ and a .prototype simultaneously. If we call them with new, we stick their prototypes to the new instance. But what about __proto__?
[[Prototype]] or just __proto__ in functions is completely another thing than .prototype. You should remember, that __proto__ always refers to the prototype of the class with which it was created. But how are other classes/functions created? Of course, with the new() Function. In JS only functions can create other functions. All of these built-in classes were created via function. That’s why each __proto__ of these classes refers to the Function.prototype. You may ensure:
console.log(String.__proto__ === Function.prototype); // true
console.log(Number.__proto__ === Function.prototype); // true
console.log(Promise.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Array.__proto__ === Function.prototype); // true
console.log(Date.__proto__ === Function.prototype); // true
Lets also describe this part of code:
class Animal {
walk() {}
}
class Cat extends Animal {
meow() {};
}
const catInstance = new Cat();
Here we have class Animal. Since this is a class it has its own prototype and a __proto__ (Animal.__proto__ === Function.prototype), because class Animal itself was created with new Function() under the hood. The more interesting thing is with class Cat. It has prototype too, which is connected to the parent prototype (Animal.prototype). But what about __proto__? We need to check, how class Cat was created. And in this case it was born from class Animal (extends keyword). Hence, Cat.__proto__ === Animal. Let me show you the scheme:
It looks complex at first glance. Don’t be afraid. Red lines are the main prototype chain. Green lines — the reference from class to its own prototype. Yellow — the reference to the constructor, which created this instance. Eventually you can check it your browser and play with it.
Like I mentioned before, the end of the prototype chain is always Object.prototype. Parent of each class/function is Function.prototype.
Weird behaviour with prototype
I’m gonna show you the really weird behaviour with prototypes. Try to put this code in your console:
console.log(Function.prototype, typeof Function.prototype === 'function'); // true
console.log(Array.prototype, Array.isArray(Array.prototype)); // true
You should see the following:
You may ask: Hey, you said that prototype is always an object, why do we see a function and an array respectively? I know it looks quite strange. But what if I say that you should treat them as regular objects! It was done by ECMA due to backward compatibility. You may read about it here and here.
Conclusion
- __proto__ is a getter/setter for [[Prototype]]
- prototype is always object
- All objects have __proto__
- Only functions have prototype, except array functions, async functions and some others
- When you call function without new keyword you don’t need its prototype
- .prototype of function doesn’t affect on function itself, its only a prototype object for new instances
- Try to avoid manipulations with prototypes manually
- End of each prototype chain is Object.prototype
- Despite prototypes and ‘this’ mostly be next to each other, its completely different independent parts.
- And the most important: try to play with above examples in your browser. Use console.dir for convenient revealing each prototype.
Answers:
console.log({}.__proto__ === {}.prototype); // false
console.log({}.__proto__ === Object.prototype); // true
console.log([].__proto__ === Object.prototype); // false
function f1() {}
function f2() {}
const arrowFunc = () => {};
console.log(f1.__proto__ === f2.__proto__); // true
console.log(f1.prototype === f2.prototype); // false
console.log(f1.__proto__ === f1.prototype); // false
console.log(f1.__proto__ === Function.prototype); // true
console.log(arrowFunc.__proto__ === arrowFunc.prototype); // false
console.log(arrowFunc.__proto__ === Function.prototype); // true
console.log(arrowFunc.prototype === Object.prototype); // false
console.log((10).__proto__ === (10).prototype); // false
console.log((10).__proto__ === Number.prototype); // true
console.log((10).prototype === Number.prototype); // false
class Animal {
constructor() {}
walk() {}
}
class Cat extends Animal {
meow() {};
}
class Dog extends Animal {
bark() {};
}
const catInstance = new Cat();
const dogInstance = new Dog();
console.log(catInstance.__proto__ === Cat.prototype); // true
console.log(catInstance.__proto__ === dogInstance.__proto); // false
console.log(catInstance.prototype === dogInstance.prototype); // true
console.log(catInstance.__proto__.__proto__ === Animal.prototype); // true
console.log(Array.__proto__ === Array.prototype); // false
console.log(Object.__proto__ === Object.prototype); // false
console.log(Function.__proto__ === Function.prototype); // true, I know what you feel bro
console.log(Function === Function.__proto__.constructor); // true
console.log(Function.__proto__.__proto__ === Function.prototype.__proto__); // true
console.log(Boolean.call === Function.prototype.call); // true