30 janvier 2014

Javascript - Objets et Héritage par prototypage


Javascript n'est pas un langage objet comme Java ou C#. Il gère les objets différemment : il utilise la notion d'héritage par prototypage.
Mais qu'est-ce qu'un prototype ??
Je vais vous présenter ces notions dans cet article et un exemple expliquant l'héritage par prototype en Javascript.

Les objets

En Java, la notion d'objet correspond à une notion de classe avec des propriétés publiques ou privées et des méthodes qui peuvent être des accesseurs sur ces propriétés.

En Javascript, rien de tout ça.

En Javascript, la notion d'objet correspond en fait à un ensemble de clés/valeurs comme on a en JSON.

Tout est visible à l'extérieur de l'objet. Il n'y a pas de notion de valeur privée. Il faut alors se servir d'une convention de nommage pour indiquer les valeurs privées, comme par exemple en préfixant les valeurs privées par '$' ou '_'.


Héritage d'objets par prototypage

En plus de cet ensemble de clés/valeurs, chaque objet Javascript contient un objet interne et masqué qui s'appelle prototype. Cet objet sert pour l'héritage entre objets. En effet, un objet enfant va avoir ses propres clés/valeurs et avoir les clés/valeurs de son objet parent. Les clés/valeurs de l'objet parent sont alors stockés dans l'objet prototype de l'objet enfant.

En Javascript, cela s'appelle l'héritage par prototypage.

Fonctionnement interne de l'héritage

Nous allons voir ce qui se passe en interne dans le moteur Javascript si on manipule des objets ayant de l'héritage.

Pour cela, nous utilisons la représentation interne du navigateur chrome en même temps que nous effectuons des actions en javascript.

Pour information, dans Chrome, l'objet prototype est nommé __proto__ :

Création de l'objet parent

Représentation interne - Avant
parent: undefined
Javascript:
var parent = {};
ou
var parent = new Object();
Représentation interne - Après
parent: Object
.  __proto__: Object
Résultat:
L'objet parent ne contient aucun attribut.
Il contient uniquement l'objet prototype qui est en fait Object.prototype car 'parent' hérite de Object comme c'est le cas pour tous les objets Javascript.

Ajout sur l'objet parent de l'attribut 'a' de valeur '10'

Représentation interne- Avant
parent: Object
.  __proto__: Object
Javascript:
parent.a = 10;
Représentation interne - Après
parent: Object
.  a: 10
.  __proto__: Object
Résultat:
L'objet parent contient maintenant un attribut 'a' de valeur '10'.

Création de l'enfant qui hérite du parent

Représentation interne - Avant
parent: Object
.  a: 10
.  __proto__: Object


enfant: undefined
Javascript:
var enfant = Object.create(parent);
Représentation interne - Après
parent: Object
.  a: 10
.  __proto__: Object


enfant: Object
.  __proto__: Object
.  .  a: 10
.  .  __proto__: Object
Résultat
L'objet enfant n'a pas lui-même d'attribut mais "hérite" au sens Javascript de toutes les clés/valeurs de l'objet parent.

En fait, il s'agit des clés/valeurs de l'objet parent qui sont accédées via l'objet prototype de l'objet enfant.

L'expression enfant.a va retourner la valeur de l'attribut 'a' de l'objet parent car aucun attribut 'a' n'est défini sur l'objet 'enfant'.

Cette règle de récupération des clés/attributs est effectuée en interne dans le moteur Javascript. C'est lui qui détermine s'il faut récupérer l'attribut à partir de l'enfant ou du parent.

Nous n'avons pas à indiquer explicitement que l'on récupère l'attribut du parent. C'est fait de façon transparente par le moteur Javascript.

D'autre part, on accède à l'attribut de l'objet parent et non à une copie. Si on change la valeur de l'attribut sur l'objet parent et que certains de ces enfants n'ont pas un attribut de même nom, alors cette valeur impacte tous ces enfants-là.

Modifier la valeur de l'attribut 'a' de l'objet parent

Représentation interne - Avant
parent: Object
.  a: 10
.  __proto__: Object


enfant: Object
.  __proto__: Object
.  .  a: 10
.  .  __proto__: Object
Javascript:
parent.a = 11;
Représentation interne - Après
parent: Object
.  a: 11
.  __proto__: Object


enfant: Object
.  __proto__: Object
.  .  a: 11
.  .  __proto__: Object
Résultat
L'attribut 'a' a changé pour le parent.

Par héritage, l'attribut 'a' a également changé pour l'enfant.
enfant.a fournit maintenant la valeur '11'

On définit sur l'objet 'enfant' un attribut 'a' de valeur 12

Représentation interne - Avant
parent: Object
.  a: 11
.  __proto__: Object


enfant: Object
.  __proto__: Object
.  .  a: 11
.  .  __proto__: Object
Javascript:
enfant.a = 12;
Représentation interne - Après
parent: Object
.  a: 11
.  __proto__: Object


enfant: Object
.  a: 12
.  __proto__: Object
.  .  a: 11
.  .  __proto__: Object
Résultat
L'attribut 'a' de l'enfant va masquer l'attribut 'a' du parent.

enfant.a fournit maintenant la valeur '12'.

Conclusion

Nous avons vu que par héritage, un objet enfant hérite des attributs du parent qui peuvent être des valeurs : nombre, chaîne de caractères et également des fonctions.

Ceci a pour conséquence que si la fonction du parent change, celle-ci change également pour les enfants.

Javascript apporte un aspect dynamique mais il faut prendre garde aux effets de bord !