Un peu de classe : l’héritage en Javascript

Par Mark Dalgleish

Les fonctions orientées objet de Javascript, telles que les constructeurs et les chaînes de prototypes sont probablement les concepts les moins bien compris de ce langage. Même parmi ceux qui écrivent du JavaScript à longueur de journée, ces concepts restent compliqués. Je suppose que c'est dû en grande partie à la ressemblance avec la syntaxe de Java.

Mais cette situation n’est pas une fatalité. Derrière le mot-clé new se cache un modèle objet élégant et riche, prenons donc le temps de nous familiariser avec celui-ci.

Voyons comme le modèle d’héritage peut être simple en JavaScript si on utilise sa syntaxe moderne, ce qui nous permettra de mieux comprendre la syntaxe malheureuse que nous subissons depuis sa création.

Javascript n’a pas de classes

La plus grande incompréhension de JavaScript vient du fait qu’on pourrait penser qu’il a un modèle de classe. Quand un nouveau venu aborde JavaScript, et en particulier lorsqu’il connaît des langages basés sur les classes, il peut être surpris de trouver ce type de code :

var charlie = new Acteur();

JavaScript laisse croire qu’il dispose de classes. Quand quelqu’un aborde ce langage, et surtout s’il vient d’un langage à classes, il a tendance à s’imaginer en voyant ce code que quelque part se trouve la définition de la classe « Acteur ». Quelle ne sera pas sa surprise en trouvant ce genre de code :

function Acteur(nom) {
  this.nom = nom;
}

Acteur.prototype.joue = function(tirade) {
  alert(this.nom + ': ' + tirade);
};

Les choses se compliquent encore plus lorsque l’héritage multiple entre en scène :

function Acteur(nom) {
  this.nom = nom;
}
Acteur.prototype.peutParler = true;

function ActeurMuet() {
  Acteur.apply(this, arguments);
}
ActeurMuet.prototype = new Acteur();
ActeurMuet.prototype.peutParler = false;

Certaines personnes ont beau avoir des années d’expérience et savoir que JavaScript n’a pas de classes, elles ne parviennent toujours pas à comprendre ce qui se cache derrière une syntaxe aussi obscure.

Un peu de recul

Pour remettre les choix dans leur contexte historique, revenons en 1995. Java est le nouveau langage à la mode sur Internet, et les applets sont sur le point de conquérir le monde.

Nous connaissons le fin mot de l’histoire aujourd’hui, mais tel était l’environnement dans lequel Brendan Eich évoluait chez Netscape quand il travaillait sur son « petit » langage de script pour le web.

Cette pression culturelle a fini par influencer le nom du langage. Ce qui fut aux origines « Mocha », puis « Live Script », est finalement devenu « JavaScript ». Alors que JavaScript n’avait pas vraiment le même champ d’application, Brendan Eich lui-même, ne rechignait pas à le présenter comme le petit frère de Java.

Ce n’est qu’un prototype

Le problème, bien sûr, est que malgré ses similarités syntaxiques avec Java, ses racines conceptuelles viennent d’ailleurs. Son typage dynamique est emprunté à Scheme et son héritage par prototype inspiré de Self.

Dans les langages de programmation classiques, comme Java, les instances sont créées sur le modèle de classes. JavaScript s’en écarte en ce qu’il propose un modèle d’héritage par prototype dans lequel il n’y a pas de classes.

Au lieu de cela, les objets héritent d’autres objets.

Héritage moderne

Pour mieux comprendre les concepts sous-jacents de l’héritage par prototype, vous devrez d’abord oublier ce que vous avez appris. Vous apprécierez plus facilement l’héritage d’objet à objet de JavaScript si vous faites comme si vous n’aviez jamais rencontré la syntaxe historique (et trompeuse).

ECMAScript 5, qui est la version la plus récente de JavaScript et que l’on retrouve dans les navigateurs modernes (Chrome, Firefox, Safari, Opera, IE9+), propose une nouvelle syntaxe pour créer des objets héritant d’autres objets. Cette syntaxe se base sur la méthode utilitaire d’héritage simple créée par Douglas Crockford.

var objetEnfant = Object.create(objetParent);

Si nous utilisons Object.create pour recréer notre précédent exemple « ActeurMuet », on comprend mieux ce qui se passe réellement.

// Notre objet 'Acteur' possède des propriétés...
var Acteur = {
  peutJouer: true,
  peutParler: true
};

// 'ActeurMuet' hérite de 'Acteur'
var ActeurMuet = Object.create(Acteur);
ActeurMuet.peutParler = false;

// 'busterKeaton' hérite de 'ActeurMuet'
var busterKeaton = Object.create(ActeurMuet);

Nous disposons également, dans les navigateurs modernes, d’une méthode fiable pour inspecter la chaîne de prototypes d’un objet.

Object.getPrototypeOf(busterKeaton); // ActeurMuet
Object.getPrototypeOf(ActeurMuet); // Acteur
Object.getPrototypeOf(Acteur); // Object

Dans cet exemple, nous avons pu créer une chaîne de prototype sans utiliser de constructeur ou l’opérateur new.

Traverser la chaîne de prototype

Comment fonctionne la chaîne de prototype ? Quand nous tentons d’accéder à une propriété de notre nouvel objet « busterKeaton », JavaScript cherche cette dernière dans l’objet lui-même, puis dans tous les objets de sa chaîne de prototype jusqu’à la première occurrence de cette propriété.

Ce mécanisme intervient lorsqu’on demande la valeur de la propriété d’un objet.

busterKeaton.peutJouer;

Pour évaluer la valeur de la propriété « peutJouer » de « busterKeaton », le moteur JavaScript passe par les étapes suivantes :

  1. chercher la propriété « peutJouer » sur l’objet « busterKeaton », mais sans la trouver,
  2. la chercher dans « ActeurMuet » mais échouer également,
  3. chercher dans « Acteur », trouver la propriété « peutJouer » et retourner sa valeur, qui vaut alors true.

Modifier la chaîne de prototypes

Il est intéressant de constater que les objets « Acteur » et « ActeurMuet » sont toujours présents au sein du système et peuvent être modifiés durant l’exécution.

Ainsi, par exemple, si tous les objets « ActeurMuet » perdaient leur emploi, nous pourrions le traduire de la manière suivante :

ActeurMuet.estEmployé = false;

// Résultat...
busterKeaton.estEmployé; // false

Mais où est « super » ?

Dans les langages classiques, quand on surcharge une méthode, on peut se référer à la méthode du même nom de la classe parente au sein de la méthode nouvellement créée.

Dans JavaScript, nous avons plus d’options : on peut exécuter n’importe quelle fonction dans n’importe quel contexte.

Comment fait-on cela ? En utilisant les méthodes JavaScript call et apply disponibles pour chaque fonction. Plus précisément, ces dernières sont disponibles parce qu’elle se trouvent dans le prototype de l’objet Function.

Ces deux fonctions nous permettent d’exécuter une fonction dans un contexte spécifique que l’on fournit en premier argument, suivi de n’importe quel argument que l’on souhaite passer à la fonction.

Par exemple, en utilisant « ActeurMuet », si nous voulons un équivalent d’un appel à « super », cela ressemblerait à ce qui suit :

Acteur.jouer = function(tirade) {
  alert(this.nom + ': ' + tirade);
}

ActeurMuet.jouer = function(tirade) {
  // Super:
  Acteur.jouer.call(this, tirade);
};

Avec plusieurs arguments en entrée, cela donnerait :

ActeurMuet.jouer = function(tirade, émotion) {
  // Avec 'call':
  Acteur.jouer.call(this, tirade, émotion);

  // Avec 'apply':
  Acteur.jouer.apply(this, [tirade, émotion]);
};

Et où sont les constructeurs ?

Dans cette configuration, il est aisé de créer votre propre constructeur.

function créerActeur(nom) {
  // Crée une nouvelle instance qui hérite de 'Acteur':
  var a = Object.create(Acteur);

  // Assigner les propriétés de cette instance:
  a.nom = nom;

  // Retourner la nouvelle instance:
  return a;
}

Comprendre les prototypes avec un peu de classe

Maintenant que nous savons à quel point le système d’héritage par prototype peut être simple, nous pouvons revoir notre exemple original avec un œil neuf.

var charlie = new Acteur();

« Acteur » n’est pas une classe, c’est en fait une fonction.

Cela pourrait être une fonction qui ne fait rien du tout.

function Acteur() {}

Elle pourrait également ajouter quelques propriétés à l’instance qu’elle crée.

function Acteur(nom) {
  this.nom = nom;
}

Dans notre exemple, la fonction « Acteur » est utilisée comme constructeur grâce à l’opérateur new.

Un truc sympa avec JavaScript, c’est que l’on peut utiliser n’importe quelle fonction comme un constructeur. Rien ne distingue en apparence la fonction « Acteur » de n’importe quelle autre fonction.

Clarifions les constructeurs

Comme toute fonction peut être un constructeur, elles ont toutes un prototype au cas où elles seraient utilisées comme un constructeur. Même si parfois cela n’a pas de sens.

Le parfait exemple est la fonction alert qui est exposée par le navigateur. Bien qu’elle ne soit pas conçue pour être utilisée comme constructeur (dans les faits le navigateur ne vous laisserait pas le faire), elle possède bien une propriété prototype.

typeof alert.prototype; // 'object'

new alert(); // TypeError: Illegal invocation

Quand « Acteur » est utilisé comme constructeur, notre nouvel objet « charlie » hérite de l’objet contenu dans sa propriété « Acteur.prototype ».

Les fonctions sont des objets

Si vous ne l’avez toujours pas accepté, laissez-moi vous le répéter : les fonctions sont des objets, et peuvent avoir des propriétés arbitraires.

Par exemple, ceci est tout à fait valable :

function Acteur(){}

// Nous pouvons créer n'importe quelle propriété  :
Acteur.foo = 'bar';
Acteur.abc = [1,2,3];

Cependant, la propriété « prototype » reste l’endroit où nous devons stocker l’objet dont héritera chaque nouvelle instance :

function Acteur(){}

// Utilisé pour les constructeurs :
Acteur.prototype = {foo: 'bar'};

var charlie = new Acteur();
charlie.foo; // 'bar'

Créer la chaîne de prototype

Mettre en place l’héritage multiple avec notre syntaxe semblable aux classes devrait vous sembler un peu plus simple à réaliser.

Il s’agit en effet de faire en sorte que le prototype d’une fonction hérite du prototype d’une autre fonction.

// Configurer Acteur
function Acteur() {}
Acteur.prototype.peutJouer = true;
Acteur.prototype.peutParler = true;

// Configurer ActeurMuet pour qu'il hérite d'Acteur :
function ActeurMuet() {}
ActeurMuet.prototype = Object.create(Acteur.prototype);

// Nous pouvons maintenant ajouter de nouvelles propriétés au prototype d'ActeurMuet :
ActeurMuet.prototype.peutParler = false;

// Ainsi, les instances peuvent jouer, mais pas parler :
var charlie = new ActeurMuet();
charlie.peutJouer; // true
charlie.peutParler; // false

Construction : Trois étapes simples

Il est important, à ce point de la démonstration, de clarifier ce qui se passe vraiment au sein du constructeur. Instancier un nouvel objet avec un constructeur implique trois actions. Pour illustrer, voici ce qui se passe au sein du constructeur de l’objet « Acteur ».

var charlie = new Acteur();

Cette simple ligne de code provoque les actions suivantes :
1. Création d’un nouvel objet « charlie » héritant de l’objet placé dans « Acteur.prototype »,
2. Exécution de la fonction « Acteur » dans le contexte de l’objet « charlie »,
3. Passage en retour de l’objet « charlie ».

Bouclons la boucle

Cela vous semblera sans doute familier puisque c’est exactement ce que fait le constructeur maison vu précédemment, et que revoici :

function créerActeur(nom) {
  // Crée une nouvelle instance qui hérite de 'Acteur':
  var a = Object.create(Acteur);

  // Assigner les propriétés de cette instance:
  a.nom = nom;

  // Retourner la nouvelle instance:
  return a;
}

Tout comme pour notre précédent exemple, demander la propriété « peutJouer » de notre objet « busterKeaton » provoquera une interrogation de la chaîne de prototype. Cependant, le moteur JavaScript procédera un peu différemment :

  1. Chercher la propriété « peutJouer » dans l’objet « busterKeaton », mais échouer,
  2. chercher celle-ci dans « ActuerMuet.prototype » et échouer également,
  3. chercher dans « Acteur.prototype », trouver la propriété « peutJouer » et retourner sa valeur.

Vous remarquerez que la chaîne est composée d’objets contenus dans le prototype de fonctions qui ont toutes été utilisées comme constructeur d’objet.

Rester dans les classiques

Même en comprenant cette syntaxe, on veut souvent s’en éloigner le plus possible.

Il existe des implémentations plus « classiques » d’héritage construites en surcouche du système prototypal, la plus réputée étant la fonction Class de John Resig.

Vous trouverez aussi des langages compilant vers Javascript, tels que CoffeeScript ou TypeScript, proposant un système de classes. TypeScript en particulier s’approche des travaux sur les classes dans le futur standard ECMAScript 6.

Oui, le brouillon de la spécification d’ES6 contient une syntaxe simplifiée pour créer des classes, tout comme CoffeeScript et TypeScript. Mais sous le capot, ça reste de l’héritage par prototype.

Voir plus clair

Bien sûr, aucun contournement ni aucune surcouche ne peuvent remplacer une compréhension véritable de l’héritage par prototype et des pouvoirs qu’il vous confère.

J’espère que cet article vous a permis de voir plus clair et, même si vous êtes peut-être encore un peu déstabilisé, que la prochaine fois que vous vous plongerez dans l’héritage par prototype vous aurez été suffisamment préparé pour penser comme l’a fait Brendan Eich il y a tant d’années de cela.

Si vous n’y arrivez pas, vous pouvez toujours développer des applets Java !

Tiré d’une présentation

Cet article est basé sur les diapos d’une présentation que j’ai donnée le 18 octobre 2012 à Web Directions South à Sydney en Australie. Le diaporama est à votre disposition sur le Web.

Fiche technique

À propos de l'auteur

Mark Dalgleish est concepteur d’interfaces utilisateur à Melbourne, Australie.

Articles similaires

Voici la liste des dix articles les plus récents en rapport avec cet article :

JavaScript

Technique

© 2001-2016 Pompage Magazine et les auteurs originaux - Hébergé chez Nursit.com ! - RSS / ATOM - About this site