Question 1Pour une méthode summary écrite dans une classe, que renvoie jsCourse.hasOwnProperty("summary") ?
Prototypes — ce qui se cache derrière une classe
Où vivent les méthodes d'une classe, et comment la chaîne de prototypes les trouve : Object.getPrototypeOf, hasOwnProperty et in.
Un cours créé à partir d'une classe peut appeler summary(), et pourtant l'instance elle-même ne possède pas cette méthode. Si tu ne sais pas où elle se trouve, tu ne peux pas non plus expliquer pourquoi une classe enfant peut appeler les méthodes de sa classe parente.
Cet article traite du prototype, l'objet où sont rangées les méthodes d'une classe, et de la chaîne de prototypes, l'ordre dans lequel JavaScript cherche un nom.
Appeler une méthode que l'instance n'a pas — prototype
Si tu crées 300 cours, tu te demandes peut-être si 300 copies du corps de summary sont créées elles aussi, une par instance. Object.keys(jsCourse) ne liste pas summary : cela ne te dit pas où sont rangées les méthodes que tu appelles.
Le prototype (l'objet dont un objet hérite les propriétés qu'il ne possède pas lui-même) s'obtient avec Object.getPrototypeOf(), et pour jsCourse, c'est Course.prototype. Tout nom que l'objet ne possède pas lui-même est cherché à cet endroit.
La propriété prototype de la classe est ce même objet, mais jsCourse.prototype vaut undefined.
class Course {
constructor(title, hours) {
this.title = title; // Nom du cours
this.hours = hours; // Heures d'étude
}
summary() { return `${this.title} (${this.hours} heures)`; }
}
const jsCourse = new Course("Bases de JS", 12);
const sqlCourse = new Course("Bases de SQL", 8);
// summary ne fait pas partie des clés propres de l'instance
console.log(Object.keys(jsCourse).join(", ")); // title, hours
// Le prototype de jsCourse est Course.prototype (pas jsCourse.prototype)
console.log(Object.getPrototypeOf(jsCourse) === Course.prototype); // true
console.log(jsCourse.prototype); // undefined
// Les deux cours utilisent l'unique summary de Course.prototype
console.log(jsCourse.summary === sqlCourse.summary); // true
console.log(sqlCourse.summary()); // Bases de SQL (8 heures)
Il n'y a qu'un seul corps, mais this reçoit ce qui se trouve à gauche du point sur la ligne d'appel : sqlCourse.summary() construit donc sa chaîne avec les valeurs de sqlCourse. Même avec 300 cours, seules les instances, qui contiennent title et hours, se multiplient : summary n'existe qu'une fois.
Chercher dans l'ordre jusqu'à null — la chaîne de prototypes
Si tu crées un cours en direct avec class LiveCourse extends Course, liveCourse.summary() fonctionne aussi. Pourtant, la seule méthode écrite dans le prototype de liveCourse, LiveCourse.prototype, est schedule.
Les noms sont cherchés le long de la chaîne de prototypes (la suite de références obtenue en suivant le prototype du prototype jusqu'à null), en partant de l'instance. extends fait de Course.prototype le maillon qui suit LiveCourse.prototype, et vient ensuite Object.prototype (un objet intégré placé au bout de la chaîne).
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title} (${this.hours} heures)`; }
}
class LiveCourse extends Course { schedule() { return "Le mardi à partir de 20 h"; } }
const liveCourse = new LiveCourse("React en pratique", 10);
// Récupère les prototypes un niveau à la fois
const step1 = Object.getPrototypeOf(liveCourse);
const step2 = Object.getPrototypeOf(step1);
const step3 = Object.getPrototypeOf(step2);
console.log(step1 === LiveCourse.prototype); // true
console.log(step2 === Course.prototype); // true
console.log(step3 === Object.prototype); // true
console.log(Object.getPrototypeOf(step3)); // null (la fin de la chaîne)
// summary est trouvé au deuxième niveau, Course.prototype
console.log(liveCourse.summary()); // React en pratique (10 heures)
La chaîne contient Course.prototype, et non Course lui-même. Les méthodes marquées static sont placées sur Course : si tu en lis une depuis une instance, la recherche va jusqu'à null sans la trouver et donne undefined ; l'appeler lève une TypeError.
Une clé de même nom sur l'instance est trouvée en premier
Si tu écris liveCourse.summary = "Bientôt disponible";, le nom est trouvé sur liveCourse lui-même, et le prototype n'est jamais consulté. liveCourse.summary() lève alors TypeError: liveCourse.summary is not a function.
S'en tenir aux clés propres de l'instance — hasOwnProperty
Admettons que tu reçoives des données de cours et que tu veuilles vérifier si summary a été écrasé par une affectation directe sur certains cours. "summary" in jsCourse vaut true même pour une méthode du prototype : ce test ne te dit donc pas si la valeur a été affectée à l'instance elle-même.
hasOwnProperty (une méthode qui ne renvoie true que si l'objet lui-même possède la clé) reçoit la clé sous forme de chaîne, comme dans jsCourse.hasOwnProperty("title"). Elle est elle-même définie sur Object.prototype, et l'instance y accède grâce à la chaîne.
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title} (${this.hours} heures)`; }
}
const jsCourse = new Course("Bases de JS", 12);
const sqlCourse = new Course("Bases de SQL", 8);
sqlCourse.summary = "Bientôt disponible"; // Écrasé pour ce cours uniquement
// in vaut true même pour les noms du prototype
console.log("summary" in jsCourse); // true
console.log("summary" in sqlCourse); // true
// hasOwnProperty ne vaut true que pour les clés propres du cours
console.log(jsCourse.hasOwnProperty("summary")); // false
console.log(sqlCourse.hasOwnProperty("summary")); // true (il a été écrasé)
console.log(Course.prototype.hasOwnProperty("summary")); // true
title—"Bases de JS"hours—12
summary()—trueavecin,falseavecjsCourse.hasOwnProperty
hasOwnProperty()—jsCoursepeut l'appeler parce qu'elle est trouvée ici
Pour sqlCourse, écrasé par une affectation, hasOwnProperty("summary") renvoie true : une seule ligne suffit donc pour repérer les cours écrasés. Le tableau ci-dessous montre comment d'autres façons de vérifier traitent title et summary.
| Vérification | title (propre à jsCourse) | summary (sur Course.prototype) |
|---|---|---|
| Listé par Object.keys(jsCourse) ? | Oui | Non |
| Listé par for...in ? | Oui | Non |
| Course.prototype.hasOwnProperty("clé") | false | true |
Ajouter des méthodes de l'extérieur — affecter à prototype
Suppose maintenant que les instances de cours soient créées par le code partagé d'une autre équipe, puis qu'on te les transmette : tu ne peux modifier ni le corps de la classe ni les lignes new. Si tu affectes à chaque cours, un par un, une fonction d'affichage qui renvoie « Cours : nom », l'appeler sur un cours oublié lève une TypeError.
Si tu affectes une fonction à prototype, comme dans Course.prototype.label = function () { ... }, toutes les instances trouvent le même label grâce à la chaîne. Si tu peux modifier le corps de la classe, écris plutôt la méthode dedans.
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title} (${this.hours} heures)`; }
}
const jsCourse = new Course("Bases de JS", 12); // On crée d'abord deux cours
const sqlCourse = new Course("Bases de SQL", 8);
// Affecter uniquement à jsCourse laisse sqlCourse sans label
jsCourse.label = function () { return `Cours : ${this.title}`; };
console.log(typeof sqlCourse.label); // undefined (l'appeler lève une TypeError)
// Une seule affectation à Course.prototype (écrite avec function, car elle lit this)
Course.prototype.label = function () { return `Cours : ${this.title}`; };
console.log(sqlCourse.label()); // Cours : Bases de SQL
console.log(sqlCourse.hasOwnProperty("label")); // false
// Rassemble les clés renvoyées par for...in
const keys = [];
for (const key in sqlCourse) keys.push(key);
console.log(keys.join(", ")); // title, hours, label
Il est trouvé même pour sqlCourse, créé avant l'affectation, parce que la chaîne est parcourue au moment où le nom est utilisé. Contrairement à summary, écrit dans le corps de la classe, un label ajouté par affectation apparaît aussi dans for...in : le code qui parcourt les clés le retrouve mêlé aux clés des données.
Écris avec function les fonctions ajoutées à prototype
Si tu écris Course.prototype.label = () => this.title;, la fonction fléchée utilise le this de l'extérieur de la classe. Dans l'éditeur de ce site, il vaut undefined, donc sqlCourse.label() lève une TypeError. Utilise function à la place.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Avec LiveCourse extends Course, quel est le prototype de LiveCourse.prototype ?
Question 3Des cours existent déjà. Que se passe-t-il si tu affectes ensuite à Course.prototype un label écrit avec function ?