Question 1Que se passe-t-il quand tu fais new sur un EBook qui affecte this.fileSize avant super() ?
Héritage — extends et super
Bâtir une classe sur une autre avec extends : super() dans le constructor enfant, la redéfinition de méthode et super.nomDeMethode().
Imagine une librairie en ligne qui a déjà une classe Book pour les livres papier et qui a besoin d'une classe pour les livres numériques. Les seules différences : un livre numérique se télécharge et n'a pas de frais de port. Pourtant, avec ce que tu sais jusqu'ici, tu devrais réécrire aussi toute la partie qui gère le titre et le prix.
Cet article traite d'extends, qui construit une classe à partir d'une classe existante, et de super, qui appelle le code de la classe dont tu hérites.
Réutiliser du code sans le recopier — extends
Les livres numériques ont eux aussi besoin d'un constructor qui reçoit un titre et un prix, et d'une méthode describe qui renvoie « titre : prix yens ». Si tu recopies le corps de Book dans une nouvelle classe, les mêmes lignes existent à deux endroits. Le jour où tu modifies le format d'affichage et que tu oublies l'une des deux copies, les écrans n'affichent plus la même chose.
L'héritage (le mécanisme qui permet à une autre classe d'utiliser le constructor et les méthodes d'une classe existante) s'écrit class EBook extends Book. On appelle Book la classe parente et EBook la classe enfant.
Le extends Error que tu as écrit dans l'article sur throw et les erreurs personnalisées, c'est aussi de l'héritage.
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
describe() { return `${this.title} : ${this.price} yens`; }
}
// Hérite de Book et ajoute une méthode propre aux livres numériques
class EBook extends Book {
download() { return `Téléchargement : ${this.title}`; }
}
const ebook = new EBook("Bases des stats", 2200); // Les arguments vont au constructor de Book
console.log(ebook.describe()); // Bases des stats : 2200 yens
console.log(ebook.download()); // Téléchargement : Bases des stats
const book = new Book("Bases des stats", 2640);
console.log(typeof book.download); // undefined (la classe parente n'a pas les méthodes de l'enfant)
Dans ebook.describe(), this vaut ebook : le this.title écrit dans Book lit donc le titre que tu as passé à new EBook. Et c'est le constructor de Book, qui a reçu ces arguments, qui a placé ce titre dans ebook.
Ajouter des valeurs dans une classe enfant — super() et le constructor
Suppose maintenant que les livres numériques doivent aussi stocker la taille du fichier, fileSize, en plus du titre et du prix. Pour la recevoir, EBook a besoin de son propre constructor. Mais dès que tu en écris un, le constructor de Book n'est plus appelé automatiquement, et si tu t'arrêtes là, new lève une ReferenceError.
super() (la syntaxe qui appelle le constructor de la classe parente depuis le constructor d'une classe enfant) reçoit les arguments à transmettre au parent, comme dans super(title, price);.
Dans une classe enfant, this est préparé par le constructor du parent pendant super() : appelle-le donc avant toute ligne qui utilise this.
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
}
class EBook extends Book {
constructor(title, price, fileSize) {
// Écrire this.fileSize = fileSize; au-dessus de cette ligne fait lever une ReferenceError à new
super(title, price); // Le constructor de Book définit title et price
this.fileSize = fileSize; // Ensuite, on ajoute la valeur propre à EBook
}
}
const ebook = new EBook("Bases des stats", 2200, 12);
console.log(ebook.title); // Bases des stats
console.log(ebook.fileSize); // 12
console.log(Object.keys(ebook).join(", ")); // title, price, fileSize
Les clés sortent dans l'ordre title, price, fileSize parce que Book a défini ses valeurs en premier, pendant super(). La rangée du haut échoue avec ReferenceError: Must call super constructor in derived class before accessing 'this' …, où « derived class » désigne la classe enfant.
Une classe enfant ne peut pas lire les champs en # du parent
Si Book a un champ privé #price, super() lui donne bien sa valeur, mais écrire this.#price dans le corps de EBook provoque une SyntaxError. Un nom en # n'est utilisable que dans la classe qui le déclare : une classe enfant doit donc passer par un getter défini dans le parent.
Changer les frais de port dans une classe enfant — la redéfinition
Si tu ajoutes une méthode shippingFee à Book, les livres numériques ont les mêmes frais de port que les livres papier. Tu pourrais les passer à 0 yen pour les livres numériques en testant le type de livre avec un if dans Book, mais la classe parente aurait alors besoin d'une branche de plus à chaque nouvelle classe enfant.
La redéfinition (override : écrire dans une classe enfant une méthode du même nom que dans le parent, pour que ce soit la version de l'enfant qui s'exécute sur ses instances) consiste simplement à réécrire shippingFee dans le corps de EBook. Quand tu appelles une méthode, JavaScript cherche son nom d'abord dans la classe qui a créé l'instance, puis dans la classe parente s'il ne l'y trouve pas.
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
describe() { return `${this.title} : ${this.price} yens`; }
shippingFee() { return this.price >= 3000 ? 0 : 500; } // Livraison gratuite à partir de 3000 yens
}
class EBook extends Book {
shippingFee() { return 0; } // Redéfinie sous le même nom (rien à expédier)
}
const book = new Book("Bases des stats", 2640);
const ebook = new EBook("Bases des stats", 2200);
console.log(book.shippingFee()); // 500
console.log(ebook.shippingFee()); // 0 (c'est le shippingFee de EBook qui s'exécute)
console.log(ebook.describe()); // Bases des stats : 2200 yens (c'est le describe de Book qui s'exécute)
Le shippingFee de EBook n'utilise pas le calcul de Book : même si tu changes les règles de livraison de Book, les livres numériques restent à 0 yen. La section suivante montre comment appeler le code du parent puis n'ajouter que ce qui change.
Compléter le code du parent — super.nomDeMethode()
Tu veux maintenant terminer la description des livres numériques par « (numérique, 12 Mo) ». Si tu redéfinis describe dans EBook en recopiant le code qui construit « titre : prix yens », la prochaine fois que tu modifieras le format d'affichage de Book, les descriptions des livres numériques resteront à l'ancien format.
Avec super.nomDeMethode() (la syntaxe qui appelle, depuis une méthode d'une classe enfant, une méthode définie dans la classe parente), super.describe() renvoie la valeur de retour du describe de Book. La classe enfant n'a plus qu'à ajouter les informations sur la version numérique à cette chaîne.
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
describe() { return `${this.title} : ${this.price} yens`; }
}
class EBook extends Book {
constructor(title, price, fileSize) {
super(title, price);
this.fileSize = fileSize;
}
// Ajoute les informations sur la version numérique à la valeur de retour du describe de Book
describe() { return `${super.describe()} (numérique, ${this.fileSize} Mo)`; }
}
const ebook = new EBook("Bases des stats", 2200, 12);
console.log(ebook.describe()); // Bases des stats : 2200 yens (numérique, 12 Mo)
super.describe() est une expression qui vaut la valeur de retour de Book : si tu préfères placer les informations sur la version numérique en tête, il suffit de la déplacer dans le littéral de gabarit. Le tableau ci-dessous récapitule les syntaxes d'héritage vues dans cet article et ce que chacune exécute.
| Syntaxe | Où l'écrire | Ce qui s'exécute |
|---|---|---|
| class EBook extends Book | Déclaration de classe | Hérite du constructor et des méthodes de Book |
| super(title, price) | constructor enfant, avant this | Le constructor de Book |
| Méthode au nom du parent | Corps de la classe enfant | La version de EBook, sur ses instances |
| super.describe() | Dans une méthode enfant | Le describe de Book |
Sans super, la méthode s'appelle elle-même sans fin
Si tu écris this.describe() au lieu de super.describe() dans le describe de la classe enfant, la méthode s'appelle elle-même jusqu'à lever RangeError: Maximum call stack size exceeded. Pour appeler le code du parent depuis une méthode que tu as redéfinie, utilise super..
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Si Book et EBook ont tous les deux une méthode shippingFee, laquelle s'exécute quand tu appelles ebook.shippingFee() ?
Question 3Dans le describe de EBook, comment récupères-tu la valeur de retour du describe de Book ?