Les prototypes en javascript

En faisant du javascript, on a tous observé ces étranges propriétés « prototype » et « __proto__ » dans la console, comme ci-contre, pour un objet.

Il s’agit en fait d’éléments prédéfinis alloués à l’élément que vous affichez (qu’il s’agisse d’un Object, d’un Array …). C’est le « prototype« .

Qu’est-ce que le prototype ?

Vos éléments ont de nombreuses fonctions (méthodes) rattachées à eux automatiquement, grâce au navigateur. Chaque ensemble est appelé le prototype. Vous avez un prototype d’array, un prototype d’objet, etc. Chacun de ces prototypes a ses propres méthodes et propriétés:

Vous voyez par exemple que l’array a une propriété « length » que n’a pas l’objet. Il va aussi avoir des méthodes que nous avons déjà vues, comme map(), filter () ou reduce(). Mieux .. il s’agit aussi d’un Object ! Donc il hérite aussi des méthodes du prototype Object. Cela vaut aussi pour les types de variable: les objets « String » n’ont pas le même prototype qu’un objet « Number« :

Ces éléments prédéfinis ne sont évidemment pas exclusifs. Vous pouvez également créer des prototypes. Pour résumer, on peut les définir comme suit:

Un prototype en JavaScript est un objet qui fournit des propriétés et des méthodes héritées par d’autres objets. Il permet la réutilisation de code et l’implémentation de l’héritage orienté objet.

Comment créer un prototype ?

Vous pouvez créer des prototypes. Par exemple, supposons que vous développez un petit jeu, où vous animez des humains étant capables de marcher.

Vous pouvez créer un objet pour chaque personne, qui aura une méthode « marcher », mais vous vous répèterez beaucoup:

const humain1 = {
  nom: 'Alice',
  marcher: function() {
    console.log('Marche');
  }
};

const humain2 = {
  nom: 'Bob',
  marcher: function() {
    console.log('Marche');
  }
};

Il vaut mieux créer un prototype « Humain », qui aura une méthode « marcher() » :

// Définition du prototype Humain
function Humain(nom) {
  this.nom = nom;
}

Humain.prototype.marcher = function() {
  console.log(`Marche`);
};

const alice = new Humain('Alice');
const bob = new Humain('Bob'); 

Modifier le prototype

Vous pouvez influer sur l’ensemble des instances d’un élément en modifiant son prototype.

Vous pouvez lui ajouter de nouvelles méthodes (même processus que dans l’exemple précédent) ou modifier les méthodes existantes. Par exemple, l’objet String a une méthode « toUpperCase », qui transforme toutes les lettres en majuscules. Dans l’exemple suivant, inspiré de Melvynx, nous la transformons en une autre fonction qui remplace les « J » par des « B ».

let jacques = {};
jacques.nom = "Jacques";
console.log(jacques.nom.toUpperCase()); //  JACQUES
String.prototype.toUpperCase = function () {
  return this.replace("J", "B");
};
console.log(jacques.nom.toUpperCase()); // Bacques

Notez que cette façon de faire écrase entièrement la précédente méthode.

__Proto__ ou prototype ?

Dans mes exemples plus haut, pour afficher les prototypes, j’utilisais ce code :

let jacques = {};
jacques.nom = "Jacques";
jacques.age = 42;
console.log(jacques);
console.log(jacques.nom);
console.log(jacques.nom.__proto__);
console.log(jacques.age.__proto__);

En effet, si je change __proto__ par prototype, j’obtiens « undefined ». Qu’est-ce que cela signifie ? Les deux objets sont en fait différents:

  • __proto__ est une propriété d’une instance d’objet qui pointe vers son prototype.
  • prototype est une propriété de la fonction constructor qui va être héritée par les instances.

Il s’agit en fait de la même chose à laquelle on accède sous deux angles différents. (source) Ainsi, la propriété prototype n’existe pas sur les instances d’objets, mais sur la fonction qui a créé ces objets. Lorsqu’on tente de l’afficher plus tôt, il est donc logique que la valeur renvoyée soit « undefined ».

Il est fortement déconseillé de modifier directement __proto__ (cela cause des problèmes de performance (source, en)).

Les règles d’héritage

Nous avons vu que les instances de l’élément ont accès à son prototype. On parle d’héritage. Par exemple, tous les « Objects » ont accès au prototype « Objects ». Un prototype peut aussi en contenir un autre. Ainsi, les éléments « Array » sont une sorte d’éléments « Objects », et ont donc accès aux prototypes des deux types d’éléments.

Prenons l’exemple suivant: nous avons deux types d’objets : Animal et Dog. Animal est le prototype parent et possède une méthode makeSound(). Pour créer un prototype Dog qui étend Animal, nous utilisons la fonction Object.create(Animal.prototype) pour établir Animal.prototype comme le prototype de Dog.prototype. Ensuite, nous réaffectons le constructeur de Dog.prototype à Dog pour maintenir la bonne relation entre le constructeur et le prototype. Cela permet aux instances de Dog d’avoir accès aux méthodes du prototype Animal, formant ainsi une chaîne de prototypes. De plus, nous pouvons ajouter des méthodes spécifiques à Dog comme bark().

// Prototype parent
function Animal(name) {
  this.name = name;
}

Animal.prototype.makeSound = function() {
  console.log('Generic animal sound');
};

// Prototype enfant
function Dog(name) {
  Animal.call(this, name);
}

// Étendre le prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Ajouter des méthodes supplémentaires au prototype enfant
Dog.prototype.bark = function() {
  console.log('Woof');
};

// Créer une instance
const myDog = new Dog('Rex');
myDog.makeSound();  // Output: 'Generic animal sound'
myDog.bark();       // Output: 'Woof'

Notez que la syntaxe est assez lourde. C’est plus pratique avec les classes.

Quelle différence entre prototype et classe en Javascript?

Les classes ont été créées par l’ES6 pour rapprocher le développement Javascript des langages de POO (Programmation Orientée Objets). Elles sont plus faciles à utiliser. Ainsi, dans notre exemple, pour créer la classe « Dog », on peut simplement faire:

class Dog extends Animal {
  bark() {
    console.log('Woof');
  }
}

Au niveau du moteur de javascript, les classes sont converties en prototype. On dit que les classes sont du « sucre syntaxique », car elles aident simplement à écrire et lire le code pour les humains.


Pour aller plus loin: