Prototypes are the foundation of JavaScript. They can be used to shorten the code and to reduce memory consumption significantly. Prototype makes also possible to control the inherited properties, and dynamically change existing properties, and add new properties to all the instances created from a constructor function without updating every instance one by one. They can also be used to hide properties from iterations, and prototype helps to avoid naming conflicts in a large object.
Memory consumption
I made a super simple practical example at jsFiddle, it uses jQuery, and is looking like this:
HTML: <div></div>
JS: const div = $('div'); console.log(div);
If we now peek to the console, we can see jQuery has returned an object. That object has 3 own properties and 148 properties in its prototype. Without the prototype, all those 148 properties should had been assigned as own properties to the object. Maybe that's yet a bearable memory load for a single jQuery object, but you might create hundreds of jQuery objects in a relatively simple snippet.
But, those 148 properties are just starters, open the logged tree from its very first property 0
, there are a lot of more own properties for the queried div
element, and at the end of the list, a prototype, HTMLDivElementPrototype
. Open it, and you'll find a couple of properties, and again a prototype: HTMLElementPrototype
. Open that, a long list of properties is exposed, and ElementPrototype
at the end of the list. Opening that reveals again a lot of properties, and a prototype named NodePrototype
. Open that in the tree, and go through the list at the end of that prototype, there's yet one more prototype, EventTargetPrototype
, and finally, the last prototype in the chain is Object
, which also has some properties in it.
Now, some of all these revealed properties of a div
element are objects themselves, like children
, which has a single own property (length
) and some methods in its prototype. Luckily, the collection is empty, but if we had had added a couple of div
elements inside the original, all the properties listed above would have been available for each child of the collection.
If there were no prototypes, and all the properties would be own properties of objects, your browser would still work on that single jQuery object when you've reached this point in this answer when reading. You can just imagine the work load when there are hundreds of elements collected to the jQuery object.
Object iteration
How does a prototype help in iterations then? JavaScript objects have a concept of own properties, that is, some of the properties are added as own properties, and some of the properties are in __proto__
. This concept makes it possible to store the actual data and the metadata into the same object.
While with the modern JS it's trivial to iterate through the own properties, we've Object.keys
, Object.entries
etc, that's not always have been the case. In the early days of JS, there was only for..in
loop to iterate through the properties of an object (in very early days there was nothing). With in
operator we're getting also properties from the prototype, and we've to separate the data from the metadata with hasOwnProperty
check. If everything were in own properties, we couldn't have made any separation between the data and the metadata.
Inside a function
Why the prototype is a property of functions only then? Well, it kinda isn't, functions are also objects, they just have an internal [[Callable]] slot, and a very special property, executable function body. As any other JS type has a "locker" for the own properties and the prototype properties, a function has that third "locker", and a special ability to receive arguments.
The own properties of functions are often called static properties, but they're as dynamic as the properties of regular objects. The executable body of the function and the ability of receiving arguments makes a function ideal for creating objects. Just compare to any other object creation method in JS, you can pass parameters to the "class" (= constructor function), and make very complex operations to get a value for a property. Parameters are also encapsulated inside the function, you don't need to store them in the outer scope.
Both of these advantages are not available in any other object creation operation (you can of course use an IIFE ex. in an object literal, but that's somewhat ugly). Furthermore, the variables declared inside a constructor are not accessible outside of the function, only methods created inside the function can access these variables. This way you can have some "private fields" in the "class".
The default properties of function and shadowing
When we examine a newly-created function, we can see it has some own properties in it (s.c. static properties). These properties are marked as non-enumerable, and hence they're not included in any iterations. The properties are arguments <Null>
, caller <Null>
, length <Number>
, name <String>
and prototype <Object>
containing constructor <Function>
, and the underlying prototype <Function>
of the function itself.
Wait! There are two separate properties with the same name in the function, and even with different types? Yes, the underlying prototype
is the __proto__
of the function, the other prototype
is an own property of the function, which shadows the underlying prototype
. All objects have a mechanism of shadowing a property of the __proto__
, when a value to a property with the same name existing in __proto__
is assigned. After that, object itself can't access that property in __proto__
directly, it is said to be shadowed. The shadowing mechanism preserves all the properties, and this way handles some naming conflicts. The shadowed properties are still accessible by referring them via the prototype.
Controlling inheritance
As prototype
is an own property of the function, it's free booty, you can replace it with a new object, or edit it as you wish, making so doesn't have an effect to the underlying "__proto__
", and won't conflict with the "untouched __proto__
" principle.
The power of the prototypal inheritance lies exactly on the ability to edit or replace the prototype. You can choose what you want to inherit, and you can also choose the prototype chain, by inheriting the prototype object from other objects.
Creating an instance
How the object creation with a constructor function works, is probably explained thousand times in SO posts, but I put a brief abstract here once again.
Creating an instance of the constructor function is already familiar. When calling a constructor with new
operator, a new object is created, and put to the this
used inside the constructor. Every property assigned with this
becomes to own property of the newly-created instance, and the properties in prototype
property of the constructor are shallow copied to the __proto__
of the instance.
This way all the object(ish) properties are preserving their original reference, and actual new objects are not created, just the references are copied. This provides the ability to throw the objects around without need to re-create them every time when they are needed in some other object. And when linked like this, it makes also dynamic edits possible to all the instances at once with a minimum effort.
prototype.constructor
What is the meaning of the constructor
in prototype
of the constructor function then? That function originally refers to the constructor function itself, it's a circular reference. But when you make a new prototype, you can override the constructor in it. A constructor can be taken from another function, or omitted alltogether. This way you can control the "type" of the instance. When checking whether an instance is an instance of a particular constructor function, you use instanceof
operator. This operator checks the prototype chain, and if it finds a constructor of another function from the chain, it is considered as a constructor of the instance. This way all the constructors found from the prototype chain are kinda constructors of the instance, and the "type" of an instance is any of those constructors.
Without a doubt, all this could have been achieved with some other design as well. But to answer the question "why", we need to dive in the history of JS. A recently published book by Brendan Eich and Allen Wirfs-Brock sheads some light to this question.
Everyone agreed that Mocha would be object-based, but without classes,
because supporting classes would take too long and risk competing with
Java. Out of admiration of Self, Eich chose to start with a dynamic
object model using delegation with a single prototype link.
Cite: Page 8 in JavaScript: The First 20 Years, written by Brendan Eich and Allen Wirfs-Brock.
A more deep explanation and background can be gotten by reading the book.
The code part
In your edit there were some questions arised in the comments of the code. As you've noticed, ES6 class syntax hides a regular constructor function. The syntax is not totally only syntactic sugar over the constructor functions, it also adds a more declarative way to create a constructor, and is also capable to subclass some native objects, like Array.
- "JS doesn't work that way" Correct,
method
is not an own property of the class (= function).
- "This is how it works" Yes, the methods created in a class are assigned to the prototype of the class.
- "Delete the reference to it in prototype object" Not possible, because the prototype is freezed. You can see this by revealing the descriptors of the class.
- The rest of the code ... No comment, not recommended.