Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
ACCUEIL JAVASCRIPT FORUM JAVASCRIPT F.A.Q JAVASCRIPT TUTORIELS JAVASCRIPTS SOURCES JAVASCRIPT LIVRES AJAX

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.

               Version PDF   Version hors-ligne

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].

info 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:
Télécharger
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(); 
// Affiche les valeurs des attributs attribut1 et attribut2 
obj.uneMethode(); 
// Affiche les valeurs des attributs attribut1, attribut2 et attribut3 
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:
Télécharger
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(); 
// Affiche les valeurs des attributs attribut1 et attribut2 
obj.uneMethode(); 
// Affiche les valeurs des attributs attribut1, attribut2 et attribut3 
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:
Télécharger
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(); 
// Affiche les valeurs des attributs attribut1 et attribut2 
obj.uneMethode(); 
// Affiche les valeurs des attributs attribut1, attribut2 et attribut3 
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:
Télécharger
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(); 
// Affiche les valeurs des attributs attribut1 et attribut2 
obj.uneMethode(); 
// Affiche les valeurs des attributs attribut1, attribut2 et attribut3 
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:
Télécharger
1.
2.
3.
4.
5.
function heriter(destination, source) { 
    for (var element in source) { 
        destination[element] = source[element]; 
    } 
} 
warning 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.
info 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:
Télécharger
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(); 
// Affiche les valeurs des attributs attribut1 et attribut2 
obj.uneMethode(); 
// Affiche les valeurs des attributs attribut1, attribut2 et attribut3 
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:
Télécharger
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(); 
// Affiche les valeurs des attributs attribut1 et attribut2 
obj2.uneMethode(); 
// Affiche les valeurs des attributs attribut1, attribut2 et attribut3 
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:
Télécharger
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()); // Affiche maClasse 
 
var nomInstance = "maClasse"; 
alert(nomInstance.firstUpper()); // Affiche MaClasse 
info 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:
Télécharger
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;