version 1.2.0, dernière mise à jour le 14 octobre 2024.
Le concept de Programmation Orientée Objet ne se limite pas à la définition de classes et d'instances comme dans des langages de programmation comme Java. JavaScript est ainsi un langage de programmation orientée objet, mais il s'agit non pas d'un langage à classes, mais d'un langage à prototypes. Dans ce type de programmation, on ne crée pas tout d'abord une classe abstraite, qu'on instancie ensuite dans des objets « concrets »É; on commence par définir un prototype, qui est directement instancié et peut être manipulé, avant de le « cloner ». Dans le monde réel, pour définir un objet table, on commencerait ainsi par aller dans un atelier de menuiserie et en construire une ; puis on définirait ensuite toutes les autres tables comme étant des variations de ce premier prototype.
Cependant, comme nous le verrons par la suite, un sucre syntaxique imitant les classes a été introduit dans le langage.
Comme JavaScript
est orienté prototype, il est possible de créer directement des objets, grâce au mot-clef Object
:
var objet1 = new Object ;
objet1.chaine="Bonjour!" ;
objet1.affiche=function(){
alert(this.chaine) ;
}
objet1.affiche() ;
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 », même si 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…
var
objet1 =
{
chaine:"Bonjour!",
affiche:function()
{
alert(this.chaine) ;
}
}
objet1.affiche() ;
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.
Nous allons utiliser les portées des variables pour créer des propriétés et méthodes privées :
function Personne (qqn)
{
let prenom = qqn.prenom? qqn.prenom : "Nino" ;
this.genre = qqn.genre? qqn.genre : "homme" ;
this.dateNaissance = qqn.dateNaissance? qqn.dateNaissance : 1934 ;
this.caractere = qqn.caractere? qqn.caractere : "musicien" ;
this.getPrenom = function()
{
return prenom ;
} ;
this.setPrenom = function(x)
{
prenom=x ;
} ;
function vieillit()
{
dateNaissance-- ;
}
}
var anatole = new Personne ({prenom: "Anatole", "genre: homme", dateNaissance:1990, caractere: "frivole"}) ;
alert(anatole.genre);//renvoie "homme"
var bernadette = new Personne ({prenom: "Bernadette", genre: "femme", dateNaissance: 1991, caractere: "très chouette"}) ;
alert(bernadette.genre);//renvoie "femme"
alert(anatole.prenom);//renvoie undefined car prenom est une propriété privée
alert(anatole.getPrenom());//renvoie Anatole
bernadette.setPrenom("Noémie") ;
alert(bernadette.getPrenom());//renvoie Noémie
bernadette.vieillit();//renvoie une erreur car vieillit n'est pas une méthode publique
Tout ce qui commence par this
est défini comme une propriété ou une méthode publique de la fonction, donc esst appelable de l'extérieur. Le reste est privé : on peut l'utiliser (comme la fonction vieillit
par exemple) dans la fonction, mais pas à l'extérieur.
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 :
function Etudiant(fiche, note=0)
{
Personne.call(fiche, note) ;
let etudiantNote = note ;
this.getNote = function()
{
return etudiantNote ;
} ;
}
Etudiant.prototype = Object.create(Personne.prototype) ;
Etudiant.prototype.constructor = Etudiant ;
On peut alors écrire par exemple...
var marieBerthe = new Etudiant ( {prenom:"Marie-Berthe", genre:"femme", caractere:"experte"},17) ;
alert(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...
var fratrie = {
prenom1: 'Abel' ,
prenom2: 'Yves' ,
prenom3: 'Hakim' ,
nom: 'Flaille' ,
toutLeMonde: function() {
console.log(this.prenom1 + ', ' + this.prenom2 + ', ' + this.prenom3 + ', ' + this.nom) ;
}
} ;
fratrie.toutLeMonde() ;
var 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 :
var fratrie = {
prenom1: 'Abel' ,
prenom2: 'Yves' ,
prenom3: 'Hakim' ,
nom: 'Flaille' ,
toutLeMonde: function() {
return
this.prenom1 + ', ' + this.prenom2 + ', ' + this.prenom3 + ', ' + this.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...
var fratrie = {
prenom1: 'Abel' ,
prenom2: 'Yves' ,
prenom3: 'Hakim' ,
nom: 'Flaille' ,
toutLeMonde: function() {
document.getElementById("ident").addEventListener("click",function() {
console.log(this.nom) ;
}.bind(this), 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…
class MaClasse {
constructor(…) {
this.prop1=… ;
}
methode1() {
//Code de la méthode
}
}
let monObjet = new maClasse(…) ;
constructor
est un nom réservé. Par exemple…
class Voiture {
constructor(modele, nombreRoues) {
this.marque=modele ;
this.nombreRoues=nombreRoues ;
}
demarre() {
alert ("Vroum!") ;
}
}
let maPetiteVoiture = new 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.
On ajoute simplement des propriétés, publiques ou privées :
class Voiture {
constructor(modele, nombreRoues) {
this.marque=modele ;
this.nombreRoues=nombreRoues ;
}
attacheCaravane//propriété publique silmplement déclarée (vaut undefined)
autoRadio = false;//propriété publique avec valeur par défaut
#codeAlarme = 1234;//propriété privée
demarre() {
alert ("Vroum!") ;
}
}
let maPetiteVoiture = new Voiture("Avtoros Shaman", 8) ;
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
Cette création est mise à disposition par Gilles Chagnon sous un contrat Creative Commons.