Prototypal inheritance
Auteur(s) de l'article
Prototypal is a complicated term for a very powerful concept. It comes from the word prototype, which means an original model on which something is patterned (Merriam Webster dictionnary).
In our case, it means constructing an original object, defining its characteristics and its functions, which will then serves as pattern for new objects.
In JavaScript, we can access the prototype of any object thanks to a hidden property
[[Prototype]]
. It refers to either an object or null
.How does it work ?
To access an object prototype, we can use
__proto__
which is the historical getter/setter
for [[Prototype]]
. Let's make a concrete example:let userPrototype = {
isAdmin: true,
};
let user = {
username: 'Jane Smith',
};
user.__proto__ = userPrototype;
console.log(user);
/* the [[Prototype]] hidden property is set to userPrototype
{
username: 'Jane Smith',
[[Prototype]]: Object {
isAdmin: true,
}
}
*/
console.log(user.isAdmin); // true
console.log(user.username); // Jane Smith
We can now access the property
isAdmin
directly from the user
object, because we have set its prototype to userPrototype
.Let's say now that our
user
does not have admin right. I can modify it directly:user.isAdmin = false;
console.log(user.isAdmin); // false
console.log(userPrototype.isAdmin); // true
This modifies only the
isAdmin
property for the user
object, but the prototype userPrototype
remains unchanged, with isAdmin
set to true
.Inheritance
Prototypal inheritance means we can access any properties or methods from another object. Each object inherits from a prototype, which itself inherits from another prototype and so on, until
[[Prototype]]
equals null
. This is called prototype chaining.This means when trying to access an object properties, the code search will go through each chained elements until it finds either the property or
null
is reached.Everything is prototypal
All JavaScript objects inherit properties and methods from a prototype:
Date
objects inherit fromDate.prototype
.Array
objects inherit fromArray.prototype
.Player
objects inherit fromPlayer.prototype
.
This is how we have access to a bunch of methods depending on the type of object:
toUppercase()
, split()
, format()
, etc.How powerful it can be
Why is prototyping so powerful ? Let's imagine we have the following code:
let userPrototype = {
username: '',
greetings: function() {
console.log(`Hello ${this.username}`);
},
};
let user = {
username: 'Jane Smith',
};
user.__proto__ = userPrototype;
console.log(user.greetings()); // Hello Jane Smith
Now let's say that my greeting function is no longer what I want. Instead I would d like to ask how the user is doing. By modifying the method in the prototype, it will automatically applies to the children who inherits from it:
userPrototype.greetings = function() {
console.log(`Hello ${this.username}, how are you doing ?`);
};
console.log(user.greetings()); // Hello Jane Smith, how are you doing ?
Constructor
Just like for object-oriented programming, all JavaScript objects have a constructor, even when it is not explicitly created. The following examples demonstrates this point:
let object1 = {};
let object2 = new Object;
console.log(object1.constructor); // Object
console.log(object2.constructor); // Object
let date = new Date;
console.log(date.constructor); // Date
let array1 = [];
let array2 = new Array;
console.log(array1.constructor); // Array
console.log(array2.constructor); // Array
We saw that using prototypal inheritance can prevent code duplication, since we can inherit properties and methods from a prototype. It also makes sure that if a prototypal method is modified, it will be the case as well for all the children that inherits from it.
But so far, the way to set the prototype of an object (
user.__proto__ = userPrototype
) is not great if we have to create a bunch of new users each inheriting from userPrototype
.By using the constructor function, we can improve this: it will automatically set the
[[Prototype]]
property for us.function UserPrototype(username) {
this.username = username;
};
UserPrototype.prototype.greetings = function () {
console.log(`Hello ${this.username}`);
};
const user = new UserPrototype('Jane Smith');
console.log(user.greetings()); // Hello Jane Smith
console.log(user.constructor); // UserPrototype
From this point, we can then use
Class
which is just a sugar coat to prototyping in JavaScript:class UserPrototype {
constructor (username) {
this.username = username
};
greetings = () => console.log(`Hello ${this.username}`);
}
To wrap up
Advantages
JavaScript, at its core, is prototypal. It means we can define the properties and methods of a prototype and then create objects which will inherit from it. This makes it very similar to Object-Oriented programming and has big advantages:
1. It prevents duplication of logic and data: objects can share properties and methods thanks to inheritance.
2. It uses less memory. Let's create the following code
function User() {
this.greetings = function() {};
}
const John = new User();
const Jane = new User();
(John.greetings === Jane.greetings); // false
1 function = 1 portion of memory space
Here, both greetings()
functions are different, because they have been created at the instantiation. It means we duplicate it every time we create a new User, which takes some memory space.
The more users we create, the more memory it takes, and at some point, it can become a problem. To prevent this, we can use Prototyping, where the function will only be created once, at the prototype level :
function User() {};
User.prototype.greetings = function() {};
const John = new User();
const Jane = new User();
(John.greetings === Jane.greetings); // true
3. Prototype chaining: in JavaScript, each object are descendants or instances of
Object.prototype
, which is an object that sets properties and methods to all other JavaScript data types. By chaining different objects, we get more properties and methods. But when trying to go further than Object.prototype
, you'll get null
.Limits
Despite how powerful prototypal inheritance is, there are still some limits:
- No loop possible. An error would be raised if we tried the following scenario: if B inherits from A, and C inherits from B, then A cannot inherit from C.
- Object can only inherit from one prototype. It can be chained, but C cannot inherit directly from both A and B.
- Prototypal relationships are only between objects (or
null
)
- Prototypal chaining can be quite costly: the code search will go up the chain of all prototypes to look for a property until it finds it or reaches
null
. Depending on how deep the chaining goes, this can be quite expensive for a computer resources.