One of the great things that ES6 brought us are classes. We need to remember though, that all the work is done by prototypes under the hood. It means that ES6 classes act just as syntactic sugar. In this article, I will walk you through the basics of the prototypes, so that you can better grasp the concept.
Understanding prototypes
prototype
object that provides shared properties for other objects
JavaScript happens to have an inheritance model quite different from most Object Oriented Programming languages. Objects do not have a class or type that they get their properties from: they use prototypes for it. In other languages, a class can inherit from another class (and that class can inherit from another class), in JavaScript, there is a prototype chain.
Objects here act as wrappers for properties, which are called own properties, meaning that the object directly contains them. When trying to access a property it does not own, the prototype chain is traversed. Interpreter looks for the property from the closest prototype to the furthest, until either the property is found, or the prototype is null, which means the end of the chain.
Almost all objects in JavaScript are instances of Object. There is an easy way to observe it:
1 2 3 4 |
const dog = { name: 'Fluffy' } console.log(dog.valueOf()); // { name: 'Fluffy' } |
The function was called, even though we didn’t assign a function named valueOf to our dog. What happened is that the function was looked for through the prototype chain and the Object happens to have this function.
1 2 3 4 5 6 7 |
const dog = { name: 'Fluffy', valueOf: function() { return { ...this, thisIsNotAPrototype: true }; } } console.log(dog.valueOf()); // { name: 'Fluffy', thisIsNotAPrototype: true, valueOf: f () } |
This time, since the dog itself, had a valueOf() function, there was no reason to look through the prototype chain. The same logic applies to all properties (not only functions).
In case you’re wondering what does ...this mean, check out the spread syntax.
Constructors
constructor
function object that creates and initializes objects
It might seem confusing at first but in JavaScript, all functions are also objects. One of their properties is called a prototype and refers to an object. It will serve as a prototype for a newly created object when a function is called as a constructor with a new operator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const Animal = function(type) { this.type = type; } const Dog = function(name) { Animal.call(this, 'dog'); this.name = name; } Object.setPrototypeOf(Dog.prototype, Animal.prototype); const fluffy = new Dog('Fluffy'); console.log(fluffy.name); // 'Fluffy' console.log(fluffy.type); // 'dog' console.log(Object.getPrototypeOf(fluffy) === Dog.prototype); // true console.log(Animal.prototype.isPrototypeOf(fluffy)) // true |
The Object.setPrototypeOf() method sets the prototype (i.e., the internal [[Prototype]] property) of a specified object to another object or null.
In the following example, a few things happen:
- Animal and Dog constructors are created. Dog “inherits” from Animal
- A new object is created, inheriting from Dog.prototype
- The Dog constructor is called with this referring to the new object
- The Animal constructor is called, with this referring to the new object
- Because the constructor does not return anything (it can, though), the return value of the new Dog('Fluffy) is the newly created object
In general, you should avoid changing the prototype of an already created object. If you’d like to read more on that subject, I recommend the MDN docs.
There are a few ways in which you can set the properties that a newly created object will inherit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const Dog = function(name) { this.name = name; this.getName = function() { return this.name; } } const fluffy = new Dog('Fluffy'); Object.prototype.isItADog = function() { return this instanceof Dog; } console.log(fluffy.getName()); // 'Fluffy' console.log(fluffy.isItADog()); // true |
As you can see, it can be done both by adding properties to this in the constructor and modifying the prototype itself. Once again, the prototype chain was traversed – fluffy didn’t have the isItADog function, nor the Dog.prototype. Finally, it was found in the Object.prototype. It is worth mentioning that this in the function refers to the fluffy.
The instanceof operator tests whether the prototype property of a constructor appears anywhere in the prototype chain of an object.
Also, notice that if you add more properties to the prototype, they will be accessible to the objects that are an instance of it but were declared before adding the new property.
Accessing prototypes
You can always get the prototype of an object through the Object.getPrototypeOf function. There is also a __proto__ property, but it was not standardized until ES6 (even if it was implemented by most browsers).
1 |
console.log(dog.__proto__ === Object.prototypeOf(dog)) // true |
It is not a property of our dog though: it is a property of Object.prototype. It acts as a getter. Since it is the last prototype in the prototype chain of a dog, it will be called (if not overshadowed). This piece of code illustrates how it works:
1 2 3 4 5 6 7 8 |
Object.defineProperty(Object.prototype, "__proto__", { get: function() { console.log('prototype accessed'); return Object.getPrototypeOf(this); } }); console.log(fluffy.__proto__) // Dog // 'prototype accessed' |
ES6 class
As I already mentioned, ES6 classes act just as syntactic sugar for prototypes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Animal { constructor(type){ this.type = type; } } class Dog extends Animal { constructor(name){ super('dog'); this.name = name; } getName() { return this.name; } } const fluffy = new Dog('Fluffy'); console.log(fluffy.getName()) // 'Fluffy' console.log(typeof Dog); // 'function' console.log(Object.getPrototypeOf(fluffy) === Dog.prototype); // true console.log(Animal.prototype.isPrototypeOf(fluffy)); // true |
As you can see, these are just prototypes in the end. It looks much cleaner. What issues does it solve, aside from that? Quite a few, actually!
- We have a super function inside of a constructor – it calls a constructor of a class that is being extended.
- There is no need to call Object.setPrototypeOf to extend another prototype
- You can extend any class easily and in a very natural way – even those of built-in objects (like the Array)
There are some drawbacks though. One of them is that using class syntax implicates that there are actually some classes involved, but there aren’t any. It can make the process seem even more complicated.
Summary
Understanding prototypes is one of the crucial tasks for an aspiring JavaScript developer. Currently, with the ES6 classes syntax popularized, it might seem even harder. It doesn’t make it less important though.
Hi, sir!
I copied your code to run in Jsbin, but it didn’t return true on the last line!
Thanks for pointing that out! I fixed it 🙂