ATTENTION : Tutorial en cours d'écriture ! N'hésitez pas à nous signaler toute erreur ou suggestion.
Accès rapide : Rappels sur la programmation orientée en Javascript La notion de prototype en Javascript Héritage et polymorphisme en Javascript La notion de type de données au sein de l'API JWT
Nous allons, dans ce document, nous intéresser à la programmation orientée objet en Javascript. Pour ce faire, nous allons commencer par rappeler les concepts fondamentaux inhérents à Javascript, puis nous verrons en quoi le framework NWS et l'API JWT peuvent vous simplifier les choses lors de vos développements objets en Javascript. Ce chapitre vous permettra aussi de mieux comprendre comment les différents types de composants graphiques JWT ont été développés (Composants de barres de menu, composant d'affichage de flux RSS, ...).
Javascript est un langage orienté objet : il vous propose donc un ensemble de constructions syntaxiques permettant de mettre en oeuvre ce modèle de programmation. Néanmoins, il faut aussi noter que Javascript est un langage faiblement typé : on ne type pas les variables, mais ce sont les données elles mêmes qui portent les informations de types. Il en découle (en partie) une caractéristique remarquable : on n'a pas, à proprement parler, de concepts de classe en Javascript. D'où la question que vous vous posez certainement : comment faire de la programmation orientée objet sans le concept de classe ?
La solution retenue par les concepteurs de Javascript consiste à dire qu'un objet sera en fait un tableau associatif : pour une clé (alphanumérique) donnée on lui associe une valeur. Si de plus on propose plusieurs syntaxes d'accès à ces tableaux associatifs (ces objets) on devrait pouvoir approcher un modèle de programmation orienté objet classique et habituel. Sachez néanmoins que le modèle objet de Javascript est assez limité (outre le fait qu'on ne supporte pas la notion de classe, les niveaux de visibilité des membres (public, private, ...) ne sont pas supportés non plus. Mais on pouvait s'en douter : si un objet est un tableau, on peut donc accéder à chacune des ses entrées (ou chacun de ses membres).
L'exemple suivant vous montre, de diverses manières, comment créer et enrichir un objet. Notez bien, que je jongle en permanence sur les différentes syntaxes autorisées. Notez aussi qu'en Javascript tout est donnée, y compris une fonction : c'est ce qui permet d'ajouter une fonction comme valeur pour une entrée d'un tableau associatif (voir lignes 08 et 20).
01 //--- Array like syntaxe --- 02 03 var rationalNumber1 = {}; 04 05 rationalNumber1[ "numerator" ] = 3; 06 rationalNumber1[ "denominator" ] = 2; 07 08 rationalNumber1[ "toString" ] = function() { 09 return "[" + this["numerator"] + "/" + this["denominator"] + "]"; 10 } 11 12 var result = rationalNumber1["toString"](); 13 14 //--- The same with object like syntaxe --- 15 var rationalNumber2 = new Object(); 16 17 rationalNumber2.numerator = 3; 18 rationalNumber2.denominator = 2; 19 20 rationalNumber2.toString = function() { 21 return "[" + this.numerator + "/" + this.denominator + "]"; 22 } 23 24 result = rationalNumber2.toString(); 25 26 //--- You can also do this -- 27 result = rationalNumber1.toString(); 28 result = rationalNumber2["toString"]();
Le problème de cette technique (quelque soit la variante considérée) réside dans le fait qu'elle est difficilement réutilisable (hormis faire du copier/coller) pour produire plusieurs instances d'un même type (je ne parle pas, volontairement, de classe). Si l'on veut simuler la notion de type d'objet, il faut utiliser une autre caractéristique du langage Javascript : si une fonction est utilisée conjointement avec l'opérateur new, alors cette fonction fera office de fonction de construction d'objet (un peu comme un constructeur en Java) et durant son exécution, la variable this représentera l'objet en cours d'initialisation. Le new se chargera d'allouer la mémoire alors que la fonction se chargera d'initialiser l'espace de mémoire considéré. Voici un petit exemple.
new
this
01 //--- Constructor function --- 02 function Rational( numerator, denominator ) { 03 04 // Two attributes 05 this.numerator = numerator; // this["numerator"] is supported too 06 this.denominator = denominator; 07 08 // One method 09 this.toString = function() { 10 return "[" + this.numerator + "/" + this.denominator + "]"; 11 } 12 13 } 14 15 //--- Objet manipulations --- 16 var r1 = new Rational( 3, 2 ); 17 var result = r1.toString();
J'insiste bien sur le fait qu'en Javascript, les variables et donc les paramètres, sont non typés. Du coup, en ligne 02, la fonction Rational accepte bien deux paramètres (numerator et denominator, non typés). Petit complément d'information : en réalité Rational n'est pas une fonction mais bien une méthode. En Javascript tout est objet (ou tableau associatif, c'est comme vous voulez) : du coup la méthode Rational est membre de l'objet window (du moins dans le contexte d'un navigateur). La ligne 16 aurait donc pu s'écrire autrement : var r1 = new window.Rational( 3, 2 );.
Rational
numerator
denominator
window
var r1 = new window.Rational( 3, 2 );
Depuis Javascript 1.5 (si je ne me trompe pas), une nouvelle possibilité est apparue : la notion de prototype. Ce concept vient étendre les possibilités du modèle objet de Javascript, en permettant de se rapprocher encore plus de la notion de classe, mais d'une manière un peu particulière. Un prototype s'ajoute en tant que propriété d'une fonction de constructions d'objets (encore une fois tout est objet en Javascript). Ainsi la fonction de construction ne sert plus que de constructeur d'objet alors que son prototype prend en charge la partie définition du comportement (des méthodes). Voici un petit exemple encore une fois basé sur la définition de nombres rationnels. Notez que lorsque l'on définie un prototype, il est classique d'utiliser la syntaxe associative (clé/valeur) à la définition de la table (lignes 11 et 15). Les deux syntaxes précédentes auraient néanmoins pu être utilisées.
01 //--- Constructor function --- 02 function Rational( numerator, denominator ) { 03 04 // Two attributes 05 this.numerator = numerator; // this["numerator"] is autorized too 06 this.denominator = denominator; 07 } 08 09 Rational.prototype = { 10 // One method 11 toString: function() { 12 return "[" + this.numerator + "/" + this.denominator + "]"; 13 }, 14 15 simplify: function() { 16 var maxBound = Math.min( this.numerator, this.denominator ); 17 maxBound = Math.ceil( maxBound / 2 ); 18 19 var i = 2; 20 while( i < maxBound ) { 21 if ( this.numerator%i == 0 && this.denominator%i == 0 ) { 22 this.numerator /= i; 23 this.denominator /= i; 24 continue; 25 } 26 i++; 27 } 28 } 29 30 } 31 32 //--- Objet manipulations --- 33 var r1 = new Rational( 24, 12 ); 34 r1.simplify(); 35 var result = r1.toString();
Il est à noter qu'avec cette technique, si l'on rajoute un membre au prototype considéré, alors ce membre devient accessible à toutes les instances qui lui sont associées. Ainsi l'exemple suivant rajoute une méthode contains à toutes les instances du type Array (ce type est intrinsèque à Javascript et permet de définir des tableaux à tailles variables).
contains
Array
01 var tb = new Array(); 02 03 if ( ! Array.prototype.contains ) { 04 Array.prototype.contains = function( value ) { 05 for( var i=0; i<this.length; i++ ) { 06 if ( value == this[i] ) return true; 07 } 08 return false; 09 } 10 } 11 12 tb.push( "Hello" ); 13 tb.push( "World" ); 14 var exists = tb.contains( "Hello" );
Pour comprendre le modèle d'héritage de Javascript, il nous faut comprendre quelques points supplémentaires. Ce sont ces points que j'ai tenté de synthétiser dans le diagramme ci-dessous. Quelques explications suivront.
Donc reprenons ce diagramme point par point. Commençons pas la fonction de construction d'instance de type Object : comme tout objet Javascript expose des méthodes communes (toString notamment), on se doute que la fonction Object possède un prototype définissant les membres communs. Si vous évaluez l'expression Object.prototype, vous retrouverez cette instance de prototype.
toString
Object.prototype
Passons maintenant à notre fonction de construction de nombre rationnel : celle-ci a aussi un prototype, nous l'avons d'ailleurs explicitement créé. La question importante à se poser est de savoir comment nous avons obtenu cette instance : en fait à la base, cette instance est de type Object. Quelque soit la syntaxe utilisée ({} ou new Object() - voir le premier exemple de ce chapitre), nous avons obtenu un Object. Pour s'en convaincre, je vous propose de tester l'exemple suivant (c'est un complément de code à l'exemple précédent) : la propriété constructor d'un objet permet de retrouver la fonction de construction ayant servie à le produire.
Object
{}
new Object()
constructor
01 alert( Rational.prototype.constructor ); 02 alert( r1 instanceof Object ? "Ok" : "Ko" );
Par ce biais, l'héritage a été mis en oeuvre et un nombre Rational est un Object Javascript. D'ailleurs il vous est possible d'utiliser l'opérateur (voir en ligne 02 de l'exemple ci-dessus) pour valider l'aspect polymorphique d'un objet Javascript (l'exemple affichant "Ok", bien entendu). Nous pouvons donc faire de même en dérivant un type particulier de nombre rationnel.
01 //--- Constructor function --- 02 function MyRational( numerator, denominator ) { 03 this.parent = Rational; 04 this.parent( numerator, denominator ); 05 } 06 07 //--- MyRational prototype création --- 08 MyRational.prototype = new Rational(); 09 10 //--- MyRational prototype extension --- 11 MyRational.prototype.newMember = function() { 12 alert( "OK - " + this ); 13 } 14 15 16 //--- Objet manipulations --- 17 var r2 = new MyRational( 24, 12 ); 18 r2.simplify(); 19 r2.newMember(); 20 21 alert( r2 instanceof Object ? "Ok" : "ko" ); 22 alert( r2 instanceof Rational ? "Ok" : "ko" ); 23 alert( r2 instanceof MyRational ? "Ok" : "ko" );
En ligne 08, on définie le prototype de la fonction MyRational comme étant une instance de Rational, donc l'héritage est mis en oeuvre. En ligne 11, on enrichie le prototype de MyRational. Comme à l'origine nous avons acquis le prototype en créant une nouvelle instance (ligne 08), la méthode newMember ne sera donc pas accessible à partir de Rational.
MyRational
newMember
J'insiste sur le fait que le polymorphisme (et la liaison dynamique - late binding) est utilisé en différents points de ce programme : en ligne 12, c'est bien le toString définie dans Rational qui sera invoqué. Et en ligne 21 à 23, notez encore l'utilisation de l'opérateur instanceof.
instanceof
Notez bien entendu que tous les aspects présentés ci-dessus fonctionnent très bien avec les principaux navigateurs Web utilisés (et notamment Firefox et IE).
Un des objectifs du Framework NWS est de vous fournir une solution complète de développement d'applications Web : incluant des aspects pour la mise en oeuvre de la partie s'exécutant sur le serveur (basée sur le langage Java) mais aussi un API fournissant de nombreux composants Web s'exécutant sur le client. Cette API est, bien entendu, développée par dessus Javascript et se nomme JWT (Javascript Widget toolkit). Les chapitres suivants vous présenteront plus sérieusement cette API.
Qui dit librairie de composants évolués, maintenables et évolutifs, veut dire bien sûr conception et programmation orientée objet : JWT utilise, bien entendu ce paradigme de programmation. Néanmoins, cette API ne va pas se contenter du modèle intrinsèque de programmation orienté objet en Javascript et quelques extensions (ainsi que des conventions) vont être utilisées. L'exemple ci-dessous vous montre comment, via le modèle objet de la librairie JWT, nous pouvons réaliser un héritage, garantissant bien entendu le polymorphisme. Il est clair que pour fonctionner, ce bout de code nécessite le chargement de la librairie corelib/services/web/javascript/Core.js, mais je vous rappelle que le framework NWS réalise, pour une page Web NWS, le changement de ce fichier Javascript implicitement (je vous renvoie, à ce sujet, sur le chapitre Importation des fichiers Javascript NWS).
corelib/services/web/javascript/Core.js
01 var Rational = Object.extendClass( new Object(), { 02 03 initialize: function( numerator, denominator ) { 04 this._numerator = numerator || 0; 05 this._denominator = denominator || 1; 06 }, 07 08 toString: function() { 09 return "[" + this._numerator + "/" + this._denominator + "]"; 10 }, 11 12 simplify: function() { 13 var maxBound = Math.min( this._numerator, this._denominator ); 14 maxBound = Math.ceil( maxBound / 2 ); 15 16 var i = 2; 17 while( i < maxBound ) { 18 if ( this._numerator%i == 0 && this._denominator%i == 0 ) { 19 this._numerator /= i; 20 this._denominator /= i; 21 continue; 22 } 23 i++; 24 } 25 } 26 27 getNumerator: function() { 28 return this._numerator; 29 } 30 31 getDenominator: function() { 32 return this._denominator; 33 } 34 35 }); 36 37 var MyRational = Object.extendClass( new Rational(), { 38 39 initialize: function( numerator, denominator ) { 40 Rational.prototype.initialize.call( this, numerator, denominator ); 41 // Continue initialization 42 }, 43 44 newMember: function() { 45 alert( "OK - " + this ); 46 } 47 }); 48 49 //--- Objet manipulations --- 50 var r1 = new MyRational( 24, 12 ); 51 r1.simplify(); 52 r1.newMember(); 53 54 alert( r1 instanceof Object ? "Ok" : "ko" ); 55 alert( r1 instanceof Rational ? "Ok" : "ko" ); 56 alert( r1 instanceof MyRational ? "Ok" : "ko" );
Il est important de bien comprendre que la méthode extendClass n'est pas intrinsèque à Javascript. C'est le framework JWT qui l'a ajointe au prototype de la fonction Object. Elle simplifie néanmoins largement la mise en oeuvre de l'héritage au sein de vos "pseudo classes". Dans cette nouvelle approche, la méthode initialize sert de constructeur. Elle est invoquée implicitement à chaque nouvelle construction d'instance, pour peu que la "psuedo-classe" soit basée sur cette approche (méthode extendClass).
extendClass
initialize
J'attire aussi votre attention sur le fait qu'en Javascript, il n'y a pas de niveau de visibilité (public, private, ...) : nous en avons déjà un peu parlé. Du coup, JWT a adopté une convention : les membres privés sont préfixés du caractère _. Rien n'empêche que vous accédiez à un tel champ, mais si vous le faites sur un composant JWT, dites vous alors qu'il y a certainement une autre façon de faire.
public
private
Tous les composants JWT dérive de la "pseudo classe" Component. En réalité JWT s'inspire (comme son nom l'indique) de la librairie AWT (et Swing) de Java (vous y retrouverez des conteneurs, des stratégies de placement (layout), ...). En dérivant vos types de composants graphiques de Component vous héritez ainsi d'un ensemble de méthodes qui vous faciliteront la vie. Pour de plus amples informations, sachez que vous allez très bientôt avoir accès à une documentation complète de l'API JWT.
Component
Dominique LIARD - © 2007 SARL Infini Software - Tous droits réservés Les autres marques et les noms de produits cités dans ces documents sont la propriété de leurs détenteurs respectifs.