En programmation, nous voulons souvent prendre quelque chose et lâétendre.
Par exemple, nous avons un objet user avec ses propriétés et méthodes et souhaitons en faire des variantes admin et guest légèrement modifiées. Nous aimerions réutiliser ce que nous avons dans user, et non pas copier/réimplémenter ses méthodes, mais simplement créer un nouvel objet par-dessus.
Lâhéritage prototypal est une fonctionnalité de langage qui aide à cela.
[[Prototype]]
En JavaScript, les objets ont une propriété cachée spéciale [[Prototype]] (comme indiqué dans la spécification), qui est soit null ou fait référence à un autre objet. Cet objet sâappelle âun prototypeâ :
Lorsque nous lisons une propriété depuis object, et quâelle est manquante, JavaScript la prend automatiquement du prototype. En programmation, une telle chose est appelée âhéritage prototypalâ. Et bientôt, nous étudierons de nombreux exemples dâun tel héritage, ainsi que des fonctionnalités de langage plus cool qui en découlent.
La propriété [[Prototype]] est interne et cachée, mais il y a plusieurs façons de la définir.
Lâun dâeux est dâutiliser le nom spécial __proto__, comme ceci :
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // sets rabbit.[[Prototype]] = animal
Si nous recherchons une propriété dans rabbit, et quâelle en manque, JavaScript la prend automatiquement à partir de animal.
Par exemple :
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// nous pouvons maintenant trouver les deux propriétés dans rabbit :
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
Ici, la ligne (*) définit animal comme le prototype de lapin.
Ensuite, lorsque alert essaie de lire la propriété rabbit.eats (**), ce nâest pas dans rabbit, donc JavaScript suit la référence [[Prototype]] et la trouve dans animal (regarde de bas en haut) :
Ici, nous pouvons dire que âanimal est le prototype de rabbitâ ou que ârabit hérite de manière prototypal de animalâ.
Donc, si animal a beaucoup de propriétés et de méthodes utiles, elles deviennent automatiquement disponibles dans rabbit. De telles propriétés sont appelées âhéritéesâ.
Si nous avons une méthode dans animal, elle peut être appelée sur rabbit :
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// walk est prise à partir du prototype
rabbit.walk(); // Animal walk
La méthode est automatiquement prise à partir du prototype, comme ceci :
La chaîne de prototypes peut être plus longue :
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// walk est prise à partir de la chaîne de prototype
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (de rabbit)
Maintenant, si nous lisons quelque chose de longEar, et quâil est manquant, JavaScript le recherchera dans rabbit, puis dans animal.
Il nây a que deux limitations :
- Les références ne peuvent pas tourner en rond. JavaScript va générer une erreur si nous essayons dâassigner
__proto__dans un cercle. - La valeur de
__proto__peut être un objet ounull. Les autres types sont ignorés.
Cela peut aussi ê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 novices de ne pas connaître la différence entre les deux.
Veuillez noter que __proto__ nâest pas la même que la propriété interne [[Prototype]]. Câest un getter/setter pour [[Prototype]]. Plus tard, nous verrons des situations où cela compte, pour lâinstant gardons cela à lâesprit, alors que nous construisons notre compréhension du langage JavaScript.
La propriété __proto__ est un peu obsolète. Elle existe pour des raisons historiques, le JavaScript moderne suggère que nous devrions utiliser les fonctions Object.getPrototypeOf/Object.setPrototypeOf à la place 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 cependant, tous les environnements, y compris côté serveur, prennent en charge __proto__, donc nous sommes assez sûrs de lâutiliser.
Comme la notation __proto__ est un peu plus évidente, nous lâutilisons dans les exemples.
Lâécriture nâutilise pas de prototype
Le prototype nâest utilisé que pour la lecture des propriétés.
Les opérations dâécriture/suppression fonctionnent directement avec lâobjet.
Dans lâexemple ci-dessous, nous affectons sa propre méthode walk à rabbit :
let animal = {
eats: true,
walk() {
/* cette méthode ne sera pas utilisée par rabbit */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
A partir de maintenant, 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 dâaccesseur constituent une exception, car lâaffectation est gérée par une fonction mutateur. 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 :
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // John Smith (*)
// le mutateur se déclanche !
admin.fullName = "Alice Cooper"; // (**)
alert(admin.fullName); // Alice Cooper, l'état de admin est modifié
alert(user.fullName); // John Smith, l'état de user est protégé
Ici dans la ligne (*) la propriété admin.fullName a un accesseur dans le prototype user, il est donc appelé. Et dans la ligne (**) la propriété a un mutateur dans le prototype, il est donc appelé.
La valeur de âthisâ
Une question intéressante peut se poser dans lâexemple ci-dessus : quelle est la valeur de this dans 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ù la méthode est trouvée : dans un objet ou son prototype. Dans un appel de méthode, this est toujours lâobjet avant le point.
Ainsi, lâappel du groupe admin.fullName= utilise admin comme this, pas user.
Câest en fait une chose très importante, car nous pouvons avoir un gros objet avec de nombreuses méthodes et en hériter. Ensuite, les objets hérités peuvent exécuter ces méthodes héritées, ils ne modifieront que leurs propres états, pas lâétat du gros objet.
Par exemple, ici animal représente un âstockage de méthodeâ et rabbit en fait usage.
Lâappel rabbit.sleep() définit this.isSleeping sur lâobjet rabbit :
// animal a des méthodes
let animal = {
walk() {
if (!this.isSleeping) {
alert(`I walk`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
// modifie rabbit.isSleeping
rabbit.sleep();
alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (aucune propriété de ce type dans le prototype)
Lâimage résultante :
Si nous avions dâautres objets tels que 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 dâobjet.
La boucle forâ¦in
La boucle for..in itère aussi sur les propriétés héritées.
Par exemple :
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys ne renvoie que ses propres clés
alert(Object.keys(rabbit)); // jumps
// for..in boucle sur les clés propres et héritées
for(let prop in rabbit) alert(prop); // jumps, puis eats
Si ce nâest pas ce que nous voulons et que nous aimerions 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) :
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
alert(`Our: ${prop}`); // Our : jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
Nous avons ici la chaîne dâhéritage suivante : rabbit hérite de animal, qui lui hérite de Object.prototype (car animal est un objet littéral {...}, donc câest par défaut), puis null au-dessus :
Remarque, 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 pas dans la boucle for..in, comme eats et jumps, sâil répertorie toutes les propriétés héritées.
La réponse est simple : ce nâest pas énumérable. Comme toutes les autres propriétés de Object.prototype, il possède lâattribut enumerable: false. Câest pourquoi ils ne sont pas répertoriés. Et for..in ne répertorie que les propriétés énumérables. Câest pourquoi elle et le reste des propriétés de Object.prototype ne sont pas listés.
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.
Elles ne fonctionnent que sur lâobjet lui-même. Les propriétés du prototype ne sont pas prises en compte.
Résumé
- En JavaScript, tous les objets ont une propriété masquée
[[Prototype]]qui est soit un autre objet, soitnull. - Nous pouvons utiliser
obj.__ proto__pour y accéder (un accesseur/mutateur historique, il existe dâautres moyens, à couvrir bientôt). - Lâobjet référencé par
[[Prototype]]sâappelle un âprototypeâ. - Si nous voulons lire une propriété de
objou appeler une méthode, et que celle-ci nâexiste pas, alors JavaScript essaye 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, et non dâun setter).
- Si nous appelons
obj.method(), et que laméthodeest extraite du prototype,thisfait toujours référence Ãobj. Les méthodes fonctionnent donc toujours avec lâobjet actuel, même si elles sont héritées. - La boucle
for..inparcourt les propriétés propres et héritées. Toutes les autres méthodes dâobtention de clé / valeur ne fonctionnent que sur lâobjet lui-même.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)