mardi 8 mars 2011

Javascript: comprendre la chaine des prototypes

Javascript est un langage objet sans classe. Par conséquent, essayer de comprendre l'héritage en javascript à travers des concepts d'autres langages (php, java) est une entreprise vaine.

Pour autant, le fait qu'il n'existe pas de classe ne veut pas dire pour autant que la notion d'héritage est inexistante.

Dans cette article, nous allons étudier une méthode basée sur le mécanisme des prototypes. Garder à l'esprit que ce n'est pas la seule méthode possible pour implémenter l'héritage.

La base

Créer de nouveaux objets

Il y a deux façons pour créer un nouvel objet en javascript. La syntaxe littérale ou l'opérateur new.

La fonction constructeur

Contrairement à la syntaxe littérale, l'opérateur new nous permet de spécifier une fonction constructeur. La fonction constructeur est une fonction classique qu'on appelle avec l'operateur new. Dans une telle fonction, le mot clé this fait référence à l'objet fraichement créé.

On peut imaginer que, lorsqu'une fonction est utilisé comme "constructeur", javascript réalise quelque chose comme:
L'opérateur instanceof nous révèle cependant que ces manières de construire un objet ne produisent pas tout à fait la même chose (nous verrons pourquoi plus loin):

Le prototype

Ajouter des methodes d'instance grâce au prototype

Il existe plusieurs façon d'ajouter des methodes à nos objets. L'une d'entre elles est de définir des fonctions dans l'objet prototype de la fonction constructeur:

Le prototype, un objet comme les autres

Il ne faut pas s'imaginer que le prototype est un élément syntaxique du langage qui permet de définir des méthodes d'instances. Le prototype d'une fonction est un objet tout ce qu'il y a de plus commun.
D'ailleurs, si vous utilisez le prototype de votre fonction constructeur pour définir des propriétés d'instance, vous risquez d'avoir des surprises:

Le lookup

Le lookup, part 1

L'exemple précédent montre qu'en fait les variables "a" et "b" partage le même objet store. C'est en fait normal. Lorsque vous demandez une propriété d'objet, javascript regarde d'abord si l'objet possède cette propriété. S'il ne la trouve pas, il regarde dans le prototype de la fonction constructeur de l'objet. Cela signifie que si vous modifiez une propriété du prototype d'une fonction, toutes les instances de cette fonction qui ne définissent pas eux-même la propriété seront impactés.
Sachez que, lorsque vous définissez une fonction, javascript crée automatiquement un prototype pour votre fonction. Ce prototype contient au moins une propriété constructor qui contient à son tour la fonction elle-même. (voir exemple suivant)

Comme les objets regarde dans le prototype de leur constructeur en cas de propriété manquante, on en déduit que tout objet possède une propriété constructor qui référence la fonction avec laquelle il a été créé.

Le lookup, part 2

Nous savons maintenant que les propriétés d'objets sont d'abord cherché dans l'objet lui-même puis dans le prototype de son constructeur. Mais comment javascript sait qu'un objet est issu de tel ou tel constructeur ?

Nous pourrions penser que l'interprétateur en garde la trace, un point c'est tout - et que la manière fait partie de la magie du langage. Heureusement, les implémentations moderne de javascript expose comment est conservé le lien entre un objet et son constructeur.

En deux mots, chaque objet se voit assigner une propriété __proto__ qui référence le prototype du constructeur. Tout se passe comme si javascript agissait ainsi:


D'ailleurs:

Le lookup, part 3

L'exemple précédent signifie entre autre que l'opérateur new est un simple raccourci du langage. Nous pourrions très bien l'implémenté nous même !
Résumons tous notre savoir en deux points: Primo, lorsqu'un objet est créé, on lui fixe une propriété __proto__ qui référence le prototype du constructeur. Secundo, quand une propriété n'est pas trouvé dans l'objet, javascript regarde si elle existe dans sa propriété __proto__.

La vrai question est la suivante: que se passe-t-il lorsque la propriété n'est pas non plus trouvé dans le __proto__ ? C'est simple, javascript applique le même procédé: il regarde dans sa propriété __proto__.

On cherche d'abord dans obj, puis obj.__proto__, puis dans dans obj.__proto__.__proto__, etc. On peut imaginer que cela se passe ainsi:

La chaine des prototypes

Fabriquer une chaine

Nous avons compris comment fonctionne la "chaine" des prototypes. Il nous reste maintenant à exploiter ce que nous savons: comment chainer les proto à notre guise ?

La propriété __proto__ d'un objet est égale au prototype du constructeur, mais quel est le __proto__ du prototype ? Si nous ne le fixons pas nous même, il vaut Object.prototype :
Le plus simple pour fabriquer une chaine de deux éléments serait de fixer nous même la valeur du proto du prototype :
Mais cette façon de faire n'est pas belle. Si la propriété proto est entourée de deux underscores, c'est pour une raison: nous ne sommes pas sensé y touché. Certaines implémentations de javascript ne le permettent d'ailleurs pas.

Une meilleure façon de faire

Imaginons un constructeur A donné. Nous voudrions écrire un constructeur B qui "hérite" des méthodes de A. Pour cela, nous avons vu qu'il faudrait que B.prototype.__proto__ = A.prototype. Seulement nous ne voulons pas utiliser explicitement __proto__.

Il nous faut donc trouver un objet tel que le proto de cet objet soit le prototype de A. Quel objet a cette particularité ? Eh bien, n'importe quelle instance de A !
Notons au passage que nous sommes obligé de remettre la propriété constructor de B.prototype. Pourquoi ?

Nous avons vu que javascript crée automatiquement un prototype pour les fonctions, et que ce prototype a une propriété constructor qui référence la fonction. Seulement, dans l'exemple précédent nous écrasons cet objet par défaut avec une instance de A.

Si nous demandons B.prototype.constructor, javascript va commencer son lookup, et nous ramener B.prototype.__proto__.constructor, ou autrement dit A.prototype.constructor, soit A. C'est pourquoi nous remettons explicitement cette propriété.

Une autre façon de faire

La façon précédente de chainer les prototypes peut poser un problème. Supposons que le constructeur A contienne du code qui s'execute: Ca ne va pas du tout ! quand nous voulons définir l'héritage, nous produisons l'alerte ! Il faudrait qu'à la définition du prototype nous ne fassions rien. Voici une technique qui permet de le faire. Cette technique consiste à passer par un objet intermédiaire: A la lecture de cet exemple on peut se demander pourquoi ne pas faire directement : B.prototype = A.prototype. On ne peut pas procéder ainsi car alors si on ajoutait des methodes à B on les ajouterait aussi à A. Ce qui n'est pas souhaité !

Par ailleurs, il y a également un inconvénient à cette technique: si des méthodes d'instance sont directement définies dans A, B n'en héritera pas.



Un mix des deux

Avec ce que nous avons appris, il serait possible de faire beaucoup de chose en matière d'héritage. Nous donnons ici seulement une piste qui combine nos deux méthodes précédentes: On peut bien sûr améliorer l'idée !

Aucun commentaire:

Enregistrer un commentaire