doSimple Chainage de prototypes en détails

Dernière modification le 18 April 2009

Chainage de prototypes en détails

Le chaînage de prototypes (appelé grossièrement héritage) est un élément qui m’a demandé quelque années pour comprendre réellement, simplement car on ne s’en sert que rarement directement (car la bibliothèque utilisée le fait à notre place) ou simplement car on en a pas le besoin. J’ai misérablement échoué une question d’entretien d’embauche une année auparavent sur ce sujet. Après avoir donné une petite explication à un ami proche récemment, il m’a suggéré d’écrire un petit truc sur le sujet. Et de le faire de manière simple et compréhensible. J’espère tenir cette promesse.

J’ai été introduit à la programmation orientée-object avec les langage C++ et Java et leur célèbre héritage de classes d’animaux qui mange et boivent. JavaScript est un langage à prototype, donc légèrement différent que Java ou C++. Tentez d’oublier ce que connaissez déjà pour les 5 minutes à venir et plongez dans ce qui suit. C’est bien plus simple qu’on se l’imagine.

Modifions le prototype de Object; code testable avec SpiderMonkey (ou via la console de Firebug).

>>> Object.prototype.foo = "bar";
>>> var a = {}; // ou new Object()
>>> a.foo
"bar"
>>> // ou print() dans SpiderMonkey
>>> for(var key in a) { console.log(key); }
bar
>>> "foo" in a
true
>>> a.hasOwnProperty("foo")
false

object.prototype example

a est un Object qui possède un prototype. Le prototype d’un Object n’a lui pas de prototype, Object est donc l’élément racine. Mais comme la clé foo est une chaîne de charactères (String) et que celle-ci est un Objet qui a donc un prototype, il y a donc une boucle.

(Presque) tout en JavaScript est un objet et tous les Object ont un prototype.

>>> a.foo.foo
"bar"
>>> new Date().foo
"bar"
>>> 3.14.foo
"bar"
>>> (42).foo
"bar"
>>> true.foo
"bar"
>>> (function(){}).foo
"bar"
>>> "spam".foo
"bar"
>>> [].foo
"bar"
>>> /^[rR](?:e[gx]){2}p$/.foo
"bar"
>>> // presque tout
>>> undefined.foo
TypeError: undefined has no properties
>>> TypeError.foo
"bar"

J’espère montrer ici qu’il ne faut jamais modifier le prototype de Object.

Un autre mauvais exemple avec différents types natifs.

Date.prototype example

>>> Object.prototype.foo = "bar";
>>> var now = new Date();
>>> now.foo
"bar"
>>> Date.prototype.foo = "spam";
>>> now.foo
"spam"
>>> new Object().foo
"bar"
>>> delete Date.prototype.foo
>>> now.foo
"bar"

Ceci montre que JavaScript remonte de prototype en prototype pour retrouver une clé. C’est simplement une chaîne. Pour obtenir le constructeur d’un object faites simplement:

>>> new Date().constructor == Date
true
>>> .1.constructor == Date
false
>>> .1.constructor == Number
true

Gecko (le moteur de Firefox) offre une propriété très intéressante __proto sur chaque élément. Elle retourne le prototype de la valeur. C’est le seul moyen que je connaisse pour obtenir le prototype d’un prototype. Pour éviter toutes confusions, je ne vais pas m’en servir ici.

Oublions vite tout cela et faisons quelque chose de plus intéressant qui va ressembler à de l’héritage traditionnel mais se reposer sur le prototype.

Shape and circle prototype example

>>> function Shape(name) { this.name = name };
>>> Shape.prototype.toString = function() {
...  return "<Shape \""+this.name+"\">"
... };
>>> new Shape("blob");
<Shape "blob">
>>> function Circle(radius) {
...  Shape.call(this, "circle");
...  this.radius = radius
... }
>>> Circle.prototype = new Shape();
>>> Circle.prototype.constructor = Circle;
>>> Circle.prototype.area = function() {
...  return this.radius * this.radius * Math.PI
... };
>>> var circle = new Circle(1);
>>> circle
<Shape "circle">
>>> circle.area()
3.14159

La partie complexe repose dans ces deux lignes là :

>>> Circle.prototype = new Shape();
>>> Circle.prototype.constructor = Circle;

La première va définir le prototype d’un cercle (Circle) comme étant une instance d’une forme (Shape) et la seconde va redéfinir la clé constructor a son réel constructeur car il a été écrasé par la ligne précédante. Le graphe montre qu’une instance de cercle à deux éléments : name et radius. Respectivement nom et rayon. La fonction area (aire) appartient au prototype du cercle (qui est une forme avec un nom, non-défini dans ce cas-ci).

C’est une manière de faire de l’héritage en JavaScript, une autre consiste à copier (les références sur) les fonctions d’un autre objet (généralement, un autre prototype).

Lorsque la fonction toString est appelée dans l’exemple précédant, il faut remonter deux niveaux de prototypes pour l’y trouver. En remplacant les deux lignes précédantes par celles-ci :

>>> for(var key in Shape.prototype) {
...  if(Shape.prototype.hasOwnProperty(key) &&
...     key !== "constructor")
...   Circle.prototype[key] = Shape.prototype[key]
... }

Jettons un œil au graphe obtenu.

Shape and circle prototype example

Le graphique est plus plat qu’auparavant, il conservere tout à un même niveau et n’effectue pas de chaînage. On évite ainsi le coup de remonter plusieurs niveaux mais avons l’inconvénient de ne pouvoir modifier un élément source et de voir cette modification se propager dans tous les éléments ayant copié cette propriété au départ.

Pour conclure, comparons deux manières apparemment identiques de réaliser une classe :

>>> function Foo() {
...  this.toString = function() {
...   return "Foo";
...  }
... }
>>> new Foo();
"Foo"
>>> function Bar() {}
>>> Bar.prototype.toString = function() {
...  return "Bar"
... };
>>> new Bar();
"Bar"

Appeler new Foo.toString() sera très direct, the fonction appartenant à l’instanc. Alors que de faire new Bar().toString()

va devoir regarder dans le prototype. Si Foo est plus rapide, créer plus d’une instance va occuper plus d’espace mémoire, toString étant propre à chaque instance. Bar sera un peu plus lent mais créer un certain nombre d’instances se fera à moindre coût puisque le prototype est identique pour toutes. Il est possible de modifier après coup (monkeypatcher) toutes les instances de Bar simplement alors que ça n’est pas possible avec Foo.

J’ai écrit un petit test qui crée une énorme chaîne de prototypes, de 1 à 100’000, et regarde combien de temps est requis par le navigateur pour accéder à une valeur appartement à la racine de la chaîne. Ma seule observation, c’est rapide, très rapide tant que ça reste dans un ordre de grandeur à l’échelle humaine, entre 1 et 100. Testez vous-même.

Auteur
Yoan Blanc
Licence
LGPL
Liens connexes
Javascript prototypal chaining for computer scientists, version anglaise.