POO en JavaScript
Transcription
POO en JavaScript
Classe de terminale SI POO en JavaScript Table des matières 1. Introduction......................................................................................................................................................................2 2. JavaScript et les objets......................................................................................................................................................2 1.1. Objets.......................................................................................................................................................................2 1.2. Pseudo objets...........................................................................................................................................................3 2. Éléments de base de JavaScript........................................................................................................................................3 2.1. Fonctions..................................................................................................................................................................3 2.2. Closures....................................................................................................................................................................4 2.3. Mot clé this..............................................................................................................................................................5 3. Structures des objets avec JavaScript...............................................................................................................................6 3.1. Structure simple.......................................................................................................................................................6 3.2. Prototypage..............................................................................................................................................................8 3.3. Combinaison des deux approches............................................................................................................................9 La programmation orientée objet (POO) est un paradigme de programmation inventé au début des années 1960. Il consiste en la définition et l'interaction de briques logicielles appelées objets ; un objet représente un concept, une idée ou toute entité du monde physique. Il possède une structure interne et un comportement. Il s'agit donc de représenter ces objets et leurs relations ; l'interaction entre les objets via leurs relations permet de concevoir et réaliser les fonctionnalités attendues. 5-POO_js.odt 1 Classe de terminale SI 1. Introduction JavaScript diffère considérablement des langages objet classiques tels que Java et C++ car il se fonde sur la programmation orientée objet par prototype. Son intérêt consiste en son aspect dynamique permettant de modifier la structure des objets après leur création. Le code JavaScript dans le navigateur devenant de plus en plus complexe, une structuration de ces traitements est de plus en plus nécessaire afin de les modulariser, de les rendre maintenables, réutilisables et facilement évolutifs. De nombreuses bibliothèques JavaScript sont actuellement disponibles sur Internet qui utilisent toutes les subtilités des concepts objet du langage afin de rendre la mise en œuvre de JavaScript plus simple et de faciliter son utilisation pour des fonctionnalités graphiques liées à (X)HTML et CSS. 2. JavaScript et les objets Le concept de base de la programmation orientée objet est bien évidemment l'objet, entité désignée également par le terme instance. Il correspond à une entité manipulée dans l'application. Pour tous ceux qui ont pratiqué des langages objet "classiques" tels que Java ou C++, la notion d'objet est associé au concept de classe qui permet de définir la structure de l'objet, à savoir ses attributs et ses méthodes, ainsi que ses liens aux autres classes (héritage, association...). L'objet est alors créé à partir d'une classe par instanciation. Toute la mise en oeuvre de la programmation orientée objet est alors réalisée en se fondant sur les classes (les types) aussi bien au niveau de l'héritage, des associations que du polymorphisme. Bien que JavaScript ait des similitudes avec ces langages, les mécanismes sont totalement différents. 1.1. Objets En JavaScript, la notion de classe n'existe pas et le langage n'est pas typé. Le langage JavaScript permet de créer simplement un objet en se fondant sur l'objet Object ou en utilisant une forme littérale dont la syntaxe est décrite par la notation JSON. Le code suivant illustre la création d'un objet "vierge" avec ces deux techniques: 1. var obj1 = new Object(); // A partir de l'objet Object 2. var obj2 = {}; // Avec la notation JSON Le langage considère les objets en tant que tableau associatif : chaque élément d'un objet correspond à une "entrée" dans l'objet. Cette dernière est identifiée par un nom, le type n'étant connu qu'à l'exécution. Ainsi, un attribut d'un objet correspond à une entrée avec un type quelconque et une méthode à une entrée dont le type attendu est fonction. Une de caractéristiques d'un tableau associatif est le fait que tout est dynamique. En effet, il est possible d'ajouter, de modifier et de supprimer les entrées de l'objet tout au long de sa vie. Le code suivant illustre la mise en œuvre de ces principes en utilisant les différentes notations supportées par JavaScript : var obj = new Object(); obj["attribut"] = "valeur1"; // similaire à obj.attribut = "valeur1"; obj["methode"] = function(parametre1, parametre2) { 5-POO_js.odt 2 Classe de terminale SI alert("parametres: " + parametre1 + ", " + parametre2); }; // similaire à obj.methode = ... // Affichage de la valeur de attribut de obj alert("Valeur de attribut: " + obj.attribut); // Exécution de la méthode methode de obj obj.methode("valeur1", "valeur2"); Le code ci-dessus peut être mis en oeuvre de manière similaire avec la notation JSON dont nous avons commencé à parler précédemment. Elle permet de décrire de manière litérale des tableaux associatifs et donc des objets JavaScript car, comme nous l'avons dit précédemment, ces deux notions sont similaires en JavaScript. Le code suivant décrit comme mettre en oeuvre l'exemple précédent avec la notation JSON : var obj = { attribut: "valeur", methode: function(parametre1, parametre2) { alert("parametres: " + parametre1 + ", " + parametre2); } } // Affichage de la valeur de attribut de obj alert("Valeur de attribut: " + obj.attribut); // Exécution de la méthode methode de obj obj.methode("valeur1", "valeur2"); Comme vous avez pu le constater dans les deux exemples précédents, rien n'est typé et le rattachement d'attributs et de méthodes à un objet est dynamique. De plus, l'exemple précédent illustre le fait que les fonctions JavaScript sont pris en compte par le langage en tant qu'objet. Les conséquences de cet aspect sont la possibilité de les affecter et les référencer. 1.2. Pseudo objets A l'instar des fonctions, les types primitifs, les chaînes de caractères et les tableaux sont en JavaScript des pseudo-objets qui peuvent être créés par une forme littérale mais également par instanciation. Ces pseudo-objets possèdent également des attributs et des méthodes et il n'est pas rare de voir ce genre de code JavaScript : // Chaîne de caractères var uneChaine = "Ma chaine de caractères"; var uneAutreChaine1 = uneChaine.toString(); var uneAutreChaine2 = uneChaine.substring(0, 10); 2. Éléments de base de JavaScript 2.1. Fonctions Contrairement à Java et C++, une fonction peut être référencée par l'intermédiaire d'une variable et être utilisée telle quelle par la suite. En JavaScript, une fonction est donc implicitement considérée et manipulée en tant qu'objet de type Function et ce, quelque soit la manière dont elle a été créée. 5-POO_js.odt 3 Classe de terminale SI Par la suite, nous désignons par le terme méthode une fonction lorsqu'elle est rattachée à un objet. Une fonction en elle-même existe et peut être appelée sans pour autant avoir d'objet rattaché. Lorsqu'une fonction est rattachée à un objet, la référence à cette dernière est appelée méthode. Si une fonction est définie explicitement pour un objet, elle est également désignée par méthode. Une des conséquences est que les fonctions JavaScript possèdent des attributs et des méthodes. L'attribut qui nous intéresse particulièrement est prototype. Comme son nom l'indique, il va être utilisé pour mettre en œuvre la programmation orientée objet par prototype en permettant de définir la structure des objets. Les fonctions JavaScript possèdent également d'autres particularités. Tout d'abord, le langage ne se fonde pas sur le concept de signature afin de les identifier à l'exécution mais uniquement sur leurs noms. Cela conduit à des comportements assez inattendus puisque, quand deux fonctions portent le même nom, c'est la dernière définie qui est exécutée et ce quelque soit les paramètres qui lui sont passés. Le code suivant illustre ce fonctionnement: function test(parametre1) { alert(parametre1); } function test(parametre1, parametre2) { alert(parametre1 + "," + parametre2); } test("une valeur"); // appelle la seconde fonction Afin de gérer les paramètres passés à une fonction, le langage JavaScript met à disposition la variable arguments dans les fonctions. Cette dernière correspond à un tableau contenant les différents paramètres passés à la fonction lors de son appel. Cet élément de langage offre la possibilité à une fonction de gérer différents appels avec un nombre de paramètres différent. Le code suivant illustre l'utilisation de la variable arguments dans une fonction : function test() { alert("Nombre de paramètres: " + arguments.length); for(var i=0; i<arguments.length; i++) { alert("Paramètre " + i + ": " + arguments[i]); } } test("valeur1", "valeur2"); test("valeur1", "valeur2", "valeur3", "valeur4"); 2.2. Closures D'autres fonctionnalités de JavaScript en rapport avec les fonctions sont le support par le langage des closures et la possibilité de définir des fonctions dans le corps de fonctions. Les closures correspondent à des fonctions particulières qui peuvent utiliser des variables définies en dehors de leur portée. Une utilisation intéressante consiste en la possibilité d'accéder aux variables définies dans une fonction contenant à partir de la fonction contenue, comme l'illustre le code suivant : function maFonction(parametre) { var maVariable = parametre; function monAutreFonction() { alert("maVariable : " + maVariable); 5-POO_js.odt 4 Classe de terminale SI } return monAutreFonction; } var fonction = maFonction("mon paramètre"); fonction(); // Affiche la valeur "mon paramètre" Ce code permet de comprendre le mécanisme des closures sur lequel va se fonder JavaScript afin de mettre en œuvre la programmation orientée objet. La fonction maFonction retourne une fonction, ce qui est possible avec JavaScript puisque le langage considère les fonctions comme des objets et qu'elles peuvent être affectées... De plus, la définition de la fonction retournée se trouve dans le corps même de la fonction maFonction et qu'elle utilise une variable locale de cette dernière. Quand la fonction retournée est exécutée, elle utilise la valeur de cette variable même si la fonction est exécutée en dehors de maFonction. 2.3. Mot clé this Le mot clé this est utilisé dans une méthode afin de référencer l'instance sur laquelle est exécutée cette méthode. Il faut néanmoins faire attention lorsque l'on utilise this dans une fonction qui n'est pas rattachée à un objet car soit une erreur se produit ou des champs possèdent des valeurs non définies. L'exemple suivant illustre la mise en œuvre de ce mot clé : var maFonction = function() { alert("attribut: " + this.attribut); }; // Affiche la valeur undefined car this.attribut ne peut être résolu maFonction(); // Création de l'objet obj1 et affectation de maFonction var obj1 = { attribut: "valeur1", methode: maFonction } obj1.methode(); // Affiche la valeur de attribut, à savoir valeur1 // Création de l'objet obj2 et affectation de maFonction var obj2 = { attribut: "valeur2", methode: maFonction } obj2.methode(); // Affiche la valeur de attribut2, à savoir valeur2 La valeur affichée par la fonction maFonction change en fonction de l'objet à laquelle elle est rattachée. Le référencement d'une méthode de classe pose un souci au moment de l'exécution car il ne spécifie pas sur quel objet la méthode sera par la suite exécutée. Les fonctions sont considérées par le langage JavaScript comme des pseudo objets. Elles possèdent deux intéressantes méthodes, les méthodes apply et call, qui permettent d'exécuter des fonctions dans le contexte d'un objet. L'unique différence entre ses deux méthodes consistent dans le passage des paramètres. La première (apply) utilise un tableau 5-POO_js.odt 5 Classe de terminale SI pour ces paramètres tandis que la seconde (call) les place en paramètre de l'appel. Le code suivant illustre la mise en œuvre de ces deux méthodes : function maFonction(parametre1, parametre2) { alert("Parametres: " + parametre1 + ", " + parametre2 + " - Attribut: " + this.attribut); } var obj1 = { attribut: "valeur1", } var obj2 = { attribut: "valeur2", } maFonction.apply(obj1, [ "valeur1", "valeur2" ]); maFonction.call(obj1, "valeur1", "valeur2"); maFonction.apply(obj2, [ "valeur1", "valeur2" ]); maFonction.call(obj2, "valeur1", "valeur2"); 3. Structures des objets avec JavaScript Bien que la notion de classe n'existe pas en JavaScript, le langage fournit néanmoins la possibilité de définir des structures d'objet qui sont utilisés lors de leur instanciation. De ce fait, il n'est pas rare de voir sur internet l'utilisation du mot classe, d'interfaces et de classes abstraites, des notions qui existent pas mais qui permettent de clarifier la modélisation objet de l'API. Il est important de noter que JavaScript ne fournit pas de manière unifiée de mettre en œuvre ce paradigme. Chaque approche a ses propres spécificités et est plus adéquate dans certains cas d'utilisation. 3.1. Structure simple Le langage JavaScript ne supporte pas le concept de classe mais il est néanmoins possible de le simuler afin de définir la structure d'objets en se fondant sur les concepts des fonctions et closures du langage. Le point important à comprendre à ce niveau est l'utilisation du mot clé new. En JavaScript, ce dernier peut être utilisé en se fondant sur une fonction afin d'initialiser un objet. L'initialisation est réalisée en utilisant les éléments contenus dans la fonction, ces derniers pouvant être aussi bien des attributs que des méthodes : function MaClasse(parametre1, parametre2) { this.attribut1 = parametre1; this.attribut2 = parametre2; this.methode = function() { alert("Attributs: " + this.attribut1 + ", " + this.attribut2); } } var obj = new MaClasse("valeur1", "valeur2"); // Affiche la valeur de l'attribut attribut1 obj.methode(); // Affiche la chaîne de caractères contenant les valeurs des attributs 5-POO_js.odt 6 Classe de terminale SI obj.methode(); Dans le code ci-dessus que la fonction MaClasse permet de définir le nom de la classe et correspond au constructeur de cette dernière. Ainsi les paramètres de la fonction permettent d'initialiser la classe. Nous notons également l'utilisation du mot clé this qui permet de définir des éléments publiques de la classe. Attention à ne pas oublier le mot clé this pour référencer les attributs attribut1 et attribut2 dans la méthode methode sous peine d'erreurs et même si l'on se trouve dans la classe elle-même. Le précédent exemple permet donc de définir une classe MaClasse dont la figure suivante illustre sa structure dans un diagramme de classes UML. Cette première approche permet de gérer la visibilité des éléments d'une classe. En effet, en supprimant this. devant les attributs attribut1 et attribut2, ces derniers ne sont plus accessibles qu'en interne à la classe et sont alors de visibilité privée, comme l'illustre le code suivant : function MaClasse(parametre1, parametre2) { var attribut1 = parametre1; var attribut2 = parametre2; this.methode = function() { alert("Attributs: " + attribut1 + ", " + attribut2); } } var obj = new MaClasse("valeur1", "valeur2"); alert("Attribut1: " + obj.attribut1); // Affiche la valeur undefined (attribut1 ne peut être résolu) obj.methode(); // Affiche la chaîne de caractères contenant les valeurs des attributs Cet aspect peut également être décliné afin de mettre en œuvre des méthodes privées. Dans ce cas, les variables référençant les méthodes de classe sont des variables locales au constructeur, comme l'illustre le code suivant : function MaClasse(parametre1, parametre2) { var attribut1 = parametre1; var attribut2 = parametre2; var methode = function() { alert("Attributs: " + attribut1 + ", " + attribut2); } } var obj = new MaClasse("valeur1", "valeur2"); alert("Attribut1: " + obj.attribut1); // Affiche la valeur undefined (attribut1 ne peut être résolu) try { obj.methode(); } catch(err) { print(err); } 5-POO_js.odt 7 Classe de terminale SI // Génère une erreur car la méthode ne peut pas être résolue à l'extérieur de la classe Cette approche consiste en la manière la plus simple de mettre en œuvre des classes en JavaScript mais elle souffre d'une limitation. En effet, à chaque fois que la méthode de construction de l'objet est appelée, une nouvelle méthode methode est créée pour l'objet. Aussi, si dix objets de type MaClasse sont créés, dix méthodes methode sont créées. Cet aspect a des impacts sur les performances et la consommation mémoire surtout dans des applications JavaScript utilisant beaucoup d'objets du type MaClasse. Le comportement souhaité serait que tous les objets pointent vers la méthode methode. La fonctionnalité de prototypage de JavaScript permet de pallier à cet aspect. 3.2. Prototypage Le concept de prototypage correspond à spécifier une sorte de modèle indépendamment du constructeur afin d'initialiser chaque objet à sa création. Comme nous l'avons mentionné précédemment, la spécification de ce modèle se réalise en se fondant sur la propriété prototype de la classe Function. Il convient donc ainsi de toujours de créer une fonction constructeur afin de définir une classe. Cependant, contrairement à l'approche précédente, les éléments de la classe ne sont plus tous définis dans cette fonction. La propriété prototype s'initialise en se fondant sur un objet ou un tableau associatif. Le code suivant illustre l'adaptation de la classe MaClasse en se fondant sur le prototypage : function MaClasse(parametre1, parametre2) { this.attribut1 = parametre1; this.attribut2 = parametre2; } MaClasse.prototype = { methode: function() { alert("Attributs: " + this.attribut1 + ", " + this.attribut2); } } var obj = new MaClasse("valeur1", "valeur2"); // Affiche la valeur de l'attribut attribut1 alert("Attribut1: " + obj.attribut1); // Affiche la chaîne de caractères contenant les valeurs des attributs obj.methode(); La methode méthode n'est plus définie dans le corps de la fonction MaClasse mais dans un bloc bien distinct. La visibilité privée n'est alors plus possible. D'un autre côté, toutes les instances de la classe MaClasse pointent vers la même méthode methode, cette dernière étant définie dans le prototype associée à la classe. Cette fonctionnalité permet donc de corriger ainsi le problème soulevé précédemment. L'objet spécifié au niveau du prototype permet également de préciser des attributs, ces derniers servant de valeurs par défaut aux attributs des objets. Une des caractéristiques importantes du prototypage est que les modifications de l'objet qui lui est associé, sont appliquées sur tous les objets qui vont été instanciés. Par contre, les objets précédemment instanciés ne sont pas impactés. Le code suivant illustre cette aspect : function MaClasse() { this.attribut = "valeur"; } 5-POO_js.odt 8 Classe de terminale SI var obj1 = new MaClasse(); try { obj1.methode(); // Erreur car la méthode n'existe pas pour l'objet } catch(err) { alert("Erreur: " + err); } MaClasse.prototype = { methode: function() { alert("Attribut: " + this.attribut); } } var obj2 = new MaClasse(); obj2.methode(); // Fonctionne correctement car la méthode a été ajoutée au prototype de MaClasse La modification du prototype d'une classe peut être mis en œuvre aussi bien sur nos objets et classes que sur des objets et classes de JavaScript ou sur ceux fournis par l'environnement d'exécution. Comme nous le verrons par la suite, cet aspect est utilisé dans des bibliothèques. 3.3. Combinaison des deux approches Une dernière approche consiste à combiner la première approche avec celle fondée sur le prototypage. Dans ce cas, l'initialisation de la propriété prototype est réalisée dans le code du constructeur de la classe, ceci offrant la possibilité d'avoir accès à toutes les variables et méthodes de cette fonction particulière. Par contre, les principes décris dans les diffé rentes approches restent vrais. Les méthodes définies directement dans le constructeur seront dupliquées alors que celles positionnées sur le prototype non. Cette approche permet de forcer la propriété prototype à n'être initialisée qu'une seule et unique fois la première fois que le constructeur est appelé. Pour ce faire, il suffit d'ajouter une propriété personnalisée directement sur le constructeur de la manière suivante : function MaClasse() { this.attribut = "valeur"; if ( typeof MaClasse.initialized == "undefined" ) { MaClasse.prototype.methode = function() { alert("Attribut: " + this.attribut); }; MaClasse.initialized = true; } } var obj = new MaClasse(); alert(obj.attribut); // Affiche la valeur de l'attribut attribut obj.methode(); // Fonctionne correctement car la méthode a été ajoutée au prototype de MaClasse 5-POO_js.odt 9