L’héritage de classe est un moyen pour une classe d’étendre une autre classe.
Nous pouvons donc créer de nouvelles fonctionnalités à partir de celles qui existent déjà.
Le mot-clé “extends”
Supposons que nous ayons la classe Animal :
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
let animal = new Animal("My animal");
Voici comment nous pouvons représenter graphiquement l’objet animal et la classe Animal :
… Et nous aimerions créer une autre class Rabbit.
Comme les lapins sont des animaux, la classe Rabbit devrait être basé sur Animal, avoir accès à des méthodes animales, de sorte que les lapins puissent faire ce que les animaux “génériques” peuvent faire.
La syntaxe pour étendre une autre classe est la suivante : class Child extends Parent.
Créons class Rabbit qui hérite de Animal :
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit court à la vitesse 5.
rabbit.hide(); // White Rabbit se cache!
L’objet de la classe Rabbit a accès à la fois aux méthodes Rabbit, telles que rabbit.hide(), et aux méthodes Animal, telles que rabbit.run().
En interne, le mot clé extended fonctionne en utilisant le bon vieux prototype. Il établit Rabbit.prototype.[[Prototype]] vers Animal.prototype. Donc, si une méthode n’est pas trouvée dans Rabbit.prototype, JavaScript le prend de Animal.prototype.
Par exemple, pour trouver la méthode rabbit.run, le moteur vérifie (de bas en haut sur l’image) :
- L’objet
rabbit(n’a pas derun). - Son prototype, c’est-à-dire
Rabbit.prototype(ahide, mais pasrun). - Son prototype, c’est-à-dire (en raison de
extends)Animal.prototype, qui a finalement la méthoderun.
Comme nous pouvons nous en rappeler dans le chapitre Prototypes natifs, JavaScript lui-même utilise l’héritage prototypale pour les objets intégrés. Exemple : Date.prototype.[[Prototype]] est Object.prototype. C’est pourquoi les dates ont accès aux méthodes d’objet génériques.
extendsLa syntaxe de classe permet de spécifier non seulement une classe, mais toute expression après extends.
Par exemple, un appel de fonction qui génère la classe parent :
function f(phrase) {
return class {
sayHi() { alert(phrase); }
};
}
class User extends f("Hello") {}
new User().sayHi(); // Hello
Ici la classe user hérite du résultat de f("Hello").
Cela peut être utile pour les modèles de programmation avancés lorsque nous utilisons des fonctions pour générer des classes en fonction de nombreuses conditions et que nous pouvons en hériter.
Remplacer une méthode
Maintenant, avançons et substituons une méthode. Par défaut, toutes les méthodes qui ne sont pas spécifiées dans class Rabbit sont prises directement “telles quelles” dans class Animal.
Mais si nous spécifions notre propre méthode dans Rabbit, telle que stop(), elle sera utilisée à la place :
class Rabbit extends Animal {
stop() {
// ...maintenant ceci sera utilisé pour rabbit.stop()
// au lieu de stop() de la classe Animal
}
}
Mais en général, nous ne voulons pas remplacer totalement une méthode parente, mais plutôt construire dessus, modifier ou étendre ses fonctionnalités. Nous faisons quelque chose dans notre méthode, mais appelons la méthode parente avant / après ou dans le processus.
Les classes fournissent le mot clé "super" pour cela.
super.method(...)pour appeler une méthode parente.super(...)pour appeler un constructeur parent (dans notre constructeur uniquement).
Par exemple, laissons rabbit se cacher automatiquement à l’arrêt :
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
stop() {
super.stop(); // appeler stop du parent
this.hide(); // puis hide
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit court à la vitesse 5.
rabbit.stop(); // White Rabbit reste immobile. White Rabbit se cache!
Maintenant, Rabbit a la méthode stop qui appelle le super.stop() du parent dans le processus.
superComme mentionné dans le chapitre Les fonctions fléchées revisitées, les fonctions fléchées n’ont pas super.
Si on y accède, c’est tiré de la fonction externe. Par exemple :
class Rabbit extends Animal {
stop() {
setTimeout(() => super.stop(), 1000); // appel stop du parent après 1sec
}
}
Le super dans la fonction fléchée est le même que dans stop(), donc cela fonctionne comme prévu. Si nous spécifions ici une fonction “régulière”, il y aurait une erreur :
// Super inattendu
setTimeout(function() { super.stop() }, 1000);
Remplacement du constructeur
Avec les constructeurs, cela devient un peu délicat.
Jusqu’à maintenant, Rabbit n’avait pas son propre constructor.
Selon la spécification, si une classe étend une autre classe et n’a pas de constructor, alors le constructor “vide” suivant est généré :
class Rabbit extends Animal {
// généré pour l'extension de classes sans constructeur propre
constructor(...args) {
super(...args);
}
}
Comme nous pouvons le constater, il appelle essentiellement le constructor du parent en lui passant tous les arguments. Cela se produit si nous n’écrivons pas notre propre constructeur.
Ajoutons maintenant un constructeur personnalisé à Rabbit. Il spécifiera le earLength en plus de name :
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
this.speed = 0;
this.name = name;
this.earLength = earLength;
}
// ...
}
// Ça ne marche pas!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
Oups ! Nous avons une erreur. Maintenant, nous ne pouvons pas créer de lapins. Qu’est-ce qui s’est passé ?
La réponse courte est :
- les constructeurs dans les classes qui héritent doivent appeler
super(...), et (!) le faire avant d’utiliserthis.
…Mais pourquoi ? Que se passe t-il ici ? En effet, l’exigence semble étrange.
Bien sûr, il y a une explication. Entrons dans les détails pour que vous compreniez vraiment ce qui se passe.
En JavaScript, il existe une distinction entre une fonction constructeur d’une classe héritante (appelée “constructeur dérivé”) et d’autres fonctions. Un constructeur dérivé a une propriété interne spéciale [[ConstructorKind]] : "derived". C’est un label interne spécial.
Ce label affecte son comportement avec new.
- Lorsqu’une fonction normale est exécutée avec
new, elle crée un objet vide et l’assigne àthis. - Mais lorsqu’un constructeur dérivé s’exécute, il ne le fait pas. Il s’attend à ce que le constructeur parent fasse ce travail.
Ainsi, un constructeur dérivé doit appeler super pour exécuter son constructeur parent (de base), sinon l’objet pour this ne sera pas créé. Et nous aurons une erreur.
Pour que le constructeur Rabbit fonctionne, il doit appeler super() avant d’utiliser this, comme ici :
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
this.earLength = earLength;
}
// ...
}
// maintenant ça marche
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10
Remplacer les champs de classe : une note délicate
Cette note suppose que vous avez une certaine expérience avec les classes, peut-être dans d’autres langages de programmation.
Cela donne un meilleur aperçu du langage et explique également le comportement qui pourrait être une source de bogues (mais pas très souvent).
Si vous avez du mal à comprendre, continuez, poursuivez votre lecture et revenez-y un peu plus tard.
Nous pouvons remplacer non seulement les méthodes, mais également les champs de classe.
Cependant, il existe un comportement délicat lorsque nous accédons à un champ surchargé dans le constructeur parent, assez différent de la plupart des autres langages de programmation.
Prenons cet exemple :
class Animal {
name = 'animal';
constructor() {
alert(this.name); // (*)
}
}
class Rabbit extends Animal {
name = 'rabbit';
}
new Animal(); // animal
new Rabbit(); // animal
Ici, la classe Rabbit étend Animal et remplace le champ name par sa propre valeur.
Il n’y a pas de constructeur propre dans Rabbit, donc le constructeur Animal est appelé.
Ce qui est intéressant, c’est que dans les deux cas : new Animal() et new Rabbit(), l’alert dans la ligne (*) montre animal.