How does JavaScript .prototype work?
This is a very simple prototype-based object model that would be considered as a sample during the explanation, with no comment yet:
function Person( name ){
this.name = name;
}
Person.prototype.getName = function(){
console.log( this.name );
}
var person = new Person( 'George' );
There are some crucial points that we have to consider before going through the prototype concept.
1- How JavaScript functions actually work:
To take the first step we have to figure out how JavaScript functions actually work: as a class like function using this
keyword in it, or just as a regular function with its arguments. In addition, we have to figure out what the function does, and what it returns.
Let's say we want to create a Person
object model, but in this step, I'm gonna be trying to do the same exact thing without using prototype
and new
keyword.
So, in this step functions
, objects
and this
keyword are all we have.
The first question would be how this
keyword could be useful without using new
keyword.
So to answer that, let's say we have an empty object and two functions like:
var person = {};
function Person( name ){
this.name = name;
}
function getName(){
console.log( this.name );
}
JavaScript has 3 different ways to use these functions without using new
keyword.
a. Call the function as a regular function:
Person( 'George' );
getName(); //would print the 'George' in the console
In this case, this
would be the current context object, which is usually is the global window
object in the browser or GLOBAL
in Node.js
. It means we would have window.name
in browser or GLOBAL.name
in Node.js, with 'George'
as its value.
It is worth noting that running this code snippet in 'use strict'
mode would prevent this
keyword to get attached to the window
object in browser or GLOBAL
in Node.js. Instead, the value of the this
would be undefined
. To learn more about the strict mode, take a look at this post: Strict mode.
b. Attach the function to an object as its property
-The easiest way to do this is by modifying the empty person
object, like:
person.Person = Person;
person.getName = getName;
This way, we can call the person like:
person.Person( 'George' );
person.getName();// -->'George'
Now, the person
object becomes:
Object { Person: function, getName: function, name: 'George' }
-The other way to attach a property to an object is by using the prototype
of that object that can be found in any JavaScript object with the name of __proto__
. I'll explain this a bit later in the summary, but what's important is we can get a similar result by doing:
person.__proto__.Person = Person;
person.__proto__.getName = getName;
However, we are actually modifying the Object.prototype
, because whenever we create a JavaScript object using literals ( { ... }
), it gets created based on Object.prototype
. This means it gets attached to the newly created object as an attribute named __proto__
, so if we change it as we have done on our previous code snippet, all the JavaScript objects would get changed. Not a good practice. So what could be the better practice now:
person.__proto__ = {
Person: Person,
getName: getName
};
And now the other objects are in peace, but it still doesn't seem to be a good practice. So we have still one more solution, but to use this solution we should get back to that line of code where the person
object got created ( var person = {};
), and then change it like:
var propertiesObject = {
Person: Person,
getName: getName
};
var person = Object.create( propertiesObject );
What this does is it creates a new JavaScript Object
and attaches the propertiesObject
to the __proto__
attribute. So to make sure this works, you can do:
console.log( person.__proto__ === propertiesObject ); //true
However, the tricky point here is you have access to all the properties defined in __proto__
on the first level of the person
object (read the summary for more detail).
Warning: While support for Object.prototype.__proto__
already exists today in most browsers, its behavior has only been standardized recently in the new ECMAScript 6 specification. If you need support for pre-ES6 browsers, it is recommended that only Object.getPrototypeOf()
be used instead.
As you see using any of these two-way this
would point to the person
object.
call or apply to invoke the function
c. Use- The
apply()
method calls a function with a giventhis
value and with arguments provided as an array (or an array-like object).
and
- The
call()
method calls a function with a giventhis
value and with arguments provided individually.
This way which is my favorite, as we can easily call our functions like:
Person.call( person, 'George' );
or
//apply is more useful when params count is not fixed
Person.apply( person, [ 'George' ]);
getName.call( person );
getName.apply( person );
these 3 methods are the important initial steps to figure out the .prototype
functionality.
new
keyword work?
2- How does the Understanding how the new
keyword works is the second step to understand the .prototype
functionality. This is what I use to simulate the process:
function Person( name ){
this.name = name;
}
myPersonPrototype = {
getName: function(){
console.log( this.name );
}
};
In this part I'm going to take all the steps JavaScript takes when using the new
keyword, but I'm not going to use the new
keyword and prototype
. So when we do new Person( 'George' )
, Person
function serves as a constructor, these are what JavaScript does, one by one:
a. Make an empty object
Javascript basically makes an empty hash like:
var newObject = {};
b. Attach the all prototype objects to the newly created object
We have myPersonPrototype
here similar to the prototype object.
for( var key in myPersonPrototype ){
newObject[key] = myPersonPrototype[ key ];
}
This is not the way that JavaScript actually attaches the properties that are defined in the prototype. The actual way is related to the prototype chain concept.
An Alternative to Steps a & b:
Instead of taking steps a & b, you can get the exact same result by doing:
var newObject = Object.create( myPersonPrototype );
//here you can check out the __proto__ attribute
console.log( newObject.__proto__ === myPersonPrototype ); //true
//and also check if you have access to your desired properties
console.log( typeof newObject.getName );//'function'
now we can call the getName
function in our myPersonPrototype
:
newObject.getName();
c. Give the object to the constructor,
We can do this with our sample like:
Person.call( newObject, 'George' );
or
Person.apply( newObject, [ 'George' ] );
Then the constructor can do whatever it wants, because this inside of that constructor is the object that was just created.
This the end result before simulating the other steps:
Object {
name: 'George'
}
Summary:
Basically, when you use the new keyword on a function, you are calling on that and that function serves as a constructor, so when you say:
new FunctionName()
JavaScript internally makes an object, an empty hash. Then it gives that object to the constructor, where the constructor can do whatever it wants because this inside of that constructor is the object that was just created. Finally, it gives you that object if you haven't used the return statement in your function, or if you've put a return undefined;
at the end of your function body.
So when JavaScript goes to look up a property on an object, the first thing it does is it looks it up on that object. Then, there is a secret property [[prototype]]
, usually as __proto__
, and that property is what JavaScript looks at next. Furthermore, when JavaScript looks through the __proto__
, as far as it is again another JavaScript object, it has its own __proto__
attribute. JavaScript goes up and up until it gets to the point where the next __proto__
is null.
Object.prototype
is the only object in JavaScript where its __proto__
attribute is null:
console.log( Object.prototype.__proto__ === null );//true
and that's how inheritance works in JavaScript.
In other words, when you have a prototype property on a function and you call a new
on that, after JavaScript finishes looking at that newly created object for properties, it will go look at the function's .prototype
. Thus, it is possible that this object has its own internal prototype, and so on.