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 from Date.prototype.
  • Array objects inherit from Array.prototype.
  • Player objects inherit from Player.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.

            Sources