Le point sur Javascript et l’héritage prototypal

Javascript n’est pas à proprement parler un langage objet, mais il dispose quand-même d’un opérateur new et de trucs qui ressemblent vachement à des propriétés et des méthodes. Donc par raccourci, on parle d’objet.

Dans les différents tutoriels sur le sujet, quand on aborde l’héritage, la méthode proposée est souvent celle-là:

1
2
3
4
5
6
7
8
9
10
function Animal(nom) { // class Animal
  this.nom = nom;
}
Animal.prototype.manger = function() { // Animal.manger()
  console.log('Miam!');
}
function Chien(nom) { // class Chien extends Animal
  Animal.call(this, nom); // super(nom)
}
Chien.prototype = new Animal(); // le fameux extends est là

Cette méthode n’est pas parfaite puisque pour implémenter l’héritage on va instancier un objet de la classe parente. WTF ? Du coup, faisons un petit tour des méthodes alternatives.

D’abord, comprendre…

Comme on le sait, en Javascript il n’y a pas de classes, pas vraiment non plus d’objets, juste des « hashs ». On parle de manière générale d’objet, on devrait toujours préciser « anonyme » ou non.

// un objet anonyme
var o = {prop: "value"};
// un objet... heu... pas anonyme quoi :) On parle alors d'instance
var o = new String();

La différence entre un objet anonyme et un autre ? Elle est triviale: le second est issue d’un constructeur nommé. À part ça ? Aucune différence.

De ce point de vue une « méthode » est une propriété comme une autre, si ce n’est qu’il s’agit d’une propriété de type Function. Dans tous les cas, quand une fonction est appelée comme une méthode sous la forme objet.methode(); le « contexte d’exécution » de cette fonction définit une variable this qui sera accessible et représentera l’objet, tout simplement !

Jouons un peu avec this pour comprendre comment il est défini:

1
2
3
4
5
6
7
8
9
var objet = { "nom": "Toto", "bonjour": function() {
  console.log("Bonjour, je m'appelle " + this.nom);
} };
objet.bonjour(); // "Bonjour, je m'appelle Toto", this a bien été défini
var foo = objet.bonjour;
foo(); // "Bonjour, je m'appelle undefined", this n'était pas défini cette fois !
var objet2 = { "nom": "Titi", "bonjour": objet.bonjour };
objet2.bonjour(); // "Bonjour, je m'appelle Titi", comme on s'y attend
// maintenant qu'on a compris ;)

Avec cet exemple, on comprend bien qu’un objet, même anonyme, mérite un peu le nom d’objet, vu que ses « méthodes » définissent bien un this, pour peu qu’elles soient appelées correctement. Concrètement, objet.methode(...) est toujours strictement équivalent à methode.call(objet, ...).

Et les objets « à constructeur » dans tout ça ? Et bien ils sont passés par l’opérateur new, et ça change tout ! Tout d’abord, toute fonction en Javascript a une propriété prototype qui est toujours un objet (anonyme ou non, vous suivez ?). L’opérateur new va appeler une fonction en lui offrant un this qui est une copie de son prototype, puis retourner comme valeur cette copie du prototype. On parle alors d' »instance » de cette « classe » (même si le terme de classe est galvaudé, c’est un raccourci pour mieux appréhender le concept).

1
2
3
4
5
function Animal(nom) { // class Animal
  this.nom = nom; // "this" est une copie de "Animal.prototype" lors du "new ...".
}
var animal = new Animal("Toto"); // "animal" = une copie de "Animal.prototype",
// modifiée par l'exécution de la fonction "Animal(...)" = instance d'Animal.

Bien sûr, il y a un tout petit plus de choses qui se passent derrière, et en pratique l’objet Animal.prototype est partagé par toutes les instances de Animal en même temps, ce qui implique que si on modifie le prototype, on modifie également toutes les instances existantes et à venir (ajout et suppression de méthodes ou d’attributs).

En bonus, un objet instancié a également toujours une propriété constructor qui a pour valeur la fonction ayant été appelée initialement.

console.log(animal.constructor); // affiche "Animal(nom)"

Les avantages et inconvénients de la méthode « classique »

ClasseFille.prototype = new ClasseParente();

Cette méthode semble idéale:

  • Le prototype de ClasseFille étant une instance de ClasseParente on récupère bien toutes les méthodes et tous les attributs.
  • Si on ajoute dynamiquement une méthode ou un attribut à la classe parente, la classe fille en bénéficiera aussi.

Néanmoins elle pose quelques problèmes:

  1. Impossible d’appeler la version parente d’une méthode qu’on surcharge.
  2. Mais plus grâve à mon sens, car conceptuellement c’est du total n’importe quoi: une instance de la classe mère ? What ? Depuis quand on crée une instance au moment de déclarer des classes ? Et si moi dans mon constructeur je fais des actions spécifiques ?

L’ajout des méthodes parentes de manière statique

Une solution serait donc de simplement copier les éléments d’un prototype à l’autre

for (var p in ClasseMere.prototype) {
   ClasseFille.prototype[p] = ClasseMere.prototype[p];
}

Cette méthode règle la question de l’infâme instance. Mais les autres problèmes restent là, et en plus cette fois on perd la capacité à suivre les évolutions ultérieures du prototype de la classe parente.

L’héritage prototypal, le vrai ?

Notre sauveur pourrait être __proto__ ! Pour résumer: quand on a var objet = new Classe();, alors objet est une instance de Classe, et il s’agit d’une copie dynamique (et maintenue à jour) de Classe.prototype, en cela il est à la fois similaire à Classe.prototype (même propriété, et mêmes attributs) mais il est sa propre copie unique. objet.__proto__ lui EST Classe.prototype. On ne dirait pas, mais c’est exactement ce qu’il nous fallait :)

Pour résumer, quand on cherche une propriété objet.propriete, si elle n’existe pas directement dans objet, Javascript va chercher dans objet.__proto__. Tout simplement :)

D’abord une illustration pour comprendre avec des objets anonymes:

1
2
3
4
5
var objet = { "nom": "Dupont" };
console.log(objet.prenom); // undefined, ouais, normal
var objet = { "nom": "Dupont", "__proto__": { "prenom": "Albert" } };
console.log(objet.prenom); // "Albert"
// Avec "__proto__" on a indiqué à Javascript que "objet" est une instance de ce prototype

Un autre exemple avec des instances:

1
2
3
4
5
6
7
8
var Animal = function() {} // Class Animal
Animal.prototype.manger = function() {}; // Animal.manger()
var a = new Animal();
var b = new Animal();
console.log(a.constructor === Animal); // true
console.log(a.__proto__ === Animal.prototype); // true
a.__proto__.boire = function() {}; // équivalent à Animal.prototype.boire = ...
console.log(b.boire); // Du coup, la méthode est définie pour b aussi :)

Et une application dans l’héritage prototypal:

ClasseFille.prototype.__proto__ = ClasseMere.prototype;
// Signifie "ClasseFille.prototype est un objet dont le prototype est le même que ClasseMere"
// Il agit exactement comme une instance de ClasseMere() sans qu'on ait eu besoin d'appeler
// le constructeur.

C’est beau, simple, et ça répond à tous nos problèmes: pas d’instance injustifiable, on suit les évolutions du prototype parent, et en plus on peut appeler les méthodes parentes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// class Animal
function Animal(nom) {
  this.nom = nom;
}
// Animal.manger()
Animal.prototype.manger = function() {
  console.log("Animal.manger");
}
// class Chien extends Animal
function Chien(nom) {
  // this = instance de Chien
  // this.__proto__ === Chien.prototype (classe)
  // this.__proto__.__proto__ === Animal.prototype (classe parente)
  // this.__proto__.__proto__.constructor (constructeur parent)
  // super(nom):
  this.__proto__.__proto__.constructor.apply(this, arguments);
}
// le "extends" est ici
Chien.prototype.__proto__ = Animal.prototype;
Chien.prototype.manger = function() {
  // super.manger():
  this.__proto__.__proto__.manger.apply(this, arguments);
  console.log("Chien.manger");
}
var c = new Chien("Rantanplan");
c.manger(); // affiche "Animal.manger" puis "Chien.manger"

C’est un peu verbeux, mais ça semble être la réponse idéale :) Idéale ? Non car IE, l’irréductible trou du cul du web, ne supporte pas __proto__. Cool.

Pour finir

Pour le Javascript côté client, le mieux est de s’en tenir au bon vieux ClasseFille.prototype = new ClasseMere();, maintenant qu’on sait exactement ce que ça signifie. Mais bien sûr, ça implique d’avoir un constructeur qui n’effectue aucune action autre que de la pure définition d’attributs.

Par contre, grosse note positive pour la JS côté serveur, où cette fois le moteur d’exécution ne sera jamais celui d’IE :) Donc quelque soit la solution (en l’occurrence, ça a de grandes chances d’être NodeJS hein), vous pourrez utiliser __proto__, alors n’hésitez pas! cet objet existe est est manipulé de toute façon « en coulisses » donc autant le comprendre et l’utiliser là où c’est possible car il représente le mode d’héritage prototypal le plus performant 😉

29 réflexions au sujet de « Le point sur Javascript et l’héritage prototypal »

  1. Ping : Les tweets qui mentionnent Le point sur Javascript et l’héritage prototypal « naholyr -- Topsy.com

  2. Palleas

    Moi ce que j’aime beaucoup dans ces exemples sur le héritage et la notion de prototype, c’est que c’est toujours des exemples où on manipule des instances de chien ou de voiture. :3

    Sinon +1 pour le javascript coté serveur ou là c’est beaucoup plus pertinent d’avoir une notion d’héritage, ne serait-ce parce qu’on va également manipuler des objets d’un éventuel modèle de donnée, et là ça se tient un peu plus…

    Répondre
  3. Fabien

    Bon article mais pour moi ‘prototype’ c’est une vraie boite à caca. A ouvrir seulement si on aime les emmerdes…

    Non, sincèrement, quel sont les avantages dans la pratique ?
    Réponse : rien qu’on ne puisse pas faire avec des méthodes et propriétés d’objets.

    Enfin si : juste modifier ou corriger une méthode en dehors de la classe. Mais si il s’agit pas d’une classe native, on peut modifier le code directement. Et si il s’agit d’une classe native, c’est juste déconseillé. Et franchement, ca vous arrive souvent ?

    D’autant qu’avec les méthodes et propriétés d’objets on peut utiliser des variables définies dans le constructeur qui sont assimilables à des propriétés privées. C’est un gain de performance et de cohérence.
    On peut aussi faire de l’héritage multiple.

    Exemple :

    var A = function() {
     
    	this.publicPropertyA = 10;
     
    	this.publicMethodA = function() {
    		console.log("publicMethodA");
    	};
     
    };
     
    var B = function() {
     
    	// héritage
    	A.call(this);
     
    	this.publicPropertyB = 5;
     
    	//donnée calculée une fois et accessible par toutes les méthodes
    	var privatePropertyB = 'B';
     
    	this.publicMethodB = function() {
    		this.publicMethodA();
    		return this.publicPropertyA + this.publicPropertyB;
    	};
     
    };
     
    var C = function() {
     
    	// héritage
    	A.call(this);
    	B.call(this);
     
    	this.publicPropertyC = 20;
     
    	this.publicMethodC = function() {
    		this.publicMethodA();
    		return this.publicMethodB() + this.publicPropertyC;
    	};
     
    };

    Cette solution a le mérite de couvrir 99.9% des usages et sans se prendre la tête ! Prototype, ca doit resté dans l’arrière boutique.

    Répondre
    1. naholyr Auteur de l’article

      Faut pas trop s’énerver hein, c’est pas grâve 😛

      En revanche, ce qui ne va pas avec ta méthode, c’est que ce n’est plus du tout de l’orienté objet comme on l’entend lorsqu’on manipule le prototype. On essaie toujours de simuler (à tort ou à raison) un comportement habituel de « classe » lorsqu’on fait ça. Dans ton cas, ce n’est plus du tout une classe, mais plutôt quelque chose qui ressemblerait à une implémentation du design pattern factory 😉

      Concrètement, qu’est-ce qui fait que l’on ne fait pas du tout la même chose ?

      var Foo_Module = function () {
        this.hello = function () { console.log('Hello, world'); };
      };
      var Foo_Classe = function () { };
      Foo_Classe.prototype.hello = function () { console.log('Hello, world'); };
       
      var m1 = new Foo_Module(), m2 = new Foo_Module();
      var c1 = new Foo_Classe(), c2 = new Foo_Classe();
       
      // Dans le cas du module, "hello" est une propriété directe de l'objet
      console.log(m1.hasOwnProperty('hello') === true);
      // Dans le cas de la classe, "hello" est une "méthode", qui n'est pas une propriété directe de l'objet
      console.log(c1.hasOwnProperty('hello') === false);
       
      // Dans le cas du module, on a créé une nouvelle fonction à chaque fois
      console.log(m1.hello !== m2.hello);
      // Dans le cas de la classe, c'est bien la même fonction qui est appelée à chaque fois
      console.log(c1.hello === c2.hello);

      On voit bien qu’il ne s’agit plus du tout des mêmes concepts, et il est important de comprendre ce qui se passe derrière. Après, de là à être systêmatiquement pour ou contre l’usage de prototype, je trouve ça un peu irrefléchi 😉

      Répondre
      1. Phildes

        Bonjour,

        Je trouve se sujet très intéressant.

        A l’éxécution comment se comportent : m1.hello() et c1.hello()
        Pouvez-vousd écrire précisément les différentes opérations que l’interpréteur effectuera pour accéder à chacune de ces 2 fonctions ?

        Répondre
        1. naholyr Auteur de l’article

          Précisément ce que fait l’interpréteur ? Non, certainement pas 😉

          Mais assez basiquement :

          m1.hello() // m1.hello = function () { … }
          // donc on appelle directement la fonction, pas d'opération particulière
           
          c1.hello() // c1 = new …, lors du "new" le constructeur a été attaché
          // à l'objet : c1.constructor === Foo_Classe
          // lorsqu'on appelle c1.hello(), c1.hello n'existant pas directement
          // on va regarder dans c1.constructor.prototype
          // or Foo_Classe.prototype.hello a été définie, c'est elle qu'on appelle
          // équivalent : c1.constructor.prototype.hello.call(c1)
          Répondre
          1. Phildes

            Merci pour cette réponse détaillée.
            Ceci pose peut-être le problème de la vitesse de traitement avec l’usage des prototypes.

            Il serait peut-être utile de faire une analyse précise du gain de production avec les prototype comparée aux pertes de vitesse qu’il engendre.
            Personnellement, je n’ai jamais rencontré de situation que je ne puisse résoudre de façon simple et efficace (car rapide en exécution) sans-prototype.

            J’aimerais savoir si des intervenants de ce sujet ont été confrontés à des situations où les prototypes étaient incontournables, ou si, plus simplement, ces prototypes apportaient un gain pour l’exécution.
            Si oui, il serait très intéressant d’avoir des exemples concrets.

          2. Phildes

            Les prototypes nous offre des surprises amusantes, comme, par exemple, la surcouche-TypeScript :http://jsfiddle.net/8ez4x/12/

            Nous rappelons, qu’il existe d’autres méthodes d’héritage plus simple et rapide d’exécution, qui couvre la quasi totalité(*) des besoins (peut-être, ‘tous’ les besoins)

  4. Simon Thépot

    Hello,
    très intéressant ce billet, il contient ce que j’ai pu reconstruire après de multiples lectures de différents articles !

    Mais en y pensant un peu je me suis dit, le problème de cet héritage vient du constructeur qui « peut » faire quelque chose.

    L’idéal serait de créer un constructeur vide, d’assigner le prototype dont on veut hériter et de faire un new dessus.

    noop = function () {}
    noop.prototype = Animal.prototype
    var awesome_inheritance = new noop()

    Et voila.
    Du coup, ca m’a fait comprendre pour de bon le snippet de Douglas Crockford:

    Object.beget = (function(Function){
        return function(Object){
            Function.prototype = Object;
            return new Function;
        }
    })(function(){});

    Ou comment réaliser un héritage en une ligne:

    var Chien = Object.beget(Animal.prototype);

    Javascript, tout est possible :b

    Répondre
  5. fabien

    Pourquoi utiliser ‘prototype’ qui est comme on peut le voir dans ton article, un casse tête ?
    Pourquoi ne pas se contenter des propriétés et méthodes d’objets qui couvre 99.99% des usages ?

    Répondre
    1. naholyr Auteur de l’article

      `prototype` en lui-même n’a rien d’un casse-tête. C’est juste pour l’héritage que ce n’est pas toujours évident, tout simplement parce que Javascript n’est pas un langage orienté « classes », l’héritage n’y est donc pas naturel. Maintenant comme l’indique justement l’article, la méthode « universelle » est donc

      var ClasseFille = function () { ... }
      ClasseFille.prototype = new ClasseParent();
      ...

      ce qui implique, comme l’a précisé Simon, qu’il faut penser ses classes correctement pour qu’elles puissent être héritées: le constructeur ne doit effectuer de préférence aucune action, afin que C2.prototype = new C1() ne provoque pas d’effet de bord.

      Répondre
  6. Fabien

    Merci naholyr et simon. Je comprends mieux et … je reste calme. :)

    Si dans le cas du module, les méthodes sont crées pour chaque instance, ca veut dire que le concept de « module » est potentiellement plus consommateur de ressources machines que le concept de « classe » ?

    Répondre
    1. naholyr Auteur de l’article

      J’hésite à parler de modules, car en général cette notion est implémentée sous forme de singleton… Je ne maîtrise ici pas assez les design patterns JS pour être vraiment sûr du terme à apposer à la méthode que tu proposais.

      En tous cas oui pour les ressources consommées, c’est sûr qu’il en prendra plus. Après ça peut très bien être évité, voilà par exemple une version sans prototype et sans duplication de ressources:

      var Foo = (function () {
        function hello() {
          console.log('Hello, world');
        }
       
        return function () {
          this.hello = hello;
        }
      })();
       
      var m1 = new Foo, m2 = new Foo;
      console.log(m1.hello === m2.hello); // true

      En revanche on a toujours « hello » qui est une propriété directe de l’objet. Aucune idée si ça a un impact sur les performances, mais disons qu’on n’est plus sur le même concept.

      Un dernier point en faveur de prototype, qui fait que là encore on est beaucoup plus proche de la notion habituelle de classe:

      var Vehicule = function () {}
      var Voiture = function () {}
      Voiture.prototype = new Vehicule
      var VoitureSansPermis = function () {}
      VoitureSansPermis.prototype = new Voiture
       
      var v = new VoitureSansPermis
      console.log(v instanceof Voiture); // true
      console.log(v instanceof Vehicule); // true

      Même sur plusieurs niveaux d’héritage, l’opérateur instanceof fonctionne, et c’est pratique 😉

      Répondre
      1. Phildes

        On peut implémenter l’équivalent de instanceof dans de l’héritage conçu avec ‘call’.
        Il suffit d’ajouter une variable dans le constructeur, ensuite il ne reste qu’à vérifier la présence de cette variable.
        Puisque le ‘call’ recherche les constructeurs des parents, l’objet pourra se reconnaître dans le type des parents.
        pratique 😉

        Répondre
  7. k33g_org

    pour plus me prendre la tête j’écrit mes « classes » en coffeescript, je génère le js et hop j’ai une bonne fausse classe/function qui marche à merveille (et je ne fais plus d’erreur)

    allez des animaux pour faire plaisir à @palleas

    class Animal
    	constructor:(what)->
    		@what = what
     
    	toString :->
    		"Hi i'am a #{@what}"
     
    class Dog extends Animal
    	constructor:(name)->
    		@name = name
    		super "DOG"

    JS généré:

    var Animal, Dog;
    var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
      for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
      function ctor() { this.constructor = child; }
      ctor.prototype = parent.prototype;
      child.prototype = new ctor;
      child.__super__ = parent.prototype;
      return child;
    };
    Animal = (function() {
      function Animal(what) {
        this.what = what;
      }
      Animal.prototype.toString = function() {
        return "Hi i'am a " + this.what;
      };
      return Animal;
    })();
    Dog = (function() {
      __extends(Dog, Animal);
      function Dog(name) {
        this.name = name;
        Dog.__super__.constructor.call(this, "DOG");
      }
      return Dog;
    })();
    Répondre
    1. naholyr Auteur de l’article

      Le code généré fait un peu vomir (comme souvent avec Coffescript :P) mais effectivement CS permet d’apporter une abstraction qui évite de se poser ce type de question…

      Répondre
      1. dhar

        Hello,
        Je trouve au contraire que le code coffescript est plutôt lisible, en tout cas dans l’exemple donné ci-dessus.
        Il manque juste quelques commentaires pour en faire un parfait exemple d’héritage JS.
        L’utilisation de ‘hasOwnProperty’ est intéressante aussi.

        Répondre
  8. Fabien

    Pas mal Coffescript, je connaissais pas.

    D’après les exemple que je vois, impossible de simuler des « propriétés privées » avec « prototype » puisque les méthodes sont hors contexte. Je me trompe ou qqlqu’un a une solution ?

    Répondre
  9. naholyr Auteur de l’article

    En effet pour avoir de la variable privée il faut forcément que la variable soit dans le même contexte que la méthode qui va l’utiliser, ce qui impose en général this.methode = ... dans le constructeur.

    Répondre
  10. Ludo

    Petite correction à la relecture de ton billet :

    > le « contexte d’exécution » de cette fonction définit une variable this qui sera accessible et représentera l’objet, tout simplement !

    la variable this représentera le contexte d’execution de la fonction, pas seulement l’objet. C’est un détail mais je trouve qu’il a son importance 😉

    Répondre
  11. Thierry

    Bonjour,
    le commentaire le plus récent avant le mien date de plus de 5 ans, et pourtant il est toujours d’actualité malgré la toute récente version 10 de IE ! Merci pour cet article bien utile. :)

    Répondre
    1. Thierry

      En fait le format de date ne semble pas être le format français, le dernier commentaire n’a que quelques mois, lol.
      Mais très bon article tout de même, et IE toujours aussi misérable ! -)

      Répondre
  12. Draeli

    Je déterre également le sujet.
    Je suis tombé dessus et malgré les différents éclairages (intéressant mais très clairement prise de tête), je suis encore plus perdu sur la méthode d’héritage a préférer sachant que ce qui m’intéresse est d’avoir la vrai notion d’héritage sans forcément viser la vitesse mais surtout la compatibilité (en Ecma5 on a effectivement l’apparition de Object.create mais vu que cette buse d’IE dans ses anciennes versions est encore pas mal utilisé ….).
    Une idée au final de l’héritage « type » à effectuer ? (ou encore mieux une fonction qui permet de cacher la complexité d’héritage)

    Répondre
  13. Ping : Environnement Java Script | Pearltrees

Laisser un commentaire