Apprenez en lisant dans l'ordre

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)
L'arbre d'héritage de Book à EBook
class Bookconstructor,describebook n'appelleque describeclass EBookextends BookEBook ne définitque downloadInstance crééepar new EBook(...)Peut appelerdescribe etdownload
EBook ne définit que download, et pourtant tu peux aussi appeler describe. L'inverse ne marche pas : book ne peut pas appeler download.

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.

Tu vas afficher une ligne de journal pour chaque mot de passe réinitialisé par un administrateur. User et targets sont déjà déclarés.

① Définis AdminUser, qui hérite de User.

② Ajoute resetPassword, qui renvoie une ligne de journal réunissant la description de l'administratrice et le nom de l'utilisateur concerné.

③ Crée l'administratrice et affiche une ligne de journal pour chaque personne de targets.

④ Affiche si l'administratrice est aussi une instance de User.

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

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
super() est-il appelé avant this ?
this.fileSizeécrit en premiersuper() pasencore appeléErreur à la lignequi utilise thisReferenceErrorà newsuper(title,price) écriten premierBook définittitle et pricethis.fileSizereçoit 12ebook contientles 3 valeurs
Dans la rangée du haut, utiliser this avant super() lève une erreur et aucune instance n'est créée. Tu ne peux utiliser this qu'une fois super() terminé.

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.

Tu vas afficher les droits de chaque administrateur dans un écran d'administration. User et adminInput sont déjà déclarés.

① Définis AdminUser, qui hérite de User et reçoit un nom, une adresse e-mail et un tableau de droits.

② Fais définir le nom et l'adresse e-mail par le constructor de User.

③ Crée un administrateur à partir de adminInput et affiche son nom avec le détail de ses droits.

④ Affiche les clés dans l'ordre, et regarde où apparaît le status défini dans User.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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)
Où JavaScript cherche la méthode appelée
EBook ne redéfinitque shippingFeebook.shippingFee()ebook.shippingFee()ebook.describe()Part de Book,trouvé dans BookPart de EBook,trouvé dans EBookAbsent de EBook,trouvé dans Book2640 < 3000,donc 5000, sans chercherdans BookBases des stats :2200 yens
La recherche part de la classe qui a créé l'instance. Si le nom est trouvé dans EBook, la méthode de même nom de Book ne s'exécute pas.

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.

Sur un forum interne, tu vas décider pour chaque message s'il faut afficher un bouton de modification. User et posts sont déjà déclarés.

① Définis AdminUser, qui hérite de User, et redéfinis la vérification pour qu'un administrateur puisse modifier n'importe quel message.

② Crée une utilisatrice standard « Léa » et une administratrice « Alice ».

③ Pour chaque message, affiche si chacune des deux peut le modifier, sous la forme « Message N : Léa X / Alice Y ».

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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)
Ce qui s'exécute dans ebook.describe(), dans l'ordre
ebook.describe()démarrethis vaut ebook(fileSize vaut 12)super.describe()exécute ledescribe de Bookthis reste ebook :Bases des stats :2200 yensAjoute (numérique,12 Mo) à la valeurde retourRenvoie Bases desstats : 2200 yens(numérique, 12 Mo)
Le describe de Book s'exécute au milieu du describe de EBook, puis rend la main. Le code qui assemble le titre et le prix n'existe que dans Book.

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.

SyntaxeOù l'écrireCe qui s'exécute
class EBook extends BookDéclaration de classeHérite du constructor et des méthodes de Book
super(title, price)constructor enfant, avant thisLe constructor de Book
Méthode au nom du parentCorps de la classe enfantLa version de EBook, sur ses instances
super.describe()Dans une méthode enfantLe 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..

Dans un outil interne, tu vas appliquer une règle de mot de passe plus stricte aux seuls administrateurs. User, AdminUser, admin, member et passwords sont déjà déclarés.

① Ajoute checkPassword à AdminUser. Elle doit d'abord passer par la vérification de User et, si elle réussit, vérifier aussi qu'il y a au moins 12 caractères.

② Vérifie les mots de passe dans l'ordre en tant qu'administratrice, et affiche les résultats.

③ Vérifie le deuxième mot de passe en tant qu'utilisatrice standard, et affiche le résultat.

É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 1Que se passe-t-il quand tu fais new sur un EBook qui affecte this.fileSize avant super() ?

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 ?