En programmation, nous voulons souvent prendre quelque chose et l’étendre.
Par exemple, nous avons un objet user
avec ses propriétés et ses méthodes, et nous souhaitons en faire des variantes légèrement modifiées de admin
et guest
. Nous aimerions réutiliser ce que nous avons dans user
, ne pas copier/réimplémenter ses méthodes, mais simplement créer un nouvel objet par-dessus.
L'héritage prototypique est une fonctionnalité du langage qui y contribue.
En JavaScript, les objets ont une propriété cachée spéciale [[Prototype]]
(telle que nommée dans la spécification), qui est null
ou fait référence à un autre objet. Cet objet est appelé « un prototype » :
Lorsque nous lisons une propriété d' object
et qu'elle est manquante, JavaScript la extrait automatiquement du prototype. En programmation, cela s’appelle « l’héritage prototypique ». Et bientôt, nous étudierons de nombreux exemples d'un tel héritage, ainsi que des fonctionnalités de langage plus intéressantes qui en découlent.
La propriété [[Prototype]]
est interne et cachée, mais il existe de nombreuses façons de la définir.
L'une d'elles consiste à utiliser le nom spécial __proto__
, comme ceci :
soit animal = { mange : vrai } ; laissez lapin = { sauts : vrai } ; lapin.__proto__ = animal ; // définit le lapin.[[Prototype]] = animal
Maintenant, si nous lisons une propriété de rabbit
et qu'elle est manquante, JavaScript la prendra automatiquement de animal
.
Par exemple:
soit animal = { mange : vrai } ; laissez lapin = { sauts : vrai } ; lapin.__proto__ = animal ; // (*) // nous pouvons maintenant trouver les deux propriétés dans Rabbit : alerte( lapin.eats ); // vrai (**) alert( lapin.jumps ); // vrai
Ici, la ligne (*)
définit animal
comme le prototype de rabbit
.
Ensuite, lorsque alert
essaie de lire la propriété rabbit.eats
(**)
, elle n'est pas dans rabbit
, donc JavaScript suit la référence [[Prototype]]
et la trouve dans animal
(regardez de bas en haut) :
Ici, nous pouvons dire que « animal
est le prototype du rabbit
» ou « rabbit
hérite de manière prototypique de animal
».
Ainsi, si animal
possède de nombreuses propriétés et méthodes utiles, elles deviennent automatiquement disponibles chez rabbit
. De telles propriétés sont dites « héritées ».
Si on a une méthode chez animal
, on peut l'appeler sur rabbit
:
soit animal = { mange : vrai, marcher() { alert("Promenade d'animaux"); } } ; laissez lapin = { sauts : vrai, __proto__ : animal } ; // la marche est tirée du prototype lapin.walk(); // Promenade animalière
La méthode est automatiquement reprise du prototype, comme ceci :
La chaîne de prototypes peut être plus longue :
soit animal = { mange : vrai, marcher() { alert("Promenade d'animaux"); } } ; laissez lapin = { sauts : vrai, __proto__ : animal } ; soit longEar = { longueur d'oreille: 10, __proto__ : lapin } ; // la marche est extraite de la chaîne de prototypes longEar.walk(); // Promenade animalière alerte (longEar.jumps); // vrai (de lapin)
Maintenant, si nous lisons quelque chose dans longEar
et qu'il manque, JavaScript le recherchera dans rabbit
, puis dans animal
.
Il n'y a que deux limites :
Les références ne peuvent pas tourner en rond. JavaScript générera une erreur si nous essayons d'attribuer __proto__
dans un cercle.
La valeur de __proto__
peut être soit un objet, soit null
. Les autres types sont ignorés.
Cela peut aussi paraître évident, mais quand même : il ne peut y avoir qu'un seul [[Prototype]]
. Un objet ne peut pas hériter de deux autres.
__proto__
est un getter/setter historique pour [[Prototype]]
C'est une erreur courante des développeurs débutants de ne pas connaître la différence entre ces deux éléments.
Veuillez noter que __proto__
n'est pas la même chose que la propriété interne [[Prototype]]
. C'est un getter/setter pour [[Prototype]]
. Plus tard, nous verrons des situations où cela est important, pour l'instant gardons cela à l'esprit, alors que nous développons notre compréhension du langage JavaScript.
La propriété __proto__
est un peu obsolète. Il existe pour des raisons historiques, le JavaScript moderne suggère que nous devrions plutôt utiliser les fonctions Object.getPrototypeOf/Object.setPrototypeOf
pour obtenir/définir le prototype. Nous aborderons également ces fonctions plus tard.
Selon la spécification, __proto__
ne doit être pris en charge que par les navigateurs. En fait, tous les environnements, y compris côté serveur, prennent en charge __proto__
, nous pouvons donc l'utiliser en toute sécurité.
Comme la notation __proto__
est un peu plus intuitive, nous l'utilisons dans les exemples.
Le prototype n'est utilisé que pour lire les propriétés.
Les opérations d'écriture/suppression fonctionnent directement avec l'objet.
Dans l'exemple ci-dessous, nous attribuons sa propre méthode walk
à rabbit
:
soit animal = { mange : vrai, marcher() { /* cette méthode ne sera pas utilisée par lapin */ } } ; laissez lapin = { __proto__ : animal } ; lapin.walk = fonction() { alert("Lapin ! Rebond-rebond !"); } ; lapin.walk(); // Lapin! Rebondissez-rebondissez !
Désormais, l'appel rabbit.walk()
trouve la méthode immédiatement dans l'objet et l'exécute, sans utiliser le prototype :
Les propriétés des accesseurs sont une exception, car l'affectation est gérée par une fonction setter. Donc écrire dans une telle propriété revient en fait à appeler une fonction.
Pour cette raison, admin.fullName
fonctionne correctement dans le code ci-dessous :
laissez l'utilisateur = { nom : "Jean", nom de famille : « Smith », définir fullName (valeur) { [this.name, this.surname] = value.split(" "); }, obtenir fullName() { return `${this.name} ${this.surname}` ; } } ; laissez admin = { __proto__ : utilisateur, isAdmin : vrai } ; alerte(admin.fullName); // John Smith (*) // déclencheurs de setter ! admin.fullName = "Alice Cooper"; // (**) alerte(admin.fullName); // Alice Cooper, état d'admin modifié alerte (utilisateur.fullName); // John Smith, état d'utilisateur protégé
Ici, dans la ligne (*)
la propriété admin.fullName
a un getter dans le prototype user
, c'est pourquoi elle est appelée. Et dans la ligne (**)
la propriété a un setter dans le prototype, c'est ainsi qu'elle s'appelle.
Une question intéressante peut se poser dans l'exemple ci-dessus : quelle est la valeur de this
set fullName(value)
? Où sont écrites les propriétés this.name
et this.surname
: dans user
ou admin
?
La réponse est simple : this
n’est pas du tout affecté par les prototypes.
Peu importe où se trouve la méthode : dans un objet ou son prototype. Dans un appel de méthode, this
toujours l'objet avant le point.
Ainsi, l'appel setter admin.fullName=
utilise admin
comme this
, pas user
.
C'est en fait une chose extrêmement importante, car nous pouvons avoir un gros objet avec de nombreuses méthodes et des objets qui en héritent. Et lorsque les objets héritiers exécutent les méthodes héritées, ils modifient uniquement leurs propres états, pas l'état du gros objet.
Par exemple, ici animal
représente une « méthode de stockage », et rabbit
l’utilise.
L'appel rabbit.sleep()
définit this.isSleeping
sur l'objet rabbit
:
// l'animal a des méthodes soit animal = { marcher() { si (!this.isSleeping) { alert(`Je marche`); } }, dormir() { this.isSleeping = vrai ; } } ; laissez lapin = { nom: "Lapin Blanc", __proto__ : animal } ; // modifie lapin.isSleeping lapin.sleep(); alerte(rabbit.isSleeping); // vrai alerte(animal.isSleeping); // non défini (aucune propriété de ce type dans le prototype)
L'image résultante :
Si nous avions d'autres objets, comme bird
, snake
, etc., héritant de animal
, ils auraient également accès aux méthodes de animal
. Mais this
dans chaque appel de méthode serait l'objet correspondant, évalué au moment de l'appel (avant le point), et non animal
. Ainsi, lorsque nous écrivons des données dans this
, elles sont stockées dans ces objets.
En conséquence, les méthodes sont partagées, mais pas l’état de l’objet.
La boucle for..in
parcourt également les propriétés héritées.
Par exemple:
soit animal = { mange : vrai } ; laissez lapin = { sauts : vrai, __proto__ : animal } ; // Object.keys renvoie uniquement ses propres clés alert(Object.keys(lapin)); // saute // for..in boucle sur les clés propres et héritées for(let prop in Rabbit) alert(prop); // saute, puis mange
Si ce n'est pas ce que nous voulons et que nous souhaitons exclure les propriétés héritées, il existe une méthode intégrée obj.hasOwnProperty(key) : elle renvoie true
si obj
a sa propre propriété (non héritée) nommée key
.
Nous pouvons donc filtrer les propriétés héritées (ou faire autre chose avec elles) :
soit animal = { mange : vrai } ; laissez lapin = { sauts : vrai, __proto__ : animal } ; pour (laisser accessoire dans le lapin) { let isOwn = lapin.hasOwnProperty(prop); si (estpropre) { alert(`Notre : ${prop}`); // Nos : sauts } autre { alert(`Hérité : ${prop}`); // Hérité : mange } }
Nous avons ici la chaîne d'héritage suivante : rabbit
hérite de animal
, qui hérite de Object.prototype
(car animal
est un objet littéral {...}
, donc c'est par défaut), puis null
au-dessus :
Attention, il y a une chose amusante. D'où vient la méthode rabbit.hasOwnProperty
? Nous ne l'avons pas défini. En regardant la chaîne, nous pouvons voir que la méthode est fournie par Object.prototype.hasOwnProperty
. En d’autres termes, c’est hérité.
… Mais pourquoi hasOwnProperty
n'apparaît-il pas dans la boucle for..in
comme le font eats
et jumps
, si for..in
répertorie les propriétés héritées ?
La réponse est simple : ce n’est pas dénombrable. Tout comme toutes les autres propriétés de Object.prototype
, il a l'indicateur enumerable:false
. Et for..in
ne répertorie que les propriétés énumérables. C'est pourquoi celle-ci et le reste des propriétés Object.prototype
ne sont pas répertoriés.
Presque toutes les autres méthodes d'obtention de clé/valeur ignorent les propriétés héritées
Presque toutes les autres méthodes d'obtention de clé/valeur, telles que Object.keys
, Object.values
et ainsi de suite ignorent les propriétés héritées.
Ils n'agissent que sur l'objet lui-même. Les propriétés du prototype ne sont pas prises en compte.
En JavaScript, tous les objets ont une propriété [[Prototype]]
cachée qui est soit un autre objet, soit null
.
Nous pouvons utiliser obj.__proto__
pour y accéder (un getter/setter historique, il existe d'autres moyens, qui seront bientôt abordés).
L'objet référencé par [[Prototype]]
est appelé un « prototype ».
Si nous voulons lire une propriété d' obj
ou appeler une méthode et qu'elle n'existe pas, alors JavaScript essaie de la trouver dans le prototype.
Les opérations d'écriture/suppression agissent directement sur l'objet, elles n'utilisent pas le prototype (en supposant qu'il s'agisse d'une propriété de données, pas d'un setter).
Si nous appelons obj.method()
et que la method
est extraite du prototype, this
fait toujours référence obj
. Ainsi, les méthodes fonctionnent toujours avec l'objet actuel même si elles sont héritées.
La boucle for..in
parcourt à la fois ses propres propriétés et ses propriétés héritées. Toutes les autres méthodes d'obtention de clé/valeur ne fonctionnent que sur l'objet lui-même.
importance : 5
Voici le code qui crée une paire d'objets, puis les modifie.
Quelles valeurs sont affichées dans le processus ?
soit animal = { sauts : nul } ; laissez lapin = { __proto__ : animal, sauts : vrai } ; alert( lapin.jumps ); // ? (1) supprimer lapin.jumps ; alert( lapin.jumps ); // ? (2) supprimer animal.jumps ; alert( lapin.jumps ); // ? (3)
Il devrait y avoir 3 réponses.
true
, tiré du rabbit
.
null
, tiré de animal
.
undefined
, une telle propriété n'existe plus.
importance : 5
La tâche comporte deux parties.
Étant donné les objets suivants :
laissez la tête = { verres : 1 } ; soit table = { stylo : 3 } ; laisser se coucher = { feuille : 1, oreiller : 2 } ; laissez les poches = { argent : 2000 } ;
Utilisez __proto__
pour attribuer des prototypes de manière à ce que toute recherche de propriété suive le chemin : pockets
→ bed
→ table
→ head
. Par exemple, pockets.pen
devrait être 3
(trouvé dans table
) et bed.glasses
devrait être 1
(trouvé dans head
).
Répondez à la question : est-il plus rapide d'obtenir glasses
sous forme pockets.glasses
ou head.glasses
? Benchmark si nécessaire.
Ajoutons __proto__
:
laissez la tête = { verres : 1 } ; soit table = { stylo: 3, __proto__ : tête } ; laisser se coucher = { feuille: 1, oreiller : 2, __proto__ : tableau } ; laissez les poches = { argent : 2000, __proto__ : lit } ; alerte( poches.pen ); // 3 alerte( lit.lunettes ); // 1 alerte( table.argent ); // non défini
Dans les moteurs modernes, en termes de performances, il n'y a aucune différence entre la propriété d'un objet et celle de son prototype. Ils se souviennent de l'endroit où la propriété a été trouvée et la réutilisent lors de la prochaine demande.
Par exemple, pour pockets.glasses
ils se souviennent de l'endroit où ils ont trouvé glasses
(dans head
), et la prochaine fois, ils chercheront juste là. Ils sont également suffisamment intelligents pour mettre à jour les caches internes si quelque chose change, afin que l'optimisation soit sécurisée.
importance : 5
Nous avons rabbit
qui hérite d' animal
.
Si nous appelons rabbit.eat()
, quel objet reçoit la propriété full
: animal
ou rabbit
?
soit animal = { manger() { this.full = vrai ; } } ; laissez lapin = { __proto__ : animal } ; lapin.eat();
La réponse : rabbit
.
C'est parce this
s'agit d'un objet avant le point, donc rabbit.eat()
modifie rabbit
.
La recherche et l'exécution d'une propriété sont deux choses différentes.
La méthode rabbit.eat
est d'abord trouvée dans le prototype, puis exécutée avec this=rabbit
.
importance : 5
Nous avons deux hamsters : speedy
et lazy
héritant de l'objet général hamster
.
Quand on nourrit l’un d’eux, l’autre est également plein. Pourquoi? Comment pouvons-nous y remédier ?
laissez hamster = { estomac: [], manger(nourriture) { this.stomach.push(nourriture); } } ; soit rapide = { __proto__ : hamster } ; laissez paresseux = { __proto__ : hamster } ; // Celui-ci a trouvé la nourriture speedy.eat("pomme"); alerte( speedy.stomach ); // pomme // Celui-ci l'a aussi, pourquoi ? corrigez s'il vous plaît. alert( paresseux.estomac ); // pomme
Regardons attentivement ce qui se passe lors de l'appel speedy.eat("apple")
.
La méthode speedy.eat
se trouve dans le prototype ( =hamster
), puis exécutée avec this=speedy
(l'objet avant le point).
Ensuite, this.stomach.push()
doit trouver la propriété stomach
et appeler push
dessus. Il cherche de stomach
this
( =speedy
), mais rien n'est trouvé.
Ensuite, il suit la chaîne prototype et trouve stomach
d' hamster
.
Ensuite, il appelle push
dessus, ajoutant de la nourriture dans l'estomac du prototype .
Tous les hamsters partagent donc un seul estomac !
Tant pour lazy.stomach.push(...)
que speedy.stomach.push()
, la propriété stomach
se trouve dans le prototype (car elle ne se trouve pas dans l'objet lui-même), puis les nouvelles données y sont insérées.
Veuillez noter qu'une telle chose ne se produit pas dans le cas d'une simple affectation this.stomach=
:
laissez hamster = { estomac: [], manger(nourriture) { // attribuer à this.stomach au lieu de this.stomach.push this.estomac = [nourriture]; } } ; soit rapide = { __proto__ : hamster } ; laissez paresseux = { __proto__ : hamster } ; // Rapidement, on a trouvé la nourriture speedy.eat("pomme"); alerte( speedy.stomach ); // pomme // Le ventre du paresseux est vide alert( paresseux.estomac ); // <rien>
Maintenant, tout fonctionne bien, car this.stomach=
n'effectue pas de recherche de stomach
. La valeur est écrite directement dans this
objet.
Nous pouvons également éviter totalement le problème en veillant à ce que chaque hamster ait son propre estomac :
laissez hamster = { estomac: [], manger(nourriture) { this.stomach.push(nourriture); } } ; soit rapide = { __proto__ : hamster, estomac: [] } ; laissez paresseux = { __proto__ : hamster, estomac: [] } ; // Rapidement, on a trouvé la nourriture speedy.eat("pomme"); alerte( speedy.stomach ); // pomme // Le ventre du paresseux est vide alert( paresseux.estomac ); // <rien>
En tant que solution courante, toutes les propriétés qui décrivent l'état d'un objet particulier, comme stomach
ci-dessus, doivent être écrites dans cet objet. Cela évite de tels problèmes.