Apprenez en lisant dans l'ordre

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)
Ce vers quoi pointent une classe et ses instances
new Course("Bases de JS", 12)class Coursenew Course("Bases de SQL", 8)jsCourseCourse.prototypea summarysqlCourseClés propres :title, hoursUn seul corpspour summaryAbsent de l'objet,donc cherché dansCourse.prototypejsCourse.prototypevaut undefinedLes deux summarysont identiquessummary() donneBases de SQL(8 heures)
Le prototype des deux cours est le prototype de la classe. summary, qu'ils ne possèdent pas eux-mêmes, se trouve là.

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)
L'ordre dans lequel summary est cherché
liveCourse(instance)A title, hoursPas de summaryLiveCourse.prototypeA schedulePas de summaryCourse.prototypeA summaryTrouvé iciObject.prototypeA des méthodesintégrées(ensuite : null)
summary est cherché de haut en bas et trouvé dans Course.prototype. La chaîne se termine par null, juste après Object.prototype.

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.

Tu vas afficher l'ordre de recherche des méthodes pour chaque type de voiture. Car, ElectricCar, compactCar, ev et prototypeNames sont déjà déclarés.

① Affiche si le prototype de ev est ElectricCar.prototype.

② Définis chainOf, qui relie les noms depuis le prototype de la voiture reçue jusqu'à juste avant null.

③ Affiche les résultats pour compactCar et ev.

(Si le code s'exécute correctement, une explication s'affiche.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
Où regardent in et hasOwnProperty
Parcouru par "summary" in jsCourse (toute la chaîne)
jsCourse lui-même (vu par hasOwnProperty)
  • title"Bases de JS"
  • hours12
Course.prototype
  • summary()true avec in, false avec jsCourse.hasOwnProperty
Object.prototype
  • hasOwnProperty()jsCourse peut l'appeler parce qu'elle est trouvée ici
in regarde toute la chaîne, alors que hasOwnProperty ne regarde que dans le cadre de jsCourse. summary n'est pas dans le cadre de jsCourse.

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érificationtitle (propre à jsCourse)summary (sur Course.prototype)
Listé par Object.keys(jsCourse) ?OuiNon
Listé par for...in ?OuiNon
Course.prototype.hasOwnProperty("clé")falsetrue

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
Là où tu affectes label change le résultat
label affecté àjsCourse seulsqlCourse lui-mêmen'a pas labelAbsent de lachaîne aussi :undefinedsqlCourse.label()lève une TypeErrorAffecté àCourse.prototype.labelsqlCourse lui-mêmen'a pas labelTrouvé surCourse.prototypeCours :Bases de SQL
Dans les deux cas, sqlCourse lui-même n'a pas label. Il n'est trouvé grâce à la chaîne que si tu l'affectes à Course.prototype.

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.

Tu vas ajouter l'affichage de la durée à une liste de films, sans modifier le corps de Video. Video et videos sont déjà déclarés.

① Ajoute durationLabel, que tous les films peuvent appeler et qui renvoie « N h N min ».

② Pour chaque film, affiche « Titre : N h N min ».

③ Affiche si le premier film possède lui-même durationLabel.

④ Parcours le premier film avec for...in et affiche les clés que le film ne possède pas lui-même.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Pour une méthode summary écrite dans une classe, que renvoie jsCourse.hasOwnProperty("summary") ?

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 ?