Cours de JavaScript


précédentsommairesuivant

V. Les objets

V-A. Rappels

Nous avons déjà introduit, de façon succincte, la notion d'objet au chapitre IILes-Objets. Nous avons distingué deux types d'objets, les objets de type primitif qui font référence à une seule valeur (chaînes, nombres ou booléens) et ceux de type composé qui, par l'intermédiaire d'une référence unique, le nom de l'objet, permettent d'accéder à un ensemble d'informations multiples, propriétés nommées de l'objet en question. Nous nous intéresserons ici essentiellement à cette seconde catégorie.

V-B. Les constructeurs

La création d'un objet s'opère à l'aide de l'opérateur new suivi du nom d'un constructeur. Le constructeur le plus général est Object. À partir de celui-ci, on peut créer un nouvel objet en précisant ses propriétés et méthodes dont le (ou les) constructeur(s), de telle manière que ce nouvel objet puisse lui-même être invoqué avec l'opérateur new pour générer de nouveaux objets « clones » possédant les caractéristiques de l'objet d'origine. De proche en proche, on décrit une arborescence dans laquelle, quel que soit le niveau où l'on se place, toute propriété peut être un objet lui-même pouvant contenir des propriétés objets, etc.

Image non disponible

Dans la figure ci-dessus, on a défini un objet Figure comportant trois propriétés de noms HautGauche, BasDroit et Couleur. Celles-ci sont elles-mêmes des objets. Pour les deux premières, il s'agit d'objets de type point, par exemple, objet préalablement défini et comportant deux propriétés de nom x et y définissant les coordonnées d'un point dans un repère donné. Pour la troisième, il s'agit aussi d'un objet remplissage possédant trois propriétés, Rouge, Vert et Bleu, permettant d'indiquer les intensités des couleurs primaires dans un codage RVB.

La création d'un tel objet peut se faire par les instructions suivantes :

 
Sélectionnez
var Figure = new Object();
Figure.HautGauche = new point(140,16);
Figure.BasDroit = new point(185,61);
Figure.Couleur = new remplissage(33,66,0);

Si, par exemple, au lieu de définir totalement cette variable on s'était contenté d'écrire Figure.BasDroit = new point(), les valeurs x et y de BasDroit auraient été indéfinies (undefined). Cette variable ayant été créée, on peut accéder à ses caractéristiques en lecture et écriture. Si l'on veut affecter les valeurs x et y de BasDroit, le membre gauche de l'affectation devra parcourir toute l'arborescence : Figure.BasDroit.x = 185; Figure.BasDroit.y = 61;(nous avons vu en étudiant l'instruction withPréfixage-d'objets comment raccourcir cette écriture).

Nous avons, par ailleurs, indiqué plus hautLes-littéraux que JavaScript autorisait des littéraux pour définir des objets. La définition de Figure à l'aide de littéraux objets aurait été :

 
Sélectionnez
var Figure = { HautGauche : new point(140,16),
               BasDroit : new point(185,61),
               Couleur : new remplissage(33,66,0),
             };
ou encore....
var Figure = { HautGauche : { x : 140,
                              y : 16
                            },
              BasDroit :    { x : 185,
                             y : 61
              Couleur :     { Rouge : 33,
                             Vert : 66,
                             Bleu : 0
                            }
             };

Nous avons vu qu'un objet est défini par un constructeur, méthode particulière qui reçoit en paramètre tout ou partie des valeurs permettant d'affecter les diverses propriétés. Pour comprendre comment fonctionne un constructeur, cliquez ici…

V-C. Les propriétés et les méthodes

Il est difficile de parler des constructeurs sans faire référence aux propriétés et méthodes, comme nous l'avons vu dans le paragraphe précédent. Aussi, dans ce paragraphe, nous nous contenterons de préciser ou de rappeler quelques points d'intérêt.

En JavaScript, un constructeur est réalisé grâce à une fonction, tout comme une méthode. La seule différence entre les deux, c'est que le constructeur est invoqué par l'opérateur new et qu'il a pour effet de créer un objet vide et éventuellement de l'initialiser en tout ou partie par l'intermédiaire du mot this qui constitue une référence vers cet objet, alors qu'une méthode est appelée en tant que propriété de l'objet dans lequel elle a été définie.

À ce niveau, il convient de préciser que l'utilisation du mot this n'est pas réservée aux seuls constructeurs. Employé dans une méthode, il fait référence à l'objet auquel appartient ladite méthode. Par exemple, en accédant à la méthode MonCarre.Surface(), on peut, dans le corps de la méthode Surface() utiliser this pour faire référence à l'objet MonCarre. C'est ainsi que l'on pourra définir cette méthode par

 
Sélectionnez
Surface()=function() {return this.cote * this.cote;}

Quelle différence entre méthode et fonction ?

Au moment où l'on définit une fonction, elle reçoit un nom en tant que variable qui n'est autre qu'une propriété d'un objet « englobant » (dans lequel est définie cette propriété). Ainsi, l'appel d'une fonction revient à invoquer une méthode d'un objet global et en conséquence la différence entre fonction et méthode apparaît très mince. Elle se résume en fait à ce que les méthodes sont véritablement dédiées pour opérer sur l'objet les contenant en tant que propriété, alors que le rôle des fonctions est totalement dissocié de l'objet dans lequel elles sont définies.

Dans une fenêtre HTML, l'objet omniprésent auquel tout ce que vous créez se réfère est l'objet window : il s'agit de la fenêtre (ou du cadre - « frame ») contenant. Cela est quasiment toujours vrai, si bien que c'est implicite : ce qui signifie que l'on n'est pas obligé de faire apparaître le terme window dans la hiérarchie des accès. Cela signifie aussi que la plupart du temps le terme this se réfère à window. Il y a deux exceptions à cette règle : les constructeurs et les gestionnaires (« handlers ») d'événements. Dans les fonctions permettant de mettre en œuvre respectivement, la construction d'un objet ou la prise en compte d'un événement, le terme this réfère respectivement à l'objet créé ou à l'événement traité et window n'est plus implicite. Si bien que dans ces cas, pour faire référence à un objet autre que celui traité, la hiérarchie d'accès devra être complète.

Il convient en outre d'ajouter que les objets étant atteints via une référence, il peut advenir que les valeurs référencées ne soient plus accessibles. Par exemple, dans le programme suivant :

 
Sélectionnez
var Acces = new Rectangle(100,45);
...
Acces = 12;
...

La variable Acces, dans un premier temps a été une référence vers un objet de type Rectangle qui a été créé. Puis plus loin dans le programme, le même identificateur a été affecté à une variable entière. L'objet précédemment créé n'est donc plus accessible. Son adresse d'implantation en mémoire est définitivement perdue. Cela peut se reproduire plusieurs fois dans un programme, ce qui peut engendrer une perte d'espace mémoire importante. Dans les langages Pascal, C ou C++, cette notion de référence existe et est totalement prise en charge par le programmeur au travers de pointeurs. Le bon programmeur, avant de rendre inaccessible un enregistrement, aura pris soin, explicitement, de libérer le pointeur d'accès, ce qui a pour effet de récupérer la mémoire occupée par l'enregistrement pointé. En Java et JavaScript (toujours pour des raisons de robustesse et de sécurité) il n'y a pas de pointeur !!!… ou du moins, ceux-ci ne sont pas accessibles au programmeur. Ils sont totalement gérés par le langage. De ce fait, la récupération d'espace mémoire est elle aussi automatiquement gérée par le Garbage Collector.

De plus, et cela permet de surpasser les autres langages cités, cette fonctionnalité ne se limite pas aux objets de type composé, mais s'étend à tous les objets !! Soit, pour exemple, le programme suivant :

 
Sélectionnez
var S1 = "javascript";     // affectation de S1
var S2 = S1.toUpperCase(); // S2 prend la valeur "JAVASCRIPT"
S1 = S2;                   // S1 prend la valeur de S2

… à la suite de cette portion de programme, la chaîne "javascript" ne peut plus être atteinte. Le Garbage Collector déterminera cela et récupérera donc la place occupée.

Une propriété d'un objet peut aussi être rendue inaccessible par suppression, tout simplement ! Depuis la version 1.2 de JavaScript, l'opérateur delete permet cela. Bien entendu, le Garbage Collector intervient ensuite pour la récupération d'espace. On va mettre cela en évidence. Un objet de nom « Obj » a été prédéfini et contient cinq propriétés de nom « prop1 » à « prop5 ». Vous pouvez vous en assurer en utilisant le bouton « VOIR ». À chaque action sur le bouton « DELETE », vous allez pouvoir supprimer une des propriétés. Vous pourrez vérifier à chaque fois l'état de l'objet par le bouton « VOIR ».

V-D. Prototype et héritage

Dans les pages d'exercices rencontrées plus haut, nous avons élaboré un constructeur d'objets de type Rectangle qui, outre les propriétés de Longueur et Largeur qui peuvent changer d'un tel objet à l'autre, contenait aussi les méthodes de calcul du Périmètre et de la Surface. Or, quelles que soient les dimensions d'un rectangle, la façon de calculer son périmètre ou sa surface sera toujours la même. Et pourtant, chaque Rectangle construit par ce constructeur disposera de ces méthodes, ce qui va avoir pour conséquence d'occuper inutilement de l'espace. Si au lieu de cela, ces méthodes, et plus généralement toutes les propriétés constantes communes à une classe d'objets étaient partagées par eux, l'économie d'espace en découlant constituerait un gain appréciable.

Pour cela, JavaScript propose la notion d'objet prototype. Tout objet dispose d'un objet prototype possédant toutes les propriétés constantes communes à la classe, propriétés dont il hérite au moment de sa création.

L'héritage, bien sûr, ne se traduit pas par une recopie dans l'objet (sinon, on n'aurait rien gagné). Vu de l'extérieur, tous les objets prototypés sembleront posséder l'ensemble de l'héritage, mais en fait ils ne pourront y accéder seulement en lecture en cas de besoin !…
Outre l'intérêt déjà mentionné concernant le gain d'espace, cette remarque a une autre conséquence intéressante : en cas de modification a posteriori du prototype, l'ensemble d'héritage des objets précédemment créés avec ce prototype sera modifié en conséquence.

Du fait de l'utilisation du prototype, l'accès aux propriétés selon qu'il s'opère en lecture ou en écriture deviendra un peu plus délicat :

  • en écriture, tout d'abord, la modification, par l'utilisateur, d'une propriété ne pourra effectivement se faire que si celle-ci n'appartient pas au prototype. En effet, si une telle propriété pouvait être modifiée, elle le serait alors pour tous les objets ayant le même prototype ;
  • en lecture, JavaScript vérifie si l'objet possède en propre la propriété invoquée. Dans la négative, la vérification se prolonge vers le prototype de l'objet. Si la recherche réussit (dans l'un ou l'autre cas), la valeur atteinte est fournie, dans l'autre cas, la valeur rendue est undefined.

Question : que se passe-t-il si l'utilisateur crée une propriété dont le nom apparaît dans le prototype ?

Cette propriété va tout simplement devenir propre à l'objet sur lequel la création aura eu lieu. Cet objet n'en héritera donc plus, mais il continuera à hériter du reste (éventuel) du prototype et les autres objets de la classe qui n'auront pas redéfini cette propriété continueront à en hériter. On voit donc l'importance de l'accès prioritairement sur les propriétés propres de l'objet avant de considérer le prototype. Les propriétés d'un objet masquent les propriétés de même nom de son prototype.

Pour créer une propriété dans le prototype d'un constructeur, la syntaxe sera la suivante :

 
Sélectionnez
<Nom constructeur>.prototype.<Nom propriété> = <expression> ;

Voici un exemple qui va nous permettre de mieux comprendre ce fonctionnement :

 
Sélectionnez
<script language="JavaScript">
  function Objet(x,y){                    // définition constructeur
    this.x=x;                             // propriété propre
    this.y=y;                             // propriété propre
  }
  function Moy(){                         // définition méthode
    return (this.x + this.y)/this.Cte;
  }
  Objet.prototype.Cte = 2;                // propriété prototypée
  Objet.prototype.calcul = Moy;           // méthode prototypée
  var O1 = new Objet(3,5);                // création
  var O2 = new Objet(7,9);                // création
  O2.Cte+=3; O1.star = '*';               // ajout propriétés propres
  alert(O1.x+O1.y+O1.Cte+O1.calcul());
  alert(O2.x+O2.y+O2.Cte+O2.calcul());
  Objet.prototype.New='new';              // ajout propriété prototypée
  delete O1.Cte;                          // suppression dans prototype
  delete O2.Cte;                          // suppression en propre
  S="Proprietes de O1 :\n";
  for(var i in O1)S+=i+', '; alert(S);    // affich O1
  S="Proprietes de O2 :\n";
  for(var i in O2)S+=i+', '; alert(S);    // affich O2
</script>

Ce script définit un constructeur Objet comportant deux propriétés x et y. Par ailleurs on prototype deux autres propriétés, une constante de nom Cte et une méthode opérant un calcul simple, calcul. On crée ensuite deux objets, 01 et 02. La propriété Cte de 02 est masquée par une nouvelle propriété évaluée à partir du prototype. La propriété 02.Cte vaut donc à présent 2 + 3, soit 5. Enfin on ajoute une nouvelle propriété au prototype.

Les résultats nous montrent bien que :

  • les propriétés du prototype sont bien accessibles à partir des objets construits ;
  • 01 a conservé la Cte égale à 2, tandis pour 02 on a atteint la valeur 5 puisque Cte a été redéfinie ;
  • la propriété calcul prend en compte la variable Cte prototypée pour 01 et la variable Cte propre pour 02 ;
  • l'opérateur delete est sans effet sur la propriété Cte de 01, car elle est prototypée ;
  • par contre dans O2, la propriété propre Cte qui valait 5 est bien supprimée et la propriété prototype de valeur 2 est alors démasquée ;
  • enfin on constate bien, l'ajout a posteriori d'une propriété de nom New dans le prototype.
exécution

On retrouve ce mécanisme de prototypage dans les classes d'objets prédéfinies de JavaScript, comme la classe String. Ainsi, dans ces classes, on aura le loisir de modifier le prototype en ajoutant, par exemple, des méthodes qui seront donc accessibles par tous les objets String.

V-E. Les tableaux associatifs

De la même façon que l'on utilise l'opérateur « . » pour accéder aux propriétés d'un objet, on peut réaliser la même opération en utilisant le mécanisme des tableaux associatifs.
En fait au lieu d'utiliser la notation <ident objet>.<propriété>, on peut la remplacer par <ident objet>["<propriété>"]. Alors que dans la première forme, la propriété apparaît en tant qu'identificateur, c'est la chaîne de caractères correspondant à celui-ci qui est utilisée dans la seconde forme. Cela est très intéressant, car on peut utiliser toutes les méthodes accessibles à partir d'un objet de type String pour créer ou construire le nom de la propriété. Par exemple, dans l'illustration de l'opérateur delete ci-dessus, c'est le numéro N de la propriété qui a été fourni par l'utilisateur sous la forme de chaîne. Il a suffit de concaténer la chaîne « prop » à ce numéro pour opérer enfin delete["prop"+N] qui réalisait la suppression souhaitée.
Par ailleurs, alors que dans C++ ou Java, les propriétés d'une classe d'objets sont parfaitement définies avant la compilation, nous avons vu que dans JavaScript, l'ensemble des propriétés était totalement dynamique et "qu'il suffit de l'écrire pour qu'elle existe". Dans ces conditions, il peut être judicieux, dans une phase itérative, de créer des propriétés sous la forme d'une chaîne composée d'un préfixe suivi d'un numéro (comme précédemment) et de les manipuler avec ce formalisme de tableaux associatifs.

V-F. L'objet Object

L'objet Object, en JavaScript comme en Java est la classe la plus générale à partir de laquelle tout objet est dérivé. Les autres classes prédéfinies du langage ou les classes définies par l'utilisateur comportent donc les propriétés et méthodes qui leur sont spécifiques ainsi que celles dont dispose la classe Object. Il apparaît donc nécessaire de préciser ici ces dernières.

La propriété constructor fait référence à la fonction utilisée pour construire l'objet. Par exemple, "JavaScript".constructor est une expression dont voici l'évaluation…

Il apparaît bien qu'il s'agit d'un constructeur prédéfini du langage. Voyons ce qu'il se passe pour un constructeur utilisateur en évaluant l'expression 01.constructor associée à l'objet O1 utilisé plus haut…

On retrouve bien le constructeur que l'on a présenté dans l'exemple précédent.

N.B. Les utilisateurs de Safari 1.0 sous Mac OS X s'apercevront que le fonctionnement présente quelques problèmes. Dans les deux cas, on obtient "Internal function".

Définissons un constructeur Individu, puis une instance Untel de la façon suivante :

 
Sélectionnez
function Individu(N,P){
   this.Nom=N;
   this.Prenom=P;
   this.Naiss={An:0,Mois:0,Jour:0};
 }
 var Untel = new Individu("Terieur","Alex");
 with (Untel.Naiss){
   An = 1972;
   Mois = 2;
   Jour = 29;
 }
 Untel.Job = "Enseignant";

Les méthodes que nous allons présenter à présent ont des comportements qui diffèrent entre les navigateurs et entre les diverses versions d'un même navigateur. Essayons d'écrire l'objet Untel par la méthode alert puisque l'on est en asynchrone, mais cela pourrait aussi s'appliquer à la méthode write dans le cas synchrone…

alert(Untel)

On constate que cette écriture bien que guère explicite sur le contenu de l'objet (en particulier, Internet Explorer reste muet sur cet appel) nous renseigne, pour les versions de Netscape inférieures à 4.5 sur son type (objet) et le nom de sa classe (Object). Par contre pour les versions 4.x plus récentes, la structure apparaît.

Nous allons utiliser une nouvelle méthode, toString() qui étant définie dans la classe Object est donc accessible par tout objet. Voyons donc ce que cela donne en exécutant

alert(Untel.toString())

Le résultat reste le même pour Netscape, car en fait, alert a opéré une utilisation implicite de la méthode. À noter que pour les utilisateurs d'Internet Explorer, s'agissant d'un objet, la sortie sera effectivement toujours du type [object <nom de classe>]. Par contre, pour les utilisateurs de versions de Netscape supportant la version 1.2 de JavaScript, l'utilisation de toString() dans un script, produira déjà plus d'informations sur le contenu de l'objet puisque c'est en fait sa forme littérale qui sera donnée.

N.B. Aujourd'hui, la famille Mozilla a abandonné cette fonctionnalité apportée à toString().

Mieux, on peut redéfinir et prototyper la méthode toString pour qu'elle présente toutes les instances sous la forme que l'on veut et sous quelque navigateur que ce soit. Ajoutons par exemple la fonction suivante :

 
Sélectionnez
Individu.prototype.toString = function(){
  with (this)
  var S = 
    "Il s'agit de M. "+ Nom + " "+ prenom + " ne le "+
    Naiss.Jour + "/" + Naiss.Mois + "/" + Naiss.An +
    " exerçant la profession "+ Job;
  return S;
}

… et voyons à nouveau ce que donne

alert(Untel.toString())

Désormais, grâce à cette redéfinition, sous Explorer comme sous Netscape, toute écriture d'un objet de type Individu aura cette allure.

La particularité signalée précédemment en ce qui concerne Netscape ne suit pas la norme ECMA. En conséquence, dans JavaScript 1.3, Netscape revient dans la norme en donnant à toString() la fonction de délivrer, comme pour Internet Explorer, le nom de classe. Par contre, la fonctionnalité délivrant l'objet sous sa forme littérale est à présent disponible.
Il s'agit de la méthode toSource() seulement disponible sur Netscape, mais qui le sera sur les prochaines versions d'Internet Explorer. Pour les utilisateurs de Netscape, voyons ce que donne

alert(Untel.toSource()).

À noter que là encore, Safari fait exception à la règle en ne se comportant pas comme ses alter ego (Netscape, Mozilla, Camino ou Navigator, etc.)

La philosophie de la prochaine méthode que nous allons voir se rapproche de celle de toString(). Alors que cette dernière a pour fonction de transformer un objet vers une chaîne, valueOf() tend à traduire un objet vers une valeur numérique. Lorsque cela n'est pas possible cette méthode délivre les mêmes résultats que toString(). Comme on l'a vu pour toString(), valueOf() peut être redéfini pour donner une valeur que l'utilisateur jugera représentative d'un objet. Par exemple, pour la classe Individu définie plus haut, on peut imaginer que la date de naissance sous une forme jjmmaa soit représentative de chaque objet de cette classe. La redéfinition de valueOf() sera donc de la forme :

 
Sélectionnez
Individu.prototype.valueOf = function(){
  with (this.Naiss)
   return 100*(100*Jour + Mois) + An%100;
}

Voici quelques exemples qui vont illustrer le comportement de cette méthode, en particulier sur deux types d'objets définis dans cette page : O1 dont on a rencontré précédemment la définition de la classe « objet » à laquelle il appartient et dans laquelle valueOf() reste « générique » et Untel de la classe Individu dont on vient de voir la redéfinition de la méthode en question :

Comportement de valueOf()...

Résultat
B = 3!=2; B.valueOf()
Untel.valueOf()
O1.valueOf()
"n'importe quoi".valueOf()

V-G. Exemples d'objets… et d'événements

Pour montrer un exemple de hiérarchie d'objets et accessoirement des gestionnaires d'événements qui leur sont attachés, voici une

page

qui « met en scène » une configuration d'objets couramment utilisés.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Jacques Guizol. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.