Dans la plupart des langages, hériter signifie recevoir une copie du plan de la classe parente. JavaScript fait autre chose : il relie un objet à un autre et lui laisse poser ses questions.
La différence n'est pas cosmétique. Elle explique pourquoi une méthode ajoutée après coup devient disponible sur des objets créés bien avant, et pourquoi class n'a jamais introduit de vrai système de classes.
Définition
L'héritage prototypal consiste à donner à un objet un Prototype, c'est-à-dire un autre objet auquel il délègue les propriétés qu'il ne possède pas. Rien n'est recopié : la recherche se fait à chaque lecture, en remontant la chaîne.
const animal = {
presenter() {
return "Je suis " + this.nom;
},
};
const chien = Object.create(animal);
chien.nom = "Rex";
console.log(chien.presenter()); // Je suis Rex
console.log(chien.hasOwnProperty("presenter")); // false
console.log(animal.isPrototypeOf(chien)); // truechien ne possède que nom. La méthode vient d'animal, et this y désigne malgré tout chien, parce que sa valeur dépend de l'objet sur lequel l'appel est écrit.
Les classes écrivent exactement la même chose
class et extends sont une syntaxe posée par-dessus ce mécanisme. Le résultat est la même chaîne d'objets.
class Animal {
constructor(nom) { this.nom = nom; }
presenter() { return "Je suis " + this.nom; }
}
class Chien extends Animal {
presenter() { return super.presenter() + ", un chien"; }
}
const rex = new Chien("Rex");
console.log(rex.presenter()); // Je suis Rex, un chien
console.log(Object.getPrototypeOf(Chien.prototype) === Animal.prototype); // true
console.log(rex instanceof Animal); // trueLe super de la méthode redéfinie ne fait rien de magique : il appelle la version trouvée un cran plus haut dans la chaîne.
Trois pièges à connaître
- Un objet partagé sur le prototype est partagé pour de bon. Un tableau placé là est le même pour toutes les instances, et une modification par l'une le change pour toutes.
- Une propriété écrite ne remonte jamais. Affecter
chien.presentercrée une propriété propre qui masque celle du prototype, sans toucher à l'original. - Les chaînes profondes coûtent en lisibilité. Trois niveaux d'héritage et il devient difficile de dire d'où vient une méthode. La composition est souvent préférable.
Pour partager du comportement sans construire une hiérarchie, Object.assign() copie des méthodes directement sur un objet. Le résultat n'est pas de l'héritage, mais il répond au même besoin dans bien des cas.
Questions fréquentes
Faut-il préférer Object.create() ou class ?
Les deux produisent la même structure, donc le choix est une question de lisibilité et d'équipe. class est aujourd'hui la forme attendue dans la plupart des projets, et les outils la comprennent mieux. Object.create() reste précieux pour composer des objets sans constructeur, ou pour créer un objet sans prototype du tout.
Peut-on hériter de plusieurs objets à la fois ?
Non, la chaîne est linéaire : un objet a exactement un prototype. Pour combiner plusieurs comportements, la pratique courante est le mixin, un objet de méthodes recopié sur le prototype cible avec Object.assign(). Ce n'est pas de l'héritage multiple, mais cela couvre le même besoin sans ambiguïté de résolution.
Pourquoi mes instances partagent-elles un tableau que je croyais propre ?
Parce qu'il a été déclaré sur le prototype et non dans le constructeur. Une valeur primitive donne l'illusion de fonctionner, puisque l'écriture crée une propriété propre. Un tableau ou un Object (objet), lui, est lu puis modifié sur place, donc partagé. Initialisez toujours ces valeurs dans le constructeur.