Nous avons déjà introduit, de façon
succincte, la notion d'objet au chapitre
II. 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 multiple, propriétés nommées de l'objet
en question. Nous nous intéresserons ici essentiellement à cette
seconde catégorie.
2
- 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.
A 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) constructeurs,
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éristique 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.
Exemple :
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 primaire dans un
codage RVB.
La création d'un tel objet peut se faire par les
instructions suivantes :
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 valeur 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 with
comment raccourcir cette écriture)
Nous avons, par ailleurs, indiqué plus
haut que JavaScript autorisait des littéraux pour définir
des objets. La définition de Figure
à l'aide de littéraux objets aurait
été :
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.......
3
- 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.
A 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 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 :
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 :
var S1 = "javascript"; // affectation de S1var 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 5 propriétés de nom 'prop1'
à 'prop5'. Vous pouvez vous en assurer en utilisant le bouton 'VOIR'.
A 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'.
4
- 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.
ATTENTION : 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 un autre conséquence intéressante
: En cas de modification à 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 :
Voici un exemple qui va nous permettre de mieux comprendre
ce fonctionnement :
<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, O1 et O2.
La propriété Cte de 02
est masquée par une nouvelle propriété évaluée
à partir du prototype. La propriété O2.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 ;
O1 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 O1 et la variable Cte
propre pour O2 ;
L'opérateur delete est sans effet sur la propriété
Cte de O1 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.
On retrouve ce mécanisme de prototypage dans les
classes d'objets prédéfinies de JavaScript, comme, par exemple,
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.
5
- 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 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
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.
6
- 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 disposent 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 O1.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.
NB : 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 :
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. A 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.
NB : 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 :
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()).
A noter que là encore, Safari
fait exception à la règle en ne se comportant pas comme
ses alter ego (Netscape, Mozilla, Camino ou Navigator, &)
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 :
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()
7
- Exemple 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.