Chaîne de prototypes JavaScript : Guide court et simple, par Mensur Durakovic

Avant de commencer, permettez-moi de vous mettre en garde. Cet article s'adresse à ceux qui veulent comprendre comment fonctionne la chaîne de prototypes en JavaScript.

Cela va être difficile pour certains d'entre vous et il se peut que cela ne soit pas très clair à la première lecture. Il se peut que vous deviez relire cet article plusieurs fois pour le comprendre.

Mais cela en vaut la peine. La question sur la chaîne de prototypes est assez souvent posée dans les entretiens techniques.

Vous savez probablement déjà que les tableaux et les fonctions sont des objets en JavaScript. JavaScript utilise ce que l'on appelle l'héritage prototypique. Vous voyez ces flèches ici :

Nom : 1.jpg
Affichages : 16009
Taille : 26,6 Ko

Il s'agit de l'héritage prototypique. Qu'est-ce que cela signifie ?

L'héritage est essentiellement un objet qui a accès aux propriétés et aux méthodes d'un autre objet.

Ainsi, tout est un objet en JavaScript. Le tableau a accès aux propriétés et aux méthodes de l'objet. Il en va de même pour les fonctions. Grâce à cette chaîne que nous appelons la chaîne de prototypes, les fonctions ont accès aux méthodes et aux propriétés des objets.

Voyons maintenant ce qu'il en est :

Nom : 2.jpg
Affichages : 182
Taille : 75,9 Ko

Tout d'abord, nous avons un tableau vide appelé "food".

Ensuite, nous accédons à cette propriété étrange appelée "__proto__". Elle s'écrit avec 2 underscores avant et après le mot proto. Nous pouvons voir dans la console que cette propriété bizarre est un tableau de fonctions :

  • constructor,
  • at,
  • concat,
  • copyWithin,
  • fill,
  • push, etc.


Ce sont toutes des méthodes que les données de type tableau obtiennent par défaut.

Ce que nous avons fait ici, c'est de remonter dans la chaîne des prototypes. Et nous sommes entrés dans le prototype Array (remarquez l'image et le A majuscule). Puisque Array est le "parent" de notre tableau "food", nous héritons de toutes ces méthodes.

Maintenant, à la ligne suivante, nous avons cette syntaxe bizarre "__proto__" 2 fois. Vous l'avez peut-être deviné, avec cela, nous montons d'un niveau dans la chaîne des prototypes. Nous accédons donc au prototype du grand-parent "food". Et c'est Object.

C'est l'objet à partir duquel tout est créé en JavaScript. Y compris les fonctions et les tableaux.

Par exemple, nous avons la méthode toString ici. Cela signifie que tout ce qui est un descendant d'Object obtiendra la méthode toString. Cela signifie donc qu'un tableau "food" dispose de la méthode toString.

Vous voyez que j'obtiens le même résultat pour un type de fonction :

Nom : 3.jpg
Affichages : 183
Taille : 69,4 Ko

Voilà ce qu'est l'héritage de prototype.

En fait, l'héritage de prototype est assez unique. Il n'est pas très courant dans d'autres langages populaires comme C# ou Java. Ces derniers utilisent ce que l'on appelle l'héritage classique, tandis que JavaScript utilise l'héritage prototype.

Même si le mot-clé class existe en JavaScript, il n'y a en fait pas de class dans ce langage. Nous n'avons que l'héritage prototype.

Ainsi, lorsque vous écrivez un mot-clé class, vous utilisez une fonction en arrière-plan. C'est ce que nous appelons le sucre syntaxique.

Vous pouvez vous demander où se trouve la fin de la chaîne des prototypes.

Eh bien, c'est facile à trouver :

Nom : 4.jpg
Affichages : 179
Taille : 10,2 Ko

Ici, nous avons notre objet "food" et nous passons à deux niveaux supérieurs. Le premier niveau est donc notre type parent Object, mais si vous allez plus haut, vous obtenez null.

En JavaScript, undefined est souvent utilisé pour indiquer qu'une variable ou une propriété d'objet n'a pas été initialisée ou qu'aucune valeur ne lui a été attribuée. Ou lorsqu'une fonction ne renvoie aucune valeur.

Null signifie qu'il n'y a absolument rien. Il représente l'absence intentionnelle de toute valeur d'objet.

Vous avez probablement entendu parler des listes chaînées, n'est-ce pas ? Vous pouvez représenter la chaîne de prototypes dans votre tête sous la forme d'une simple liste chaînée :

Nom : 5.jpg
Affichages : 178
Taille : 18,2 Ko

Tous les nœuds sont reliés par les liens de leur chaîne de prototypes. Le null est le dernier nœud de la liste chaînée de la chaîne de prototypes.


La chaîne de prototypes dans le code

Jetons un coup d'œil à cet extrait de code :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const human = {
    name: "Joe",
    canTalk: true,
    getIntelligence(){
        return 10;
    },
    talk(){
        if(this.canTalk){
            console.log(`Hi, my name is ${this.name}!`);
        }
    }
}
 
const chimpanzee = {
    name: "Mowgli",
    getIntelligence(){
        return 5;
    }
}
Rien de spécial ici, nous avons 2 objets simples :

  • human Joe. Il peut parler et son intelligence est de 10 points. Lorsqu'il parle, il dit : "Bonjour, je m'appelle Joe !".
  • chimpanzee Mowgli. Il ne peut pas parler et son intelligence n'est que de 5 points.


Imaginons que je veuille faire parler Mogwli. Comment faire ?

Eh bien, je peux utiliser une astuce avec la méthode .bind et "emprunter" la fonction de conversation à un human. Essayons ceci :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
const talkingChimpanzee = human.talk.bind(chimpanzee);
 
console.log(talkingChimpanzee()); 
// prints undefined
Malheureusement, la propriété canTalk du chimpanzé n'est pas réglée sur true. Ainsi, même si nous empruntons la méthode, nous n'avons pas la capacité de parler.

Que pouvons-nous donc faire ici ?

Nous pourrions ajouter la propriété, mais vous pouvez voir comment cela pourrait devenir de plus en plus compliqué, n'est-ce pas ? Que se passerait-il si nous avions un gros objet et que nous voulions emprunter plus d'une propriété/méthode ?

C'est là que l'héritage de prototype entre en jeu.

Nous pouvons créer une chaîne de prototypes qui héritera de toutes ces propriétés et méthodes d'un humain.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
chimpanzee.__proto__ = human;
 
chimpanzee.talk();                         // prints "Hi, my name is Mowgli!"
console.log(chimpanzee.canTalk);           // prints true
console.log(chimpanzee.getIntelligence()); // prints 5
Nous pouvons voir que ce qui précède correspond exactement à ce dont nous avions besoin.

L'intelligence du chimpanzé est toujours de 5 car il est défini comme un objet chimpanzé. Nous sommes donc en mesure d'hériter, par le biais de cette chaîne de prototypes, de toutes les propriétés et méthodes d'un humain.

Ensuite, nous remplaçons tout ce que nous avons déjà déclaré dans notre propre objet. Dans ce cas, il s'agit de la propriété name et de la méthode talk.


Méthode hasOwnProperty

Laissez-moi vous montrer une autre chose intéressante :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
console.log("Has name property: ", chimpanzee.hasOwnProperty('name'));
console.log("Has canTalk property: ", chimpanzee.hasOwnProperty('canTalk'));
 
// Has name property: true
// Has canTalk property: false
Tout d'abord, comment cela fonctionne-t-il ?

Nous savons qu'un chimpanzé n'a pas de méthode appelée hasOwnProperty. Un humain n'a pas non plus cette fonction.

Eh bien, un humain a également un "parent" dont il hérite de toutes les méthodes et propriétés. Ce parent est Object et je l'ai mentionné au début. Object est le parent de tous les parents, le parent final.

Ainsi, en arrière-plan, JavaScript essaie d'abord de trouver la fonction hasOwnProperty chez un chimpanzé. Mais il ne la trouve pas.

Il remonte alors dans la chaîne de prototypes et commence à chercher dans un humain. Il ne la trouve pas.

Il remonte alors d'un niveau dans la chaîne des prototypes, dans Object, et c'est là que se trouve la méthode :

Nom : 6.jpg
Affichages : 175
Taille : 48,0 Ko

Nous savons maintenant pourquoi le chimpanzé possède cette méthode hasOwnProperty.

Mais que fait la méthode hasOwnProperty ?

Elle renvoie un booléen indiquant si cet objet possède la propriété spécifiée comme sa propre propriété. Il ne s'agit donc pas d'une propriété héritée, mais d'une propriété propre.

C'est pourquoi, dans le dernier extrait de code, la méthode hasOwnProperty affiche true pour name et false pour canTalk.

Le chimpanzé a sa propre propriété name définie dans l'objet. La propriété canTalk ne lui appartient donc pas. Elle est héritée d'un humain et c'est pourquoi elle affiche false.


Attention, attention, attention !

Vous vous demandez peut-être comment il se fait que je n'aie jamais vu cette chose "__proto__" avant ?

Cela semble très utile. Pourquoi ne le voit-on pas dans les projets ?

Ce que je vous ai montré, vous ne devriez le faire dans aucun projet. Et je veux dire ceci :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
chimpanzee.__proto__ = human;
Vous ne devriez pas l'utiliser. En fait, vous ne devriez JAMAIS l'utiliser !

C'est mauvais pour les performances. Il existe différentes façons d'utiliser le prototype ou l'héritage.

Ainsi, nous ne voulons jamais assigner la chaîne de prototypes et créer cette chaîne nous-mêmes. Cela va perturber sérieusement notre compilateur JavaScript.

Mais je voulais vous montrer comment cela fonctionne.


Que peut-on utiliser à la place de "__proto__" ?

Voyons comment nous pouvons créer nos propres prototypes et quelle est la manière la plus sûre de le faire.

Vérifions ce code :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const human = {
  canTalk: true
}
 
const chimpanzee = Object.create(human);
chimpanzee.jumpOnTree = true;
 
console.log(chimpanzee);
// prints {jumpOnTree: true}
 
console.log(chimpanzee.canTalk);
// prints true
 
console.log(human.isPrototypeOf(chimpanzee));
// prints true
Nous avons donc ici un humain qui peut parler. Puis nous avons un chimpanzé qui hérite de l'humain. Nous faisons cela avec la méthode Object.create.

Il existe de nombreuses façons de procéder, et c'est l'une d'entre elles qui nous permet d'hériter d'un humain.

Si nous imprimons un objet chimpanzé, tout ce que nous pouvons voir est la propriété jumpOnTree. Mais si nous accédons explicitement à cette propriété héritée d'un humain. Nous constatons qu'elle existe et qu'elle vaut true.

Enfin, nous vérifions si un humain est le prototype d'un chimpanzé avec la méthode isPrototypeOf. Et nous obtenons true parce que nous avons créé, en utilisant Object.create, une chaîne de prototypes jusqu'à un humain.

Maintenant, vous savez comment faire cela sans utiliser cette chose diabolique qu'est le "__proto__".

En fait, ils l'ont nommée ainsi (avec des doubles soulignés) pour que personne ne s'amuse avec la chaîne de prototypes.


Pourquoi la chaîne de prototypes est-elle utile ?

Le fait que les objets puissent partager des prototypes signifie que vous pouvez avoir des objets dont les propriétés pointent vers le même endroit de la mémoire. Ils sont donc plus efficaces.

Imaginez que nous ayons un grand nombre de chimpanzés, n'est-ce pas ? Et que nous copiions toutes les fonctionnalités de l'homme sur le chimpanzé à différents endroits de la mémoire.

Cela pourrait vite devenir trop lourd et faire exploser votre code.

Au lieu de copier toutes ces fonctionnalités à différents endroits de la mémoire, avec le prototype d'héritage, nous les avons en un seul endroit.

Tout ce qui hérite d'un humain utilisera cette instance de cette méthode. Car le moteur JavaScript va chercher la chaîne de prototypes.

Source : "JavaScript Prototype Chain: Short And Simple Guide" (Mensur Durakovic)

Et vous ?

Quel est votre avis sur le sujet ?

Voir aussi :

Une proposition visant à ajouter des signaux à JavaScript afin de fournir une infrastructure, permettant aux développeurs de se concentrer sur la logique plutôt que sur les détails répétitifs

État de JavaScript 2022 : React reste le framework front-end dominant mais est en perte de vitesse côté satisfaction. JQuery est la troisième bibliothèque la plus utilisée