Cours de manipulation du DOM et DHTML
Les objets dans JavaScript
G. Chagnon
Un exposé en détails des fonctionnalités de la programmation orientée prototype en JavaScript
javascript, js, objet, prototype
Il arrive que des variables soient redéfinies entre plusieurs scripts épars dans des fichiers distincts. Une redéfinition malencontreuse d'une variable peut perturber le fonctionnement d'un script, et il faut donc au maximum limiter le risque d'interférences entre deux variables. La toute première précaution à prendre est donc de limiter au maximum la portée d'une variable, en recourant à la déclaration var
.
On pourrait penser à utiliser des noms de variables très explicites, voire à les préfixer ou les suffixer, comme par exemple monCodeAMoiRienQuAMoi_maVariable
, mais il faut pour cela trouver un préfixe unique (celui de l'exemple précédent a certes peu de chances d'être repris !), et surtout, par la suite, se garder des fautes de frappe. Qui plus est, cela a comme inconvénient que si par extraordinaire, le préfixe est utilisé dans un autre fichier, l'ensemble des variables et fonctions doit être renommé. Ce n'est donc pas une solution très efficace.
JavaScript
ne permet pas encore comme d'autres langages de définir des classes, et d'instancier ensuite des objets. Mais il est possible de créer directement des objets, grâce au mot-clef Object
.
="Bonjour!"
=
La déclaration précédente définit l'objet objet1
, lui attache une propriété (chaine
) et une méthode que l'on appelle ensuite. Ainsi, la méthode est associée à cet objet uniquement, et il n'y a pas de risque que sa définition écrase une autre définition qui serait donnée pour un autre objet.
Il est possible d'aller un peu plus loin dans la création d'un objet en utilisant un « littéral objet », mais cette méthode ne fonctionne pas pour les navigateurs très anciens (Netscape jusqu'à la version 3 et Internet Explorer jusqu'à la version 4). La déclaration précédente s'écrit alors...
=
{
:"Bonjour!",
:
}
Cela permet de ne pas avoir à répéter le nom de l'objet défini, réduit donc les risques d'erreur et facilite la maintenance et la reprise ultérieure du code. Attention, dans l'exemple précédent, les lignes dans le littéral sont séparées par des virgules et non des points-virgules, et les définitions utilisent des :
et non des signes =
.
On peut aussi supprimer une propriété existante à l'aide de l'opérateur delete
: par exemple, ici, delete objet1.chaine
supprime la propriété d'objet1
. Cela ne supprime pas la propriété du prototype, mais uniquement de l'instanciation spécifiée. En mode strict, tenter cette opération sur une propriété non modifiable (par exemple, définie par défaut dans JavaScript
comme String.length
) lève une exception et déclenche une TypeError
.
La manipulation d'un littéral objet peut vite s'avérer fastidieuse et source d'erreurs. Mais Javascript
offre une manière assez élégante pour aller assez loin dans la définition d'objets. Cette méthode repose sur les propriétés des fonctions. En Javascript
en effet, les fonctions sont des objets de type function
. À ce titre, elles possèdent comme tout objet des propriétés et des méthodes (qui sont donc des fonctions appliquées à des fonctions...). Cela ouvre des perspectives intéressantes.
Commençons par créer une fonction de base, qui va nous servir de « prototype » (nous verrons par la suite que ce terme n'est pas utilisé sans raison) :
Personne (prenom, genre, dateNaissance, caractere)
.prenom=prenom
.genre=genre
.dateNaissance=dateNaissance
.caractere=caractere
anatole = Personne ("Anatole", "homme", 1990, "frivole")
anatole.genre;renvoie "homme"
bernadette = Personne ("Bernadette", "femme", 1991, "très chouette")
bernadette.genre;renvoie "femme"
Cette solution, pour être simple, n'en présente pas moins deux inconvénients :
- les propriétés ne sont pas privées, et on peut y accéder (et les modifier) à volonté ;
- les arguments doivent être passés dans un certain ordre.
Il est possible d'aller encore un peu plus loin. En effet, il n'est parfois pas souhaitable que certaines propriétés d'un objet soient accessibles de l'« extérieur » du script. En programmation orientée objet, on fait appel, dans une classe, à des membres dits privés et d'autres publics. Il est possible de faire de même en javascript, en utilisant le fait que la portée des variables déclarées dans une fonction y est limitée. Ainsi,
Personne (prenom, genre, dateNaissance, caractere)
personnePrenom=prenom
personneGenre=genre
personneDateNaissance=dateNaissance
personneCaractere=caractere
.getPrenom = ()
personnePrenom
.setPrenom = (prenom)
personnePrenom=prenom
anatole = Personne ("Anatole", "homme", 1990, "frivole")
anatole.personnePrenom;renvoie undefined car personnePrenom est une propriété privée
anatole.getPrenom();renvoie Anatole
bernadette = Personne ("Bernadette", "femme", 1991, "très chouette")
bernadette.setPrenom("Noémie")
bernadette.getPrenom();renvoie Noémie
Afin de ne pas être contraint dans l'appel de la fonction dans l'ordre des arguments, on peut lui fournir un objet, et en profiter pour proposer des valeurs par défaut si certaines propriétés sont manquantes (la méthode ES6 ne permet pas de modifier l'ordre de déclaration des paramètres) :
Personne (fiche)
personnePrenom = fiche.prenom !== 'undefined' ? fiche.prenom : "Nino"
personneGenre = fiche.genre !== 'undefined' ? fiche.genre : "homme"
personneDateNaissance = fiche.dateNaissance !== 'undefined' ? fiche.dateNaissance : "1934"
personneCaractere = fiche.caractere !== 'undefined' ? fiche.caractere : "musicien"
.getPrenom = ()
personnePrenom
.getDateNaissance = ()
personneDateNaissance
.setPrenom = (prenom)
personnePrenom=prenom
anatole = Personne ({prenom:"Anatole", genre:"homme", dateNaissance:1980, caractere:"frivole"})
anatole.getPrenom();renvoie Anatole
bernadette = Personne ( {prenom:"Bernadette", genre:"femme", caractere:"très chouette"})
bernadette.getDateNaissance();renvoie 1934
Il suffit de déclarer la méthode privée en tant que fonction, à l'intérieur de la fonction de base. Par exemple :
Personne (fiche)
personnePrenom = fiche.prenom !== 'undefined' ? fiche.prenom : "Nino"
personneGenre = fiche.genre !== 'undefined' ? fiche.genre : "homme"
personneDateNaissance = fiche.dateNaissance !== 'undefined' ? fiche.dateNaissance : "1934"
personneCaractere = fiche.caractere !== 'undefined' ? fiche.caractere : "musicien"
vieillit()
personneDateNaissance--
.getPrenom = ()
personnePrenom
.getDateNaissance = ()
personneDateNaissance
.setPrenom = (prenom)
personnePrenom=prenom
bernadette = Personne ( {prenom:"Bernadette", genre:"femme", caractere:"très chouette"})
bernadette.vieillit()
bernadette.getDateNaissance();renvoie 1934
... renvoie un message d'erreur car vieillit
, appliquée à bernadette
, est undefined. Cependant, cela n'empêche pas d'utiliser vieillit
à l'intérieur de Personne
.
Nous avons déjà évoqué la propriété prototype
. Elle s'applique tout aussi facilement à nos objets nouvellement définis, et cela permet de construire un mécanisme en tout point similaire à l'héritage en programmation orientée objet « classique ». Supposons notre prototype Personne
défini comme précédemment. On peut définir un nouveau prototype, qui pourrait être etudiant
, sous la forme :
Etudiant(fiche, note)
Personne.call(fiche, note)
etudiantNote = note !== 'undefined' ? note : 0
.getNote = ()
etudiantNote
Etudiant. = .(Personne.prototype)
Etudiant.. = Etudiant
On peut alors écrire par exemple...
marieBerthe = Etudiant ( {prenom:"Marie-Berthe", genre:"femme", caractere:"experte"},17)
marieBerthe.getNote();renvoie 17
Le mot-clef this
, que nous avons déjà eu l'occasion de rencontrer, a un comportement en apparence déroutant. Il fait en effet référence au contexte dans lequel une fonction est appelée. Considérons l'exemple suivant...
fratrie =
prenom1: 'Abel'
prenom2: 'Yves'
prenom3: 'Hakim'
nom: 'Flaille'
toutLeMonde: ()
.log(.prenom1 + ', ' + .prenom2 + ', ' + .prenom3 + ', ' + .nom)
fratrie.toutLeMonde()
tlm=fratrie.toutLeMonde()
tlm()
Le premier exemple renvoie "Abel, Yves, Hakim Flaille", le second undefined
. Dans le premier cas en effet, la fonction toutLeMonde
est appelée en tant que méthode de l'objet fratrie
son contexte est donc celui de l'objet appelant, fratrie
et par exemple this.prenom1
renvoie à fratrie.prenom1
. Dans le second cas, la fonction tlm
est appelée au niveau global, donc le contexte est celui de l'objet global, à savoir window
. this.prenom1
renvoie donc à window.prenom1
, qui n'est pas défini...
Un moyen existe pour spécifier l'objet auquel this
doit référer : la méthode bind
de la propriété Function.prototype
, qui est héritée par toute fonction. Il suffit d'écrire ainsi var tlm=fratrie.toutLeMonde.bind(fratrie);
pour que tlm()
renvoie Abel, Yves, Hakim Flaille". On peut aussi expliciter le contexte lors de la définition de la méthode :
fratrie =
prenom1: 'Abel'
prenom2: 'Yves'
prenom3: 'Hakim'
nom: 'Flaille'
toutLeMonde: ()
.prenom1 + ', ' + .prenom2 + ', ' + .prenom3 + ', ' + .nom
Cette méthode permet de gérer le contexte dans lequel un gestionnaire d'événement est appelé, ou, plus généralement, un callback, c'est-à-dire une fonction passée en paramètre d'une autre fonction. Par exemple...
fratrie =
prenom1: 'Abel'
prenom2: 'Yves'
prenom3: 'Hakim'
nom: 'Flaille'
toutLeMonde: ()
.getElementById("ident").("click",()
.log(.nom)
.bind(), false)
EcmaScript 2015 a introduit une syntaxe de classes dans JavaScript. Cependant, cette syntaxe n'est qu'un vernis sur ce qui reste le fondement de JavaScript, à savoir les prototypes. Partant, un certain nombre de concepts ne sont pas utilisables, et d'autres particularités dérivent du modèle sous-jacent de JavaScript. Par la suite, nous parlerons de classe mais il faut bien garder à l'esprit qu'il ne s'agit pas de classes au même titre qu'en Java, par exemple, mais juste une commodité de notation et de déclaration.
Notamment, toute la définition d'une classe est en mode strict.
Une classe est définie à l'aide du mot-clef class
. Contrairement à la définition de fonctions, une classe doit être définie avant d'être instanciée (contrairement aux définitions de fonction en JavaScript, dont les déclarations peuvent « remonter » —c'est ce qu'on appelle le hoisting. On écrira ainsi…
maClasse
(…)
=…
Code de la méthode
monObjet = maClasse(…)
constructor
est un nom réservé. Par exemple…
Voiture
(modele, nombreRoues)
=modele
=nombreRoues
("Vroum!")
maPetiteVoiture = Voiture("Avtoros Shaman", 8)
Attention, il n'y a pas de virgule ou de point virgule entre les différents éléments constitutifs de la déclaration. Par exemple, il n'y a pas de virgule entre le constructeur et la définition de la méthode, car il ne s'agit pas d'un littéral objet.
Il n'est actuellement pas possible de définir de propriété, ni publique, ni privée. La mise au point du mécanisme, encore expérimental, dépend encore du navigateur utilisé et on ne peut pas encore l'utiliser en production.
Une méthode publique se définit simplement comme on l'a vu plus haut. Il est cependant possible de définir des méthodes dites « statiques ». Ces méthodes sont propres à la classe et peuvent être utilisées au sein de celle-ci, mais ne peuvent pas être appliquées à une instance en particulier. Par exemple…
class Tab {
constructor(tableau){
this.tableau=tableau;
}
getLongueur () {
return this.tableau.length;
}
item(i) {
return this.tableau[i];
}
static tabSoustrait(a, b){
var res=new Array;
for (let i=0;i<a.getLongueur();res.push(a.item(i)-b.item(i++)));
return res;
}
}
var x = new Tab([5,6,7]);
var y = new Tab([3,3,12]);
console.log(Tab.tabSoustrait(x,y));
En passant, vous pouvez d'ailleurs remarquer dans l'exemple précédent qu'on ne peut pas écrire directement a.length
ou a[i]
dans la méthode statique, puisque a
et b
ne sont pas de « vrais » tableaux. On doit recourir à des définitions de méthodes.
On peut créer une sous-classe d'une classe existante avec l'instruction extends
. Cette instruction permet d'étendre notamment la définition des méthodes de la classe parente, voire de créer de nouvelles méthodes. Par exemple…
class SportCollectif {
constructor(nom, nbjoueurs){
this.nom=nom;
this.nbjoueurs=nbjoueurs;
}
get_joueurs(){
return this.nbjoueurs;
}
}
class Sport15 extends SportCollectif {
constructor(nom) {
super(nom, 15);
}
get_joueurs(){
var nomJoueurs="";
if (this.nom=="rugby") nomJoueurs+=" rugbymen";
return this.nbjoueurs+nomJoueurs;
}
}
var sport1 = new SportCollectif("football", 11);
var sport2 = new Sport15("sport");
var sport3 = new Sport15("rugby");
console.log(sport1.get_joueurs()); // 11
console.log(sport2.get_joueurs()); // 15
console.log(sport3.get_joueurs()); // 15 rugbymen