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 en plus de celles existantes.
Disons que nous avons la classe Animal
:
classe Animal { constructeur (nom) { cette.vitesse = 0 ; this.name = nom ; } courir (vitesse) { this.speed = vitesse ; alert(`${this.name} s'exécute à la vitesse ${this.speed}.`); } arrêt() { cette.vitesse = 0 ; alert(`${this.name} reste immobile.`); } } let animal = new Animal("Mon animal");
Voici comment nous pouvons représenter graphiquement un objet animal
et une classe Animal
:
…Et nous aimerions créer une autre class Rabbit
.
Comme les lapins sont des animaux, la classe Rabbit
doit être basée sur Animal
et avoir accès aux méthodes animales, afin 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
:
la classe Lapin étend Animal { cacher() { alert(`${this.name} se cache !`); } } let lapin = new Rabbit("Lapin Blanc"); lapin.run(5); // White Rabbit court à la vitesse 5. lapin.hide(); // Le Lapin Blanc se cache !
Les objets de la classe Rabbit
ont accès à la fois aux méthodes Rabbit
, telles que rabbit.hide()
, ainsi qu'aux méthodes Animal
, telles que rabbit.run()
.
En interne, les mots-clés extends
fonctionnent en utilisant la bonne vieille mécanique des prototypes. Il définit Rabbit.prototype.[[Prototype]]
sur Animal.prototype
. Ainsi, si une méthode n'est pas trouvée dans Rabbit.prototype
, JavaScript la prend dans 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 run
).
Son prototype, c'est Rabbit.prototype
(a hide
, mais pas run
).
Son prototype, c'est-à-dire (en raison de extends
) Animal.prototype
, qui a enfin la méthode run
.
Comme nous pouvons le rappeler dans le chapitre Prototypes natifs, JavaScript lui-même utilise l'héritage prototypique pour les objets intégrés. Par exemple, Date.prototype.[[Prototype]]
est Object.prototype
. C'est pourquoi les dates ont accès aux méthodes objets génériques.
Toute expression est autorisée après extends
La syntaxe de classe permet de spécifier non seulement une classe, mais n'importe quelle expression après extends
.
Par exemple, un appel de fonction qui génère la classe parent :
fonction f(phrase) { classe de retour { sayHi() { alert(phrase); } } ; } l'utilisateur de classe étend f("Bonjour") {} nouvel utilisateur().sayHi(); // Bonjour
Ici, class 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.
Avançons maintenant et remplaçons une méthode. Par défaut, toutes les méthodes qui ne sont pas spécifiées dans class Rabbit
sont extraites directement « telles quelles » de class Animal
.
Mais si nous spécifions notre propre méthode dans Rabbit
, comme stop()
alors elle sera utilisée à la place :
la classe Lapin étend Animal { arrêt() { // ...maintenant, cela sera utilisé pour Rabbit.stop() // au lieu de stop() de la classe Animal } }
Habituellement, cependant, nous ne souhaitons pas remplacer totalement une méthode parent, mais plutôt construire dessus pour peaufiner ou étendre ses fonctionnalités. Nous faisons quelque chose dans notre méthode, mais appelons la méthode parent avant/après ou dans le processus.
Les classes fournissent un mot-clé "super"
pour cela.
super.method(...)
pour appeler une méthode parent.
super(...)
pour appeler un constructeur parent (à l'intérieur de notre constructeur uniquement).
Par exemple, laissez notre lapin se cacher automatiquement lorsqu'il est arrêté :
classe Animal { constructeur (nom) { cette.vitesse = 0 ; this.name = nom ; } courir (vitesse) { this.speed = vitesse ; alert(`${this.name} s'exécute à la vitesse ${this.speed}.`); } arrêt() { cette.vitesse = 0 ; alert(`${this.name} reste immobile.`); } } la classe Lapin étend Animal { cacher() { alert(`${this.name} se cache !`); } arrêt() { super.stop(); // appelle l'arrêt parent this.hide(); // puis cache } } let lapin = new Rabbit("Lapin Blanc"); lapin.run(5); // White Rabbit court à la vitesse 5. lapin.stop(); // Le Lapin Blanc reste immobile. Le Lapin Blanc se cache !
Rabbit
dispose désormais de la méthode stop
qui appelle le parent super.stop()
dans le processus.
Les fonctions fléchées n'ont pas super
Comme cela a été mentionné dans le chapitre Fonctions fléchées revisitées, les fonctions fléchées n'ont pas super
.
En cas d'accès, il provient de la fonction externe. Par exemple:
la classe Lapin étend Animal { arrêt() { setTimeout(() => super.stop(), 1000); // appelle le parent stop après 1 seconde } }
Le super
dans la fonction flèche est le même que dans stop()
, il fonctionne donc comme prévu. Si nous spécifions ici une fonction « normale », il y aurait une erreur :
// Super inattendu setTimeout(function() { super.stop() }, 1000);
Avec les constructeurs, cela devient un peu délicat.
Jusqu'à présent, Rabbit
ne disposait pas de son propre constructor
.
Selon la spécification, si une classe étend une autre classe et n'a pas constructor
, alors le constructor
« vide » suivant est généré :
la classe Lapin étend Animal { // généré pour étendre les classes sans propres constructeurs constructeur(...arguments) { super(...arguments); } }
Comme nous pouvons le voir, il appelle essentiellement le constructor
parent en lui transmettant tous les arguments. Cela se produit si nous n'écrivons pas notre propre constructeur.
Ajoutons maintenant un constructeur personnalisé à Rabbit
. Il précisera le earLength
en plus name
:
classe Animal { constructeur (nom) { cette.vitesse = 0 ; this.name = nom ; } //... } la classe Lapin étend Animal { constructeur (nom, longueur d'oreille) { cette.vitesse = 0 ; this.name = nom ; this.earLength = earLength; } //... } // Ça ne marche pas ! let lapin = new Lapin("Lapin Blanc", 10); // Erreur : ceci n'est pas défini.
Oups ! Nous avons une erreur. Désormais, nous ne pouvons plus créer de lapins. Qu'est-ce qui n'a pas fonctionné ?
La réponse courte est :
Les constructeurs des classes héritières doivent appeler super(...)
et (!) le faire avant d'utiliser this
.
…Mais pourquoi ? Que se passe-t-il ici ? En effet, cette 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 les autres fonctions. Un constructeur dérivé possède une propriété interne spéciale [[ConstructorKind]]:"derived"
. C'est une étiquette interne spéciale.
Cette étiquette affecte son comportement avec new
.
Lorsqu'une fonction régulière 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
afin d'exécuter son constructeur parent (de base), sinon l'objet 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 :
classe Animal { constructeur (nom) { cette.vitesse = 0 ; this.name = nom ; } //... } la classe Lapin étend Animal { constructeur (nom, longueur d'oreille) { super(nom); this.earLength = earLength; } //... } // maintenant ça va let lapin = new Lapin("Lapin Blanc", 10); alerte(lapin.nom); // Lapin Blanc alerte(lapin.earLength); // 10
Remarque avancée
Cette note suppose que vous avez une certaine expérience des cours, peut-être dans d'autres langages de programmation.
Il permet de mieux comprendre le langage et explique également les comportements qui peuvent être source de bugs (mais pas très souvent).
Si vous avez du mal à comprendre, continuez, continuez à lire, puis revenez-y quelque temps 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 remplacé dans le constructeur parent, assez différent de la plupart des autres langages de programmation.
Considérez cet exemple :
classe Animal { nom = 'animal'; constructeur() { alert(ce.nom); // (*) } } la classe Lapin étend Animal { nom = 'lapin'; } nouvel Animal(); // animal nouveau Lapin(); // 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 (*)
affiche animal
.
En d’autres termes, le constructeur parent utilise toujours sa propre valeur de champ, et non celle remplacée.
Qu'y a-t-il de bizarre là-dedans ?
Si ce n'est pas encore clair, veuillez comparer avec les méthodes.
Voici le même code, mais au lieu du champ this.name
nous appelons la méthode this.showName()
:
classe Animal { showName() { // au lieu de this.name = 'animal' alerte('animal'); } constructeur() { this.showName(); // au lieu d'alerte(this.name); } } la classe Lapin étend Animal { showName() { alert('lapin'); } } nouvel Animal(); // animal nouveau Lapin(); // lapin
Attention : le résultat est désormais différent.
Et c'est ce à quoi nous nous attendons naturellement. Lorsque le constructeur parent est appelé dans la classe dérivée, il utilise la méthode substituée.
… Mais pour les champs de classe, ce n'est pas le cas. Comme indiqué, le constructeur parent utilise toujours le champ parent.
Pourquoi y a-t-il une différence ?
Eh bien, la raison est l'ordre d'initialisation du champ. Le champ classe est initialisé :
Avant le constructeur de la classe de base (qui n'étend rien),
Immédiatement après super()
pour la classe dérivée.
Dans notre cas, Rabbit
est la classe dérivée. Il n'y a pas constructor()
dedans. Comme dit précédemment, c'est la même chose que s'il y avait un constructeur vide avec seulement super(...args)
.
Ainsi, new Rabbit()
appelle super()
, exécutant ainsi le constructeur parent, et (selon la règle des classes dérivées) seulement après que ses champs de classe soient initialisés. Au moment de l'exécution du constructeur parent, il n'y a pas encore de champs de classe Rabbit
, c'est pourquoi les champs Animal
sont utilisés.
Cette différence subtile entre les champs et les méthodes est spécifique à JavaScript.
Heureusement, ce comportement ne se révèle que si un champ remplacé est utilisé dans le constructeur parent. Il peut alors être difficile de comprendre ce qui se passe, c'est pourquoi nous l'expliquons ici.
Si cela devient un problème, on peut le résoudre en utilisant des méthodes ou des getters/setters au lieu de champs.
Informations avancées
Si vous lisez le didacticiel pour la première fois, cette section peut être ignorée.
Il s'agit des mécanismes internes derrière l'héritage et super
.
Allons un peu plus loin sous le capot de super
. Nous verrons des choses intéressantes en cours de route.
Tout d’abord, d’après tout ce que nous avons appris jusqu’à présent, il est impossible pour super
de fonctionner !
Ouais, en effet, demandons-nous, comment cela devrait-il fonctionner techniquement ? Lorsqu'une méthode objet s'exécute, elle obtient l'objet actuel sous la forme this
. Si nous appelons alors super.method()
, le moteur doit obtenir la method
du prototype de l'objet actuel. Mais comment ?
La tâche peut paraître simple, mais elle ne l’est pas. Le moteur connaît l'objet actuel this
, il pourrait donc obtenir la method
parent sous la forme this.__proto__.method
. Malheureusement, une solution aussi « naïve » ne fonctionnera pas.
Montrons le problème. Sans classes, en utilisant des objets simples par souci de simplicité.
Vous pouvez ignorer cette partie et accéder ci-dessous à la sous-section [[HomeObject]]
si vous ne souhaitez pas connaître les détails. Cela ne fera pas de mal. Ou poursuivez votre lecture si vous souhaitez comprendre les choses en profondeur.
Dans l'exemple ci-dessous, rabbit.__proto__ = animal
. Essayons maintenant : dans rabbit.eat()
nous appellerons animal.eat()
, en utilisant this.__proto__
:
soit animal = { nom: "Animal", manger() { alert(`${this.name} mange.`); } } ; laissez lapin = { __proto__ : animal, nom : "Lapin", manger() { // c'est ainsi que super.eat() pourrait probablement fonctionner this.__proto__.eat.call(this); // (*) } } ; lapin.eat(); // Le lapin mange.
À la ligne (*)
nous prenons eat
du prototype ( animal
) et l'appelons dans le contexte de l'objet actuel. Veuillez noter que .call(this)
est important ici, car un simple this.__proto__.eat()
exécuterait parent eat
dans le contexte du prototype, pas de l'objet actuel.
Et dans le code ci-dessus, cela fonctionne comme prévu : nous avons la bonne alert
.
Ajoutons maintenant un objet supplémentaire à la chaîne. Nous verrons comment les choses se passent :
soit animal = { nom: "Animal", manger() { alert(`${this.name} mange.`); } } ; laissez lapin = { __proto__ : animal, manger() { // ... rebondit à la manière d'un lapin et appelle la méthode parent (animal) this.__proto__.eat.call(this); // (*) } } ; soit longEar = { __proto__ : lapin, manger() { // ...faites quelque chose avec de longues oreilles et appelez la méthode parent (lapin) this.__proto__.eat.call(this); // (**) } } ; longEar.manger(); // Erreur : taille maximale de la pile d'appels dépassée
Le code ne fonctionne plus ! Nous pouvons voir l'erreur en essayant d'appeler longEar.eat()
.
Ce n'est peut-être pas si évident, mais si nous retraçons l'appel à longEar.eat()
, nous pouvons comprendre pourquoi. Dans les deux lignes (*)
et (**)
la valeur de this
est l'objet actuel ( longEar
). C'est essentiel : toutes les méthodes objet obtiennent l'objet actuel sous la forme this
, pas un prototype ou quelque chose du genre.
Ainsi, dans les deux lignes (*)
et (**)
la valeur de this.__proto__
est exactement la même : rabbit
. Ils appellent tous les deux rabbit.eat
sans remonter la chaîne dans la boucle sans fin.
Voici l'image de ce qui se passe :
À l'intérieur longEar.eat()
, la ligne (**)
appelle rabbit.eat
en lui fournissant this=longEar
.
// à l'intérieur de longEar.eat() nous avons ceci = longEar ceci.__proto__.eat.call(this) // (**) // devient longEar.__proto__.eat.call(this) // c'est lapin.eat.call(this);
Ensuite, dans la ligne (*)
de rabbit.eat
, nous aimerions passer l'appel encore plus haut dans la chaîne, mais this=longEar
, donc this.__proto__.eat
est à nouveau rabbit.eat
!
// dans Rabbit.eat() nous avons aussi ceci = longEar ceci.__proto__.eat.call(this) // (*) // devient longEar.__proto__.eat.call(this) // ou (encore) lapin.eat.call(this);
…Alors rabbit.eat
s'appelle dans la boucle sans fin, car il ne peut plus monter.
Le problème ne peut pas être résolu en utilisant this
seul.
[[HomeObject]]
Pour fournir la solution, JavaScript ajoute une propriété interne spéciale supplémentaire pour les fonctions : [[HomeObject]]
.
Lorsqu'une fonction est spécifiée en tant que classe ou méthode objet, sa propriété [[HomeObject]]
devient cet objet.
Ensuite, super
l'utilise pour résoudre le prototype parent et ses méthodes.
Voyons comment cela fonctionne, d'abord avec des objets simples :
soit animal = { nom: "Animal", manger() { // animal.eat.[[HomeObject]] == animal alert(`${this.name} mange.`); } } ; laissez lapin = { __proto__ : animal, nom : "Lapin", manger() { // lapin.eat.[[HomeObject]] == lapin super.manger(); } } ; soit longEar = { __proto__ : lapin, nom: "Longue Oreille", manger() { // longEar.eat.[[HomeObject]] == longEar super.manger(); } } ; // fonctionne correctement longEar.manger(); // Longue Oreille mange.
Cela fonctionne comme prévu, grâce à la mécanique [[HomeObject]]
. Une méthode, telle que longEar.eat
, connaît son [[HomeObject]]
et prend la méthode parent de son prototype. Sans aucune utilisation de this
.
Comme nous le savons auparavant, les fonctions sont généralement « gratuites », non liées à des objets en JavaScript. Ils peuvent donc être copiés entre objets et appelés avec un autre this
.
L'existence même de [[HomeObject]]
viole ce principe, car les méthodes mémorisent leurs objets. [[HomeObject]]
ne peut pas être modifié, ce lien est donc éternel.
Le seul endroit dans le langage où [[HomeObject]]
est utilisé – est super
. Ainsi, si une méthode n’utilise pas super
, nous pouvons toujours la considérer comme gratuite et la copier entre objets. Mais avec super
les choses peuvent mal tourner.
Voici la démo d'un super
résultat erroné après copie :
soit animal = { disBonjour() { alert(`Je suis un animal`); } } ; // le lapin hérite d'un animal laissez lapin = { __proto__ : animal, disBonjour() { super.sayHi(); } } ; laissez planter = { disBonjour() { alert("Je suis une plante"); } } ; // l'arbre hérite de la plante soit arbre = { __proto__ : plante, sayHi: lapin.sayHi // (*) } ; arbre.sayHi(); // Je suis un animal (?!?)
Un appel à tree.sayHi()
affiche « Je suis un animal ». Certainement faux.
La raison est simple :
Dans la ligne (*)
, la méthode tree.sayHi
a été copiée depuis rabbit
. Peut-être voulions-nous simplement éviter la duplication de code ?
Son [[HomeObject]]
est rabbit
, tel qu'il a été créé dans rabbit
. Il n'y a aucun moyen de changer [[HomeObject]]
.
Le code de tree.sayHi()
contient super.sayHi()
à l'intérieur. Il remonte du rabbit
et reprend la méthode de animal
.
Voici le schéma de ce qui se passe :
[[HomeObject]]
est défini pour les méthodes à la fois dans les classes et dans les objets simples. Mais pour les objets, les méthodes doivent être spécifiées exactement comme method()
, et non comme "method: function()"
.
La différence n'est peut-être pas essentielle pour nous, mais elle est importante pour JavaScript.
Dans l'exemple ci-dessous, une syntaxe non-méthode est utilisée à des fins de comparaison. La propriété [[HomeObject]]
n'est pas définie et l'héritage ne fonctionne pas :
soit animal = { eat: function() { // écrire intentionnellement comme ça au lieu de eat() {... //... } } ; laissez lapin = { __proto__ : animal, manger : fonction() { super.manger(); } } ; lapin.eat(); // Erreur lors de l'appel de super (car il n'y a pas de [[HomeObject]])
Pour étendre une classe : class Child extends Parent
:
Cela signifie que Child.prototype.__proto__
sera Parent.prototype
, donc les méthodes sont héritées.
Lors du remplacement d'un constructeur :
Nous devons appeler le constructeur parent comme super()
dans le constructeur Child
avant de this
utiliser.
Lors du remplacement d'une autre méthode :
Nous pouvons utiliser super.method()
dans une méthode Child
pour appeler la méthode Parent
.
Internes :
Les méthodes mémorisent leur classe/objet dans la propriété interne [[HomeObject]]
. C'est ainsi que super
résout les méthodes parentes.
Il n'est donc pas prudent de copier une méthode avec super
d'un objet à un autre.
Aussi:
Les fonctions fléchées n'ont pas leur propre this
ou super
, elles s'intègrent donc de manière transparente dans le contexte environnant.
importance : 5
Voici le code avec Rabbit
étendant Animal
.
Malheureusement, les objets Rabbit
ne peuvent pas être créés. Qu'est-ce qui ne va pas? Réparez-le.
classe Animal { constructeur (nom) { this.name = nom ; } } la classe Lapin étend Animal { constructeur (nom) { this.name = nom ; this.created = Date.now(); } } let lapin = new Rabbit("Lapin Blanc"); // Erreur : ceci n'est pas défini alerte(lapin.nom);
C'est parce que le constructeur enfant doit appeler super()
.
Voici le code corrigé :
classe Animal { constructeur (nom) { this.name = nom ; } } la classe Lapin étend Animal { constructeur (nom) { super(nom); this.created = Date.now(); } } let lapin = new Rabbit("Lapin Blanc"); // ok maintenant alerte(lapin.nom); // Lapin Blanc
importance : 5
Nous avons une classe Clock
. Désormais, il imprime l’heure toutes les secondes.
horloge de classe { constructeur ({ modèle }) { this.template = modèle ; } rendre() { let date = new Date(); let hours = date.getHours(); si (heures < 10) heures = '0' + heures ; let mins = date.getMinutes(); si (mins < 10) mins = '0' + min ; let secs = date.getSeconds(); si (secs < 10) secs = '0' + secs ; laisser la sortie = this.template .replace('h', heures) .replace('m', minutes) .replace('s', secondes); console.log(sortie); } arrêt() { clearInterval(this.timer); } commencer() { this.render(); this.timer = setInterval(() => this.render(), 1000); } }
Créez une nouvelle classe ExtendedClock
qui hérite de Clock
et ajoute la precision
du paramètre – le nombre de ms
entre les « ticks ». Doit être 1000
(1 seconde) par défaut.
Votre code doit être dans le fichier extended-clock.js
Ne modifiez pas le clock.js
d'origine. Prolongez-le.
Ouvrez un bac à sable pour la tâche.
la classe ExtendedClock étend Clock { constructeur (options) { super(options); laissez { précision = 1000 } = options ; this.precision = précision ; } commencer() { this.render(); this.timer = setInterval(() => this.render(), this.precision); } } ;
Ouvrez la solution dans un bac à sable.