Dernière mise à jour :

Notions d'héritage en JavaScript

Problématique

Il n'existe pas en EcmaScript5 le support d'un système de classes et d'héritage tel que connu dans des langages comme C#, Java ou PHP. Lorsque l'on doit créer de façon programmatique des objets on utilise souvent une fonction de construction. L'exécution de cette fonction au travers de l'utilisation du mot-clef new permet la création et l'initialisation d'objets.

Mais lorsque l'on souhaite créer de multiples objets avec des caractéristiques communes entre eux, cette méthode prend non seulement du temps mais elle est également très consommatrice en termes de mémoire. En effet chaque objet est distinct des autres objets créés ce qui signifie que la mémoire utilisée pour contenir la définition de la fonction n'est pas partagée entre toutes les instances.

Une alternative au système de classe, le prototype

Toutefois il existe en EcmaScript un mécanisme qui supplée l'absence de classes et permet une forme d'héritage qui, sous certains aspects, est plus poussée qu'avec d'autres langages. Ce mécanisme repose sur la propriété prototype, présente dans tout objet EcmaScript5, et sur le chaînage des prototypes.

En EcmaScript5 un objet est une collection de propriétés. Tout objet possède un unique objet prototype. Cet objet prototype peut être déclaré explicitement (nous verrons comment) ou exister de façon implicite, dans ce cas la propriété __proto__ (qui identifie le prototype de l'objet) renvoie à Object.prototype, qui lui aussi possède une propriété __proto__ dont la valeur est null.

L'objet prototype est, par définition, un objet (du type Object) dont le rôle est de fournir des propriétés qui seront partagées avec d'autres objets. La règle du chaînage de prototypes est simple : si une propriété ou une méthode n'est pas trouvée dans l'objet lui-même, la propriété/méthode sera recherchée dans la  chaîne de prototype. Si la propriété n'est pas trouvée dans le prototype, alors une recherche aura lieu dans le prototype du prototype et ainsi de suite jusqu'au sommet de la chaîne de prototypes (ce procédé est le même pour l'héritage basé sur les classes, lors de la résolution d'une méthode héritée, il y a un parcours de la chaîne des classes). La première propriété/méthode trouvée avec le même nom sera utilisée. Une propriété trouvée est appelée propriété héritée. Si la propriété n'est pas trouvée après le parcours intégral de la chaîne de prototype, la valeur  undefined sera retournée.

Objets créés avec des éléments syntaxiques

Quand un objet est créé avec des éléments syntaxiques sa définition est héritée de Object.prototype. Le prototype de l’objet est référencé par la propriété interne [[Prototype]] que l'on peut exprimer par la propriété __proto__. On peut s'appuyer sur cette propriété __proto__ pour reproduire un mécanisme d'héritage.
Voici une illustration :

 Exemple 1

//L'objet classe mère

var a = {

x : 10,

calculate : function(z){

return this.x + this.y + z

}

};

var b = {

y : 20

__proto__ : a

};

var c = {

y : 30,

__proto__ : a

};

// appel de la méthode héritée

console.info(b.calculate(30)); // 60

console.info(c.calculate(40)); // 80

console.info(b.x); // 10

On comprend ici très bien le principe de chaînage de prototype. Tout d'abord il faut noter que lors de la définition des objets b et c, la valeur passée à la propriété __proto__ est l'objet a. Les propriétés et méthodes qui ne sont pas trouvées dans les définitions des objets b et c sont recherchées dans leur objet prototype donc dans l'objet a.

Note : Si des propriétés ou méthodes n'étaient pas trouvées dans l'objet a, le programme remonterait dans son prototype soit dans l'objet natif Object puis dans Object.prototype dont la valeur finale est null.

Objets créés avec un constructeur

Lorsque l'on crée un objet via un constructeur et l'expression 'new', ce nouvel objet va référencer implicitement la propriété prototype du constructeur dans le but de résoudre les références aux propriétés (comme le constructeur est un objet function il possède également un objet prototype utilisé pour l'héritage et le partage de propriétés).

Ainsi tous les objets créés à partir du constructeur ont une référence implicite à la valeur de la propriété 'prototype' du constructeur. La propriété 'prototype' du constructeur peut être référencée par l'expression nomConstructeur.prototype. Toutes les propriétés ajoutées à l'objet prototype du constructeur sont partagées par héritage par tous les objets référençant ce prototype.
Voici une illustration :

Créons notre constructeur générique 'Plante' et ajoutons des fonctions à son objet prototype,

 Exemple 2

var Plante = function(){};

Plante.prototype.arroser = function(){

console.info("Verser de l'eau me fait grandir");

}

Plante.prototype.fletrir = function(){

console.info("C'est la fin de ma vie...");

}

puis notre constructeur 'Fleur' dont le prototype hérite du prototype de 'Plante',

var Fleur = function(){};

Fleur.prototype = Plante.prototype;

Fleur.prototype.fleurir = function(){

console.info("Mes pétales s'ouvrent au soleil");

}

l'instance 'tulipe' du constructeur Fleur possède sa méthode propre et hérite de celles de l'objet prototype de Plante.

var tulipe = new Fleur();

tulipe.arroser(); // Verser de l'eau me fait grandir

tulipe.fleurir(); // Mes pétales s'ouvrent au soleil

tulipe.fletrir(); // C'est la fin de ma vie...

Objets créés par la méthode Object.create()

Cette nouvelle syntaxe a été introduite dans la version 5 du langage ECMAScript. Elle s'utilise de la façon suivante :

Object.create(prototype [, propertiesObject ] )

La syntaxe create permet la création d'un nouvel objet à partir d'un objet 'prototype' donné. Dans cette syntaxe il est possible également de préciser un argument propertiesObject décrivant des propriétés additionnelles pour l'objet nouveau. Ces propriétés portent un nom (leur dénomination est « named data property » dans la spécification ECMAScript5), ont un contenu et sont associés avec des attributs définis :

Nom Type Description
Value Tout type ECMAScript La valeur retournée à la lecture de la propriété
Writable Booléen Si false la valeur ne peut être changée
Enumerable Booléen Si true la propriété pourra être listée par une énumération 'for in'
Configurable Booléen Si false seule la valeur de Value pourra être modifiée et non les autres (suppression, changement de type, etc.)

On voit que par cette méthode il est possible de préciser explicitement un prototype pour un nouvel objet.
Exemple :

On commence par définir le prototype, ici un objet littéral.

 Exemple 3

var chateau = {

construire : function(){

console.info("Ma construction est terminée");

}

};

Ensuite création du nouvel objet par la méthode Object.create().

var versailles = Object.create(chateau,

{

nom : {value:"Versailles", writable : false}

}

);

versailles.construire(); // Ma construction est terminée

versailles.nom = "Vincennes";

console.info(versailles.nom); // Affiche Versailles car la valeur de nom n'est pas modifiable (writable:false)

L'objet 'versailles' hérite de la méthode construire() de 'château', son prototype. On lui a donné à son initialisation la propriété nom avec les attributs value et writable ; la valeur false de writable en empêche la modification.

Pour aller plus loin

ECMAScript propose un système original permettant l'héritage entre classes. Ce système est puissant car il permet de gérer plus facilement l'héritage multiple. En effet il est possible d'enrichir un objet, via sa propriété 'prototype', avec des éléments de plusieurs autres objets. Ce mécanisme est largement utilisé par les frameworks comme ExtJS ou JQuery. Je vous invite à vous intéresser à la façon dont ils le font afin de bien vous approprier ces notions d'héritage en Javascript.

Sources et liens

(Les sources sur GitHub >>)

Vous trouverez après un liste de liens vers des sites traitant de Javascript qui m'ont servi à la rédaction de cet article.