Programmation orientée objet avec le langage JavaScript (2ème partie)
Date de publication : 16/07/2007 , Date de mise à jour : 03/09/2007
Par
Thierry Templier (co-auteur du livre JavaScript pour le Web 2.0)
Cette série d'articles décrit la mise en oeuvre de la programmation orientée objet par prototype avec le langage
JavaScript. Pour ce faire, il détaille les différents mécanismes du langage relatifs à ce paradigme tout
en mettant l'accent sur les pièges à éviter.
0. Introduction
0.1. Exécution des exemples de code
1. Héritage
1.1. Utilisation du constructeur de la classe mère
1.2. Utilisation du prototypage
1.3. Affectation d'éléments
1.4. Combinaison des stratégies
1.5. Héritage multiple
1.6. Récapitulatif
2. Détection du type
2.1. Mot clé typeof
2.2. Mot clé instanceof
3. Conclusion
4. Bibliographie
0. Introduction
Dans le premier article [1] de cette série, nous avons décrit les différents mécanismes de base
du langage JavaScript relatif à la programmation orientée objet. Nous avons vu que
ce langage utilisait une variante de ce paradigme, à savoir la programmation orientée
objet par prototype [2]. Ainsi, bien que ce langage soit orienté objet, il différe
considérablement des langages objet classiques tels que Java et C++ puisqu'il ne
dispose pas, entre autres choses, du mot clé class et se fonde sur les fonctions et le prototypage
afin de définir des classes.
Dans ce second article, nous allons continuer de décrire les différents mécanismes du paradigme
afin de mettre en oeuvre l'héritage d'objets et de classes. Nous verrons que, à l'instar de ses fondations,
le langage JavaScript ne possède pas d'élément de langage tel que le mot clé extends afin de
relier des classes par des liens d'héritage. Ainsi plusieurs stratégies peuvent être utilisées
avec leurs avantages et leurs inconvénients respectifs dépendant des situations d'utilisation.
Tout comme pour le premier, l'objectif de cet article est de clarifier l'utilisation de JavaScript
quand à la programmation orientée objet et mettre en lumière des fonctionnalités intéressantes afin
d'améliorer la structuration, la maintenabilité et l'évolutivité des pages Web ou applications utilisant ce
langage. L'utilisation de ces différents mécanismes dans cette optique seront abordées dans le dernier
article [3] de la série.
0.1. Exécution des exemples de code
Afin de tester les exemples de code fournis dans cet article, nous vous conseillons
d'utiliser l'outil Rhino [4], l'implémentation de JavaScript en open source et en
Java de Mozilla.
Etant très légère, cette implémentation permet donc de tester rapidement des scripts
JavaScript en dehors de navigateurs web par l'intermédiaire d'une une console interactive
d'exécution fournie par l'outil. Cette dernière peut également être utilisée pour
exécuter un fichier de scripts. Afin de lancer la console, vous pouvez utiliser le script
de lancement (rhino.bat) suivant, script fonctionnant sous windows:
set JAVA_HOME=C:\applications\jdk1.5.0_07
set RHINO_HOME=C:\applications\rhino1_6R3
%JAVA_HOME%\bin\java -classpath %RHINO_HOME%\js.jar org.mozilla.javascript.tools.shell.Main -f %1
|
Il prend en paramètre le fichier de script à exécuter et affiche les différents messages
sur le sortie standard de la console. Ces messages peuvent être applicatifs en se fondant
sur la fonction print ou résultant d'erreurs de syntaxe des scripts. L'équivalent de ce script
pour unix (rhino.sh) est décrit ci-dessous:
#!/bin/sh
JAVA_HOME=/applications/jdk1.5.0_07
RHINO_HOME=/applications/rhino1_6R3
$JAVA_HOME/bin/java -classpath $RHINO_HOME/js.jar org.mozilla.javascript.tools.shell.Main -f $1
|
Tous les scripts de l'article sont fournis sous forme de fichiers qui peuvent être passés
en paramètre de ce script de lancement. Ces derniers sont téléchargeables au niveau de chaque
portion de code de l'article. Néanmoins, si vous préférez tester l'exécution des scripts
dans un navigateur, des fichiers HTML de tests sont également fournis avec des traitements
identiques.
Maintenant que le décor a été planté, commençons la description des différents
concepts de JavaScript relatifs à l'héritage de la programmation orientée objet.
1. Héritage
Dans cette section, nous allons décrire les différentes manières de mettre en oeuvre
l'héritage en JavaScript. Ces différentes techniques ne sont pas équivalentes
et ne sont pas utilisables dans tous les cas à l'instar de la mise en oeuvre des objets
et des classes avec ce langage, mise en oeuvre détaillée dans le premier article de
la série [1].
 |
Comme nous l'avions indiqué dans le premier article, il est à noter que la notion de classe
n'existe pas en JavaScript. Nous utilisons néanmoins cette notion dans ce contexte afin de
désigner la structure des objets, de simplifier et clarifier les explications.
|
Afin de détailler ces différents mécanismes, nous nous fonderons sur les entités décrites
dans la figure suivante, à savoir la classe MaClasse et sa classe mère
MaClasseMere dans le cas d'un héritage simple:
Dans la section relative à l'héritage multiple, nous ajouterons une classe mère dénommée MonAutreClasseMere
à la classe MaClasse afin de décrire dans quelle proportion le langage JavaScript
supporte l'héritage multiple. La figure suivante décrit les différentes entités mises en oeuvre alors:
1.1. Utilisation du constructeur de la classe mère
Comme nous l'avons vu dans le premier article [1], la construction d'un objet peut se
réaliser par l'intermédiaire d'une fonction de construction, fonction mise en oeuvre
lors de l'utilisation du mot clé new. Cette fonction utilise en son sein le mot
clé this afin de spécifier les attributs et méthodes publics de classe.
Comme nous l'avons également décrit, le mot clé référence l'objet sur lequel est
exécutée la méthode. Dans le cas du contructeur utilisé conjointement avec le mot clé
new, this correspond à l'objet immédiatement instancié. La fonction
de construction peut néanmoins être utilisée avec n'importe quel autre objet (et pas nécessairement
avec le mot clé new) et, par exemple, avec celui que nous voulons faire hériter.
La technique la plus satisfaisante consiste en l'utilisation de la méthode call [5]
de la fonction de construction de la classe mère. Cette méthode permet d'exécuter ce constructeur
dans le contexte de la classe fille en se fondant sur le mot clé this. Ainsi, les différents
éléments spécifiés dans ce constructeur sont ajoutés à la classe fille et seront donc présents
pour tous les objets de ce type.
Le code suivant décrit la mise en oeuvre de cette technique afin de définir une sous
classe MaClasse pour la classe MaClasseMere, ainsi que le décrit la figure
en début d'article:
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. |
| function MaClasseMere(parametre1, parametre2) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
|
| this.methode = function() {
| alert("[methode] Attributs: " + this.attribut1 + ", " + this.attribut2);
| }
| }
|
| function MaClasse(parametre1, parametre2, parametre3) {
| MaClasseMere.call(this, parametre1, parametre2);
| this.attribut3 = parametre3;
|
| this.uneMethode = function() {
| alert("[uneMethode] Attributs: " + this.attribut1
| + ", " + this.attribut2 + ", " + this.attribut3);
| }
| }
|
| var obj = new MaClasse("parametre1", "parametre2", "parametre3");
| obj.methode();
|
| obj.uneMethode();
|
|
|
|
Le code précédent montre bien que les attributs attribut1 et attribut2 ainsi que la méthode
methode ont été ajoutés à la classe MaClasse puisqu'ils sont accessibles et utilisables
au niveau de toutes les instances de ce type. Bien que cela ne soit pas imposé, nous recommendons d'appeler le
constructeur de la classe mère en tant que première instruction de la fonction de construction de la
classe fille.
Comme nous pouvons le remarquer dans le code ci-dessus, l'appel explicite au constructeur de la classe mère
dans celui de la classe fille permet de lui passer différents paramètres afin d'initialiser les attributs
définis dans la classe mère.
Le principal inconvénient de cette approche consiste en le fait que la classe fille n'hérite
pas des éléments de la classe mère définis au niveau de son prototype. Elle permet donc de ne résoudre qu'une
partie de la problématique.
1.2. Utilisation du prototypage
Comme nous l'avons décrit dans le premier article [1], le langage JavaScript met en oeuvre la propriété
prototype de la classe Function [6] afin de définir la structure d'un objet. Dans
le cas de l'héritage, il est possible d'initialiser cette propriété avec un objet créé à partir du
constructeur de la classe mère. Avec cette technique, la classe fille possède automatiquement
tous les attributs et méthodes de la classe mère définis aussi bien au niveau de son constructeur que de son
prototype. Le code suivant illustre la mise en oeuvre de cette approche:
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. | 27. |
| function MaClasseMere(parametre1, parametre2) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
|
| this.methode = function() {
| alert("[methode] Attributs: " + this.attribut1 + ", " + this.attribut2);
| }
| }
|
| function MaClasse(parametre1, parametre2, parametre3) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
| this.attribut3 = parametre3;
|
| this.uneMethode = function() {
| alert("[uneMethode] Attributs: " + this.attribut1
| + ", " + this.attribut2 + ", " + this.attribut3);
| }
| }
|
| MaClasse.prototype = new MaClasseMere();
|
| var obj = new MaClasse("parametre1", "parametre2", "parametre3");
| obj.methode();
|
| obj.uneMethode();
|
|
|
|
Nous pouvons remarquer qu'avec cette technique, les paramètres du constructeur de la classe mère
ne peuvent pas être utilisés puisque l'appel de ce dernier pour l'affectation à la propriété prototype
se réalise bien en amont de la création des objets. Aussi, si les attributs de la classe mère doivent
être initialisés, cette opération doit être réalisée manuellement sous peine de posséder des valeurs
undefined. Dans l'exemple précédent, l'initialisation des attributs attribut1 et attribut2
se réalisent dans la constructeur de la classe MaClasse aux lignes 12 et 13. Nous remarquons
également que les traitements d'initialisation de ces lignes sont dupliquées dans les classes MaClasseMere
et MaClasse.
Afin de pallier à cet inconvénient, le constructeur de la classe mère peut néanmoins être appelé à partir du
constructeur de la classe fille comme décrit précédemment. Certains traitements peuvent être alors
redondants, notamment ceux qui se trouvent en dehors du prototype de la classe mère. Le code suivant illustre
cette aspect (ligne 11) afin d'initialiser les valeurs des attributs attribut1 et attribut2 en se fondant sur
le constructeur de la classe mère:
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. |
| function MaClasseMere(parametre1, parametre2) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
|
| this.methode = function() {
| alert("[methode] Attributs: " + this.attribut1 + ", " + this.attribut2);
| }
| }
|
| function MaClasse(parametre1, parametre2, parametre3) {
| MaClasseMere.call(this, parametre1, parametre2);
| this.attribut3 = parametre3;
|
| this.uneMethode = function() {
| alert("[uneMethode] Attributs: " + this.attribut1
| + ", " + this.attribut2 + ", " + this.attribut3);
| }
| }
|
| MaClasse.prototype = new MaClasseMere();
|
| var obj = new MaClasse("parametre1", "parametre2", "parametre3");
| obj.methode();
|
| obj.uneMethode();
|
|
|
|
Dans les exemples de cette section, nous avons utilisé des classes dont tous les constituants (attributs et
méthodes) sont définis au niveau de leurs constructeurs. Nous avons souligné, dans le précédent article, que cette
approche souffrait d'une importante limitation relative à la duplication des méthodes [7]. Pour pallier à cela,
nous avons recommandé de définir toutes les méthodes d'une classe dans l'attribut prototype associé
à la fonction de construction de la classe. Qu'en est-il au niveau de la mise en oeuvre de l'héritage avec
la technique décrite dans cette section?
Le piège à ce niveau se situe au niveau de la spécification des éléments sur le prototype de la
classe fille. En effet, il faut bien faire attention à ne pas écraser ce qui a été défini
précédemment. A cet effet, l'affectation du prototype avec une instance de la classe mère doit
être réalisée en premier lieu. Par la suite, un tableau associatif ne peut pas être affecté
directement comme nous avions l'habitude de le faire dans le premier article. En effet, cette
façon de faire aurait pour conséquence la perte de tous les éléments de la classe mère. Une
affectation élément par élément doit être préférée, comme l'illustre le code suivant qui adapte
le précédent exemple:
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. | 27. |
| function MaClasseMere(parametre1, parametre2) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
| }
|
| MaClasseMere.prototype = {
| methode: function() {
| alert("[methode] Attributs: " + this.attribut1 + ", " + this.attribut2);
| }
| }
|
| function MaClasse(parametre1, parametre2, parametre3) {
| MaClasseMere.call(this, parametre1, parametre2);
| this.attribut3 = parametre3;
| }
|
| MaClasse.prototype = new MaClasseMere();
| MaClasse.prototype.uneMethode = function() {
| alert("[uneMethode] Attributs: " + this.attribut1
| + ", " + this.attribut2 + ", " + this.attribut3);
| }
|
| var obj = new MaClasse("parametre1", "parametre2", "parametre3");
| obj.methode();
|
| obj.uneMethode();
|
|
|
|
En résumé, l'avantage de la stratégie décrite dans cette section est que la classe fille hérite de tous les
constituants définis aussi bien au niveau du constructeur que du prototype de par l'instanciation de la
classe mère. Les principaux inconvénients de cette approche consiste en le fait que certains traitements peuvent être exécutés
deux fois lors de l'utilisation du constructeur et que la propriété prototype doit être complètement réinitialisée avec une
instance de la classe mère. En effet, si des éléments ont été spécifiés précédemment sur cette propriété, ils ne seront plus présents
par la suite. Cet aspect reste néanmoins négligable dans la plupart des cas si ce n'est lorsque
l'on désire mettre en oeuvre l'héritage multiple ou enrichir des classes existantes.
1.3. Affectation d'éléments
Avec le langage JavaScript, l'héritage peut également et simplement signifier une affectation
manuelle des éléments de la classe mère à la classe fille. Comme nous l'avons décrit dans l'article
précédent, un objet n'est autre qu'un tableau associatif dont chaque constituant correspond à une de ses
entrée. De plus, JavaScript offre une manière efficace au niveau même du langage afin de
parcourir ce type de structure de données en se fondant sur le mot clé for conjointement utilisé
avec le mot clé in.
Nous allons mettre en oeuvre maintenant la fonction heriter qui référence dans un tableau
associatif les éléments d'un autre tableau associatif en se fondant sur les différents supports du langage
JavaScript. Le code suivant illustre l'implémentation de cette fonction:
| function heriter(destination, source) {
| for (var element in source) {
| destination[element] = source[element];
| }
| }
|
|
|
 |
Attention, il ne s'agit pas d'une recopie d'éléments d'une entité vers une autre mais bien d'un référencement
de ces éléments par l'entité cible tout en gardant les mêmes noms d'entrée.
|
 |
La plupart des bibliothèques JavaScript disponibles sur Internet possèdent une fonction de
ce type sur laquelle se fondent certains de leurs traitements. C'est le cas de la bibliothèque Prototype [8]
avec la fonction Object.extend et de la bibliothèque Dojo [9] avec la fonction dojo.inherits.
|
Ainsi, faire hériter une classe d'une autre peut être mis en oeuvre en se basant sur la fonction
heriter dont les paramètres sont simplement les prototypes des classes fille et mère. Le code suivant illustre
comment faire hériter la classe MaClasse de la classe MaClasseMere en se fondant sur cette approche:
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. | 27. | 28. | 29. | 30. | 31. | 32. | 33. |
| (...)
|
| function MaClasseMere(parametre1, parametre2) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
| }
|
| MaClasseMere.prototype = {
| methode: function() {
| alert("[methode] Attributs: " + this.attribut1 + ", " + this.attribut2);
| }
| }
|
| function MaClasse(parametre1, parametre2, parametre3) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
| this.attribut3 = parametre3;
| }
|
| MaClasse.prototype = {
| uneMethode: function() {
| alert("[uneMethode] Attributs: " + this.attribut1
| + ", " + this.attribut2 + ", " + this.attribut3);
| }
| }
|
| heriter(MaClasse.prototype, MaClasseMere.prototype);
|
| var obj = new MaClasse("parametre1", "parametre2", "parametre3");
| obj.methode();
|
| obj.uneMethode();
|
|
|
|
De plus, avec cette stratégie, l'héritage peut également être mis en oeuvre au niveau des objets plutôt qu'au
niveau des classes. Le même mécanisme peut être utilisé au détail près que la fonction heriter
prend désormais en paramètres les objets eux-mêmes plutôt que les prototypes de leurs classes associées.
Le code suivant illustre la mise en oeuvre de cet aspect:
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. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. |
| (...)
|
| function MaClasseMere(parametre1, parametre2) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
| }
|
| MaClasseMere.prototype = {
| methode: function() {
| alert("[methode] Attributs: " + this.attribut1 + ", " + this.attribut2);
| }
| }
|
| function MaClasse(parametre1, parametre2, parametre3) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
| this.attribut3 = parametre3;
| }
|
| MaClasse.prototype = {
| uneMethode: function() {
| alert("[uneMethode] Attributs: " + this.attribut1
| + ", " + this.attribut2 + ", " + this.attribut3);
| }
| }
|
| var obj1 = new MaClasseMere("parametre1", "parametre2");
| var obj2 = new MaClasse("parametre1", "parametre2", "parametre3");
|
| heriter(obj2, obj1);
|
| obj2.methode();
|
| obj2.uneMethode();
|
|
|
|
Comme nous l'avons vu, la fonction heriter peut être utilisée afin de faire hériter une classe
d'une autre. Elle peut également être mise en oeuvre afin d'enrichir une classe ou un objet existant
avec de nouvelles méthodes et ce, aussi bien sur nos propres classes ou objets que de ceux de
JavaScript ou de ceux fournis par l'environnement d'exécution.
Prenons un exemple. La classe String ne fournit pas de méthodes afin de mettre en majuscule ou minuscule
la première lettre d'une chaîne de caractères. Ces méthodes peuvent être intéressantes afin de déduire le nom
d'une instance du nom d'une classe et inversement. Par le biais de la fonction heriter, il est
possible d'ajouter simplement ces deux méthodes à cette classe. Le code suivant illustre la mise en oeuvre de cet
aspect en se fondant sur la fonction précédemment citée et un tableau associatif:
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. |
| (...)
|
| heriter(String.prototype, {
| firstLower: function() {
| var premierLettre = this.charAt(0);
| premierLettre = premierLettre.toLowerCase();
| return premierLettre + this.substring(1);
| },
|
| firstUpper: function() {
| var premierLettre = this.charAt(0);
| premierLettre = premierLettre.toUpperCase();
| return premierLettre + this.substring(1);
| }
| });
|
| var nomClasse = "MaClasse";
| alert(nomClasse.firstLower());
|
| var nomInstance = "maClasse";
| alert(nomInstance.firstUpper());
|
|
|
 |
Il est à noter que cet enrichissement de classes existantes ne sera effectif qu'après l'appel de la
fonction heriter dans notre code précédent. Certaines bibliothèques JavaScript telles que Prototype [8]
enrichissent des classes et objets de cette manière au moment où le fichier js de la bibliothèque
est inclu dans les pages HTML.
|
1.4. Combinaison des stratégies
Comme nous l'avons vu tout au long de cet article, deux aspects doivent être pris en compte afin de
supporter complètement l'héritage en JavaScript. Le premier se situe au niveau du constructeur
de la classe mère et le second au niveau du prototype de cette même classe. En effet,
les constituants des classes peuvent être définis à ces deux niveaux. Comme nous l'avons vu
tout au long de cet article, différentes approches peuvent être mises en oeuvre afin de gérer l'héritage
avec le langage JavaScript. Nous ne détaillerons dans cette section que les approches fondées sur
le prototypage afin de définir la structure des objets et sur la fonction de construction afin
d'initialiser les éléments de la classe.
Dans cette section, nous allons réutiliser la classe MaClasse et sa classe mère MaClasseMere.
Nous allons nous baser sur une définition de leurs structures de la manière décrite dans le code ci-dessous:
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. |
| function MaClasseMere(parametre1, parametre2) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
| }
|
| MaClasseMere.prototype = {
| methode: function() {
| alert("[methode] Attributs: " + this.attribut1 + ", " + this.attribut2);
| }
| }
|
| function MaClasse(parametre1, parametre2, parametre3) {
| this.attribut1 = parametre1;
| this.attribut2 = parametre2;
| this.attribut3 = parametre3;
|
|
|
|