Question 1RecurringTask ne définit pas complete. À quel niveau la définition de weekly.complete() est-elle trouvée ?
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
- Ordre de recherche : task lui-même → Task.prototype → Object.prototype
title,priority— listés comme clés et présents dans le JSON
complete(),get status()— trouvés lors de la recherche
#done— stocké sur task, mais lisible uniquement viathis.#donedans la classe
count,fromJSON()— hors de la zone de recherchetask.fromJSONvautundefined
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.
| Syntaxe | Emplacement | Lu depuis task |
|---|---|---|
| this.title dans le constructor | task lui-même | Clés et JSON |
| Champ de classe priority | task lui-même | Clés et JSON |
| Champ privé #done | task (lu dans la classe) | Ni clés ni JSON |
| Méthode complete() | Task.prototype | Appelable, hors clés |
| get / set status | Task.prototype | Lu sans () |
| static count / fromJSON() | La classe Task | undefined |
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
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')
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'appeler | this fixé | Une fois extraite |
|---|---|---|
| Méthode ordinaire | Gauche du point à l'appel | Perd task : TypeError |
| Retour de bind | À l'appel de bind | Garde task |
| Champ fléché | Au new | Garde task |
| Enveloppe () => task.complete() | Quand elle appelle task.complete() | Garde task |
| Méthode ajoutée par Object.assign | Gauche du point à l'appel | Perd 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
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')
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'erreur | Cause | Correction |
|---|---|---|
| SyntaxError: Private field | task.#done hors de la classe | Passer par un getter |
| SyntaxError: … export named | Export par défaut lu avec { Cart } | Retirer les accolades |
| ReferenceError: Must call super | this avant super() | Appeler super() d'abord |
| TypeError: … without 'new' | Classe appelée sans new | Ajouter new |
| TypeError: Cannot read properties | Méthode extraite appelée en run() | bind ou fonction fléchée |
| RangeError: Maximum call stack | setter qui s'affecte lui-même / super. oublié | Autre nom / ajouter super. |
Vérification des connaissances
Répondez à chaque question une par une.
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 ?