Loading...
It was a bit hard for me to understand how the prototype system in JS works, and by talking to other programmers, this concept is not well understood. I decided to write about the topic and try to explain prototype in the simplest way possible. In this article we will cover what you need to know about prototypes
Prototype is 2 things:
1 - simple object with properties and methods.
2 - properties and methods in prototype will be passed to children AKA inheritance
Lets try to clarify those two aspects of prototype with a simple example:
const myPrototype = {
hello: 'world',
sayHello: function() {console.log('hello')}
}
We said that prototype is an object with keys and methods like the one we created above. Now we also talked that those properties are passed to the children of this prototype. So lets create an new child object from the prototype we created. To do this we will create an object with the method Object.create which gets as an argument the prototype of the object we want to create.
const myPrototypeChild = Object.create(myPrototype)
with Object.create we created a child object of myPrototype whose name is myPrototypeChild. Now lets see what is happening when we try to access properties in the child that belongs to the parent.
console.log(myPrototypeChild.hello); // world
myPrototypeChild.sayHello(); // hello
From the child we can call properties and methods defined in the parent, we created inheritance. Lets check what will happen if we define a property on the child named hello.
myPrototypeChild.hello = 'foobar'
console.log(myPrototypeChild.hello); //foobar
We changed the property and print it so obviously we sould see our change, but did we just change the property on the parent? What will happen if I print that property in the parent?
console.log(myPrototype.hello); // world
notice that we didn't change anything on the parent, and printing the hello property in the parent will still equal the old value. So lets examine how js looks for properties in an object.
Lets examine what happened our child inheritend the properties from the parent, so at first we grabbed the hello property from the parent.
we then placed an hello property on the child object.
We later saw that when accessing that property again, it is no longer braught to us from the parent it is now a new property.
So now we have an hello property on the child and an hello property on the parent with different values.
From this example we can understand how JS works:
First I will look on myself, then I will look in my prototype.
Now lets define a grandchild:
const prototypeGrandchild = Object.create(myPrototypeChild)
We created a grandchild. Now lets try to access the function sayHello defined in the father myPrototype
prototypeGrandchild.sayHello()
so wait the grandchild can access methods on the child and also on the parent. What actually happens is that the prototypes are creating a linked list of prototypes. This list is called the prototype chain. If you examine the objects we created, you will notice a property that we didn't create called __proto__. that property will point to the prototype of the object, in our grandchild example this will point to the child object. and our child __proto__ will point the the parent myPrototype
prototypeGrandchild.__proto__ === myPrototypeChild; // true
prototypeGrandchild.__proto__.__proto__ === myPrototype; // true
It's considered bad practice to access the prototype using the __proto__ pointer since it is deprecated. It's better to access that property by using the method: Object.getPrototypeOf The previous example could have been written like so:
Object.getPrototypeOf(prototypeGrandchild) === myPrototypeChild; // true
Object.getPrototypeOf(Object.getPrototypeOf(prototypeGrandchild)) === myPrototype; // true
So when we are looking for a property in an object js will first look in the object, then in the __proto__ after that in the __proto__.__proto__ and so on until the end of the linked list.
We would think that myPrototype will be the last in our linked list of prototypes. But examining that object you will see it contains a __proto__ property of its own.
myPrototype.__proto__ === Object.prototype
In JS must of the types we create, will inherit from Object.prototype which means Object.prototype will be the first block of our prototype chain. For example method toString is defined in the Object.prototype that is why must objects will have that methods even without declaring one. Lets examine some prototype chains in different types we regularly use with JS
const myNumber = 3;
myNumber.__proto__ === Number.prototype; // true
myNumber.__proto__.__proto__ === Object.prototype; // true
const myString = 'hello';
myString.__proto__ === String.prototype; // true
myString.__proto__.__proto__ === Object.prototype; // true
const myBool = true;
myBool.__proto__ === Boolean.prototype; // true
myBool.__proto__.__proto__ === Object.prototype; // true
each one of the primitives has a 2 prototype chain list. the top prototype is the type prototype and its connected to the object prototype
the last types we will examine will be the array and function, which are really similar to the primitives.
const myArray = [];
myArray.__proto__ === Array.prototype; // true
myArray.__proto__.__proto__ === Object.prototype; // true
const myFunc = function() {};
myFunc.__proto__ === Function.prototype; // true
myFunc.__proto__.__proto__ === Object.prototype; // true
We created object and types the way we are used to and with Object.create. Lets examine what happens when we create objects with the new keyword
We can use the new keyword on classes and functions in JS.
The classes and functions are the custom types, and the new will create instance of that type.
The new will perform two things:
1 - create instance whose prototype is the type - basically: Object.create on the prototype of the type.
2 - run the constructor function on the instance context - basically calling call on the function while passing this as the instance.
So on code if we want to create a type Person with a name and say hello method, one might do so like this:
function Person(name) {
this.name = name;
this.sayHello = () => {
console.log(this.name);
}
}
So we defined here a custom type with a property of name and a function. Now we can create an instance object from that type by using the new keyword.
const me = new Person('yariv');
me.__proto__ === Person.prototype
// using the new keyword is equivelent to doing this:
const me2 = Object.create(Person.prototype)
Person.call(me2, 'yariv')
We created two instances of the custom Person type.
Our type has a name property and a sayHello method.
We already know that we can access the properties of the Person type from the instances we created, but it is also important to understand where these properties are located:
Is the name property and sayHello method located on the Person.prototype, or are they located on the object instance.
To determine this there is a method we can use hasOwnProperty
me.hasOwnProperty('name'); // true
me.hasOwnProperty('sayHello'); // true
me.hasOwnProperty('toString'); // false
hasOwnProperty will return true if the property is located on the instance and not along the prototype chain. This means that the properties defined in the Person type are located on the object, and this means for every object we define a seperate sayHello method. If we have 1M person instances we will have 1M seperate sayHello methods defined. If we define the sayHello method on the Person.prototype then every instance will have that function and there will only be one of those functions. Lets change the Person type:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(this.name);
}
And now if we create instances of the new person, the say hello will not be located in the instance but rather in the prototype chain of the instance.
const m3 = new Person('yariv');
m3.hasOwnProperty('sayHello'); // false
Now lets do something similar with classes and define a Person class.
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
const m4 = new Person('yariv');
m4.hasOwnProperty('sayHello') // false
using the class keyword you are basically define a function where you attach to the function prototype the methods like we did before. So if you examine the Person.prototype you will see the sayHello method is located here. So it's a bit easier to define our custom types using classes but basically class is a syntax sugercoat for creating function with the methods attached to the prototype of the function.
last topic we are going to talk about in this lesson that relates to prototype is defineProperty and for-in loops. For this example we will go back to the example we did at the beginning of the article:
const myPrototype = {
hello: 'world',
sayHello: function() {console.log('hello')}
}
const myPrototypeChild = Object.create(myPrototype);
const prototypeGrandchild = Object.create(myPrototypeChild)
we created our first prototype and the child and grandchild. to add a property to the grandchild i can simply do this:
prototypeGrandchild.foo = 'bar'
doing this is a syntax sugercoat for using Object.defineProperty method . So the above is equivelent to doing this:
Object.defineProperty(prototypeGrandchild, 'foo', {
enumerable: true,
value: 'bar',
writeable: true
});
defineProperty gives you more power on configuring the property in case you want it unwritable or uniterable. Now lets see what happens if we iterate on the object with the for in loop:
for (let key in prototypeGrandchild) {
console.log(key); // foo, hello, sayHello
}
What is intresting here that by iterating on the grandchild object we are also printing properties defined in the parent prototype. What this loop is doing is iterating on all the properties in the object and in the prototype chain and will print every property that was defined with enumerable set to true. Notice that with the regular way we assign properties by default they are enumerable. Which means that the number of iteration in the loop will depend on the entire prototype chain and if some enumerable property was added there. So even this:
Object.prototype.nerdeez = 'nerdeez.com'
for (let key in prototypeGrandchild) {
console.log(key); // foo, hello, sayHello, nerdeez
}
will print another key set to the object prototype.
Really hope that after this lesson the entire js prototype is understood, and you guys actually realized how simple it is.