Récapitulatif POO — syntaxe des classes et this en un coup d'œil

Tableaux récapitulatifs sur les classes : emplacement des membres, ordre de recherche des noms, valeur de this, où ajouter une fonctionnalité, moment des erreurs.

Dans le code d'un gestionnaire de tâches, les membres static et #, les classes enfants créées avec extends et les méthodes extraites de leurs objets finissent dans un même fichier. Sans savoir ce qui les distingue, tu ne peux pas remonter à l'origine d'un undefined ou d'une TypeError.

Cet article reprend la syntaxe de cette catégorie sous forme de tableaux récapitulatifs, en se concentrant sur la syntaxe des classes et sur this.

Lire les membres selon leur emplacement — la syntaxe des classes en un coup d'œil

Tu lis une classe Task écrite par quelqu'un d'autre. Des lignes avec static, # et get se côtoient entre les mêmes accolades, et les lire une par une ne t'explique ni pourquoi task.fromJSON(text) lève une TypeError, ni pourquoi l'état d'achèvement n'apparaît pas dans JSON.stringify(task).

La façon dont tu écris un élément dans le corps d'une classe décide de son emplacement : l'instance elle-même, Task.prototype (où vont les méthodes partagées par toutes les instances) ou la classe elle-même. Pour lire un nom, JavaScript regarde l'instance elle-même, puis Task.prototype, puis Object.prototype.

class Task {
  static count = 0;                              // Seule la classe Task en possède un
  priority = "normale";                          // Ajouté à chaque tâche à chaque new
  #done = false;                                 // Stocké sur task, mais illisible hors de la classe
  constructor(title) {
    this.title = title;
    Task.count += 1;
  }
  complete() { this.#done = true; }              // Placé sur Task.prototype, partagé par toutes les instances
  get status() { return this.#done ? "terminée" : "à faire"; }
  static fromJSON(text) { return new Task(JSON.parse(text).title); }
}

// Vérifier les noms lisibles ou non depuis une instance
const task = Task.fromJSON('{"title":"Envoyer le devis"}');
task.complete();
console.log(task.status, Task.count);                                 // terminée 1
console.log(Object.keys(task).join(", "));                            // priority, title
console.log(JSON.stringify(task));                                    // {"priority":"normale","title":"Envoyer le devis"}
console.log(task.hasOwnProperty("complete"), typeof task.fromJSON);   // false undefined
Les noms accessibles depuis task
Zone de recherche de task.nom
  • Ordre de recherche : task lui-même → Task.prototype → Object.prototype
task lui-même
  • title, priority — listés comme clés et présents dans le JSON
Task.prototype (partagé)
  • complete(), get status() — trouvés lors de la recherche
Champs privés hors de portée de task.nom
  • #done — stocké sur task, mais lisible uniquement via this.#done dans la classe
La classe Task (static)
  • count, fromJSON() — hors de la zone de recherche
  • task.fromJSON vaut undefined
Les noms en # et les membres static sont hors de la zone où task cherche. task.fromJSON est introuvable : appelle plutôt Task.fromJSON.

status se trouve sur Task.prototype, et #done est bien sur task mais n'est pas listé comme clé : aucun des deux n'apparaît donc dans Object.keys ni dans JSON.stringify. Pour les inclure dans le JSON, renvoie les valeurs voulues depuis toJSON. Le tableau ci-dessous classe la syntaxe de cette catégorie selon l'emplacement de chaque membre.

SyntaxeEmplacementLu depuis task
this.title dans le constructortask lui-mêmeClés et JSON
Champ de classe prioritytask lui-mêmeClés et JSON
Champ privé #donetask (lu dans la classe)Ni clés ni JSON
Méthode complete()Task.prototypeAppelable, hors clés
get / set statusTask.prototypeLu sans ()
static count / fromJSON()La classe Taskundefined

Suivre quelle définition s'exécute — extends et super

Prends une tâche hebdomadaire récurrente créée avec class RecurringTask extends Task. En lisant la ligne weekly.describe(), impossible de savoir si c'est le describe de la classe enfant ou celui du parent qui s'exécute, ni pourquoi tu peux appeler complete alors que la classe enfant ne le définit pas.

Un nom est cherché le long de la chaîne de prototypes, en partant de l'objet lui-même, et c'est la première définition trouvée qui s'exécute. Une méthode redéfinie dans une classe enfant est trouvée avant celle du parent, et super.describe() commence sa recherche au parent. Les méthodes écrites séparément pour chaque type grâce au polymorphisme sont trouvées dans ce même ordre.

class Task {
  constructor(title) { this.title = title; }
  describe() { return `Tâche : ${this.title}`; }
  complete() { return `Terminée : ${this.title}`; }
}

// Classe enfant : appelle le constructor du parent avec super() et ne redéfinit que describe
class RecurringTask extends Task {
  constructor(title, weekday) { super(title); this.weekday = weekday; }
  describe() { return `${super.describe()} (chaque ${this.weekday})`; }
}

// On appelle les méthodes pour voir quelle définition s'exécute
const weekly = new RecurringTask("Envoyer le rapport hebdo", "vendredi");
console.log(weekly.describe());                              // Tâche : Envoyer le rapport hebdo (chaque vendredi)
console.log(weekly.complete());                              // Terminée : Envoyer le rapport hebdo
console.log(`${weekly}`);                                    // [object Object]
console.log(weekly instanceof Task, typeof weekly.archive);  // true undefined
L'ordre des niveaux parcourus depuis weekly
weekly(l'instance)A titleet weekdayRecurringTask.prototypeTrouve describe← weekly.describe()Task.prototypeTrouve complete← super.describe()Object.prototypeTrouve toString← ${weekly}
describe est trouvé au deuxième niveau et complete au troisième. Le describe du parent ne s'exécute que via super.describe().

Un nom absent de tous les niveaux, comme archive, vaut undefined, et l'appeler lève une TypeError. ${weekly} donne [object Object] parce que c'est le toString par défaut du dernier niveau qui s'exécute.

Déterminer this selon la façon d'appeler — this en un coup d'œil

Tu ranges la méthode d'achèvement d'une tâche dans une variable pour l'appeler plus tard, ou tu la passes à setTimeout. La méthode qui fonctionne sous la forme task.complete() peut alors se retrouver avec un this qui n'est plus task, selon la façon dont tu la passes : une TypeError est levée, ou this.title vaut undefined.

Le moment où la valeur de this est fixée dépend de la façon dont la fonction est écrite. Pour une méthode ordinaire de classe, c'est ce qui se trouve à gauche du point sur la ligne d'appel ; pour un champ contenant une fonction fléchée, c'est au moment où new crée l'instance. Ce que renvoie bind est une fonction distincte, dont le this est fixé à la valeur passée à bind.

class Task {
  onComplete = () => `Terminée : ${this.title}`;  // this est la tâche créée par new
  constructor(title) { this.title = title; }
  complete() { return `Terminée : ${this.title}`; }   // this est fixé par la ligne d'appel
}
const task = new Task("Envoyer la facture");

// Appel avec task à gauche du point
console.log(task.complete());   // Terminée : Envoyer la facture

// On range la méthode dans une variable, puis on l'appelle
const runMethod = task.complete;
const runArrow = task.onComplete;
const runBound = task.complete.bind(task);
console.log(runArrow());        // Terminée : Envoyer la facture
console.log(runBound());        // Terminée : Envoyer la facture
// runMethod();                 // TypeError: Cannot read properties of undefined (reading 'title')
Comment this est fixé pour une fonction extraite
const runMethod= task.completeMéthode ordinaire→ fixé à l'appelrunMethod() : rienà gauche du pointthis = undefinedTypeErrorconst runArrow= task.onCompleteChamp fléché→ fixé au newVia runArrow(),this reste taskTerminée : Envoyerla facture
Dans runMethod(), this vaut undefined. Écrite comme dans la rangée du bas, une fonction extraite et passée ailleurs conserve task.

Dans le code ci-dessus, runBound, extrait lui aussi, s'exécute toujours avec task. Avec setTimeout(task.complete, 100), le navigateur donne à this la valeur window : enveloppe donc l'appel dans une fonction fléchée. Le tableau ci-dessous indique quand this est fixé pour chaque façon d'appeler.

Façon d'appelerthis fixéUne fois extraite
Méthode ordinaireGauche du point à l'appelPerd task : TypeError
Retour de bindÀ l'appel de bindGarde task
Champ fléchéAu newGarde task
Enveloppe () => task.complete()Quand elle appelle task.complete()Garde task
Méthode ajoutée par Object.assignGauche du point à l'appelPerd son objet cible

Une méthode du même nom dans la classe enfant ne s'exécute jamais

Même si tu écris class RecurringTask extends Task { onComplete() { … } }, le champ fléché du parent Task est placé sur weekly lui-même et trouvé en premier : weekly.onComplete() exécute donc la fonction du parent. Si des classes enfants doivent la redéfinir, écris-la aussi comme méthode dans le parent.

Choisir où ajouter une fonctionnalité — héritage, prototype ou composition

Tu ajoutes des fonctionnalités à un gestionnaire de tâches déjà en service : épingler une seule tâche, afficher une étiquette sur toutes les tâches existantes, ou assigner un responsable aux seules tâches d'équipe. Si tu écris la fonctionnalité au mauvais endroit, l'appel sur une tâche que tu croyais équipée lève une TypeError.

Les façons d'ajouter des fonctionnalités vues dans cette catégorie diffèrent par l'endroit où la fonctionnalité est écrite : l'instance elle-même, Task.prototype, ou un nouveau niveau relié par extends. Une fois l'endroit connu, tu sais aussi quelles instances peuvent trouver ce nom le long de leur chaîne.

class Task {
  constructor(title) { this.title = title; }
}
const first = new Task("Envoyer le devis");            // On crée d'abord 2 tâches
const second = new Task("Rédiger le compte rendu");

// Copie dans une seule tâche
Object.assign(first, { pin() { return `Épinglée : ${this.title}`; } });
// Affectation à Task.prototype
Task.prototype.label = function () { return `Tâche : ${this.title}`; };
// Une classe avec un niveau de plus, ajouté par un mixin
const Assignable = (Base) => class extends Base {
  assign(name) { return `Assignée à ${name} : ${this.title}`; }
};
class TeamTask extends Assignable(Task) {}

console.log(typeof second.pin, second.label());          // undefined Tâche : Rédiger le compte rendu
console.log(typeof first.assign);                        // undefined
console.log(new TeamTask("Envoyer le rapport hebdo").assign("Alice"));   // Assignée à Alice : Envoyer le rapport hebdo
L'endroit où tu écris une fonctionnalité décide de sa portée
first et secondcréées par TaskObject.assign(first, brique)Task.prototype.label = functionextendsAssignable(Task)Va dans les cléspropres de firstAjouté une fois àTask.prototypeNouveau niveauparent de TeamTaskfirst seulement ;second : undefinedLes 2 tâchesexistantes l'ontnew TeamTask seul(pas first/second)
Écrite sur first lui-même, elle n'atteint que cette tâche ; sur Task.prototype, elle atteint les deux tâches existantes. Un mixin n'atteint que les instances de la nouvelle classe.

label atteint second, qui existait déjà, parce que second cherche dans Task.prototype le long de sa chaîne. L'affectation au prototype sert pour les classes dont tu ne peux pas modifier le corps. Une méthode écrite dans une classe enfant, comme avec un mixin, n'atteint que les instances créées à partir de cette classe enfant.

Vérifier avec typeof les fonctionnalités ajoutées à un seul objet

Même si Object.assign n'a copié pin que dans first, instanceof Task vaut true pour first comme pour second. La classe ne permet pas de les distinguer : vérifie donc que la méthode existe avec typeof task.pin === "function" avant de l'appeler.

Trouver la cause selon le moment de l'arrêt — les erreurs en un coup d'œil

Quand du code de classe lève une erreur, la ligne où il s'arrête et celle à corriger peuvent être éloignées. Une faute dans le constructor d'une classe enfant ne provoque une erreur qu'à l'exécution de new, sur une ligne de ce constructor, et avec un nom en # écrit hors de la classe, même la sortie des console.log situés plus haut n'apparaît pas.

Les erreurs se répartissent en trois groupes selon le moment où elles arrêtent le code : pendant le chargement du code, quand new exécute le constructor, ou quand une méthode est appelée. Avec une SyntaxError au chargement, aucune ligne ne s'exécute ; dans les deux autres cas, les lignes situées avant l'erreur s'exécutent d'abord, puis le code s'arrête.

class Task {
  #done = false;
  constructor(title) { this.title = title; }
  complete() { return `Terminée : ${this.title}`; }
}
class RecurringTask extends Task {
  constructor(title, weekday) {
    this.weekday = weekday;   // this utilisé avant super()
    super(title);
  }
}
console.log("Début de la vérification");   // Affiché avec ② et ③, mais pas si la ligne ① est présente

// ① Au chargement : console.log(new Task("Envoyer la facture").#done);
//    SyntaxError: Private field '#done' must be declared in an enclosing class
// ② Au new : new RecurringTask("Envoyer le rapport hebdo", "vendredi");
//    ReferenceError: Must call super constructor in derived class before accessing 'this' …
// ③ À l'appel : const run = new Task("Envoyer la facture").complete; run();
//    TypeError: Cannot read properties of undefined (reading 'title')
Les trois moments où une erreur arrête le code
① Pendant lechargementSyntaxErrorAucune ligneexécutée② new exécutele constructorReferenceErrorLignes avantnew exécutées③ Une méthodeest appeléeTypeErrorLignes avantl'appel exécutées
Plus la rangée est haute, plus l'arrêt est précoce ; avec ①, même le message de vérification n'apparaît pas. Si rien ne s'affiche du tout, soupçonne une SyntaxError au chargement.

Avec ② et ③, tout ce qui précède s'affiche avant l'arrêt : tu peux donc chercher la cause juste après la dernière sortie. Le tableau ci-dessous liste les erreurs rencontrées dans cette catégorie, de l'arrêt le plus précoce au plus tardif.

Message d'erreurCauseCorrection
SyntaxError: Private fieldtask.#done hors de la classePasser par un getter
SyntaxError: … export namedExport par défaut lu avec { Cart }Retirer les accolades
ReferenceError: Must call superthis avant super()Appeler super() d'abord
TypeError: … without 'new'Classe appelée sans newAjouter new
TypeError: Cannot read propertiesMéthode extraite appelée en run()bind ou fonction fléchée
RangeError: Maximum call stacksetter qui s'affecte lui-même / super. oubliéAutre nom / ajouter super.
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1RecurringTask ne définit pas complete. À quel niveau la définition de weekly.complete() est-elle trouvée ?

Question 2Si tu appelles un champ fléché avec const run = task.onComplete; run();, que vaut this ?

Question 3Si un new placé après un console.log lève une ReferenceError, qu'advient-il de cette sortie ?