Le modèle objet¶
Le JavaScript utilise un modèle objet un peu particulier puisqu’il se base
sur le principe du prototype. Il diffère de la plupart des autres langages
de programmation qui utilisent la notion de classe. Depuis ES6, le mot-clé
classe
a été introduit pour faciliter la compréhension du code.
Créer des objets¶
Il existe deux façons de créer des objets en JavaScript : la déclaration littérale et l’utilisation d’une fonction de construction (appelée plus simplement constructeur).
La déclaration littérale¶
La déclaration littérale consiste à utiliser des accolades :
let utilisateur = {
prenom: "David",
nom: "Gayerie"
}
console.log(utilisateur.prenom, utilisateur.nom);
Si on désire ajouter des méthodes à l’objet, on peut utiliser les fonctions anonymes :
let utilisateur = {
prenom: "David",
nom: "Gayerie",
print: function() {
console.log(this.nom, this.prenom);
}
}
utilisateur.print();
Le mot-clé this
permet d’accéder aux attributs et aux méthodes de l’objet
à l’intérieur d’une méthode.
Note
Depuis ES6, il est possible de déclarer directement la fonction sans passer par une affectation d’une fonction anonyme :
let utilisateur = {
prenom: "David",
nom: "Gayerie",
print() {
console.log(this.nom, this.prenom);
}
}
utilisateur.print();
Le constructeur¶
Un constructeur est une fonction qui permet de créer un objet. Par convention ces fonctions commencent par une majuscule :
function Utilisateur() {
this.prenom = "David";
this.nom = "Gayerie";
this.print = function() {
console.log(this.nom, this.prenom);
}
}
Pour créer un objet, il faut utiliser le mot-clé new
:
let u = new Utilisateur;
u.print();
Un des intérêts d’un constructeur est d’utiliser des paramètres pour initialiser l’état de l’objet :
function Utilisateur(prenom, nom) {
this.prenom = prenom;
this.nom = nom;
this.print = function() {
console.log(this.nom, this.prenom);
}
}
let u = new Utilisateur("David", "Gayerie");
u.print();
Note
En utilisant le principe de la fermeture (closure) sur les fonctions, il est possible de créer un état interne et privé pour un objet :
function Utilisateur(prenom, nom) {
let _prenom = prenom;
let _nom = nom;
this.print = function() {
console.log(_nom, _prenom);
}
}
let u = new Utilisateur("David", "Gayerie");
u.print();
Dans l’exemple précédent, les variables _prenom
et _nom
sont locales
à la fonction et ne seront pas visibles à travers la variable u
.
Le prototype¶
Un constructeur possède un attribut prototype
qui permet de stocker les méthodes
d’un objet. Plutôt que d’utiliser le mot-clé this
pour affecter des méthodes
à la construction, on ajoute les méthodes au prototype.
function Utilisateur(prenom, nom) {
this.prenom = prenom;
this.nom = nom;
}
Utilisateur.prototype.print = function() {
console.log(this.nom, this.prenom);
}
let u = new Utilisateur("David", "Gayerie");
u.print();
Nous verrons dans la section suivante que l’utilisation du prototype
optimise
la gestion mémoire.
Note
Tous les objets créés possèdent un attribut constructor
qui permet d’accéder
à la fonction de construction :
function MaClasse() {
}
let o = new MaClasse;
console.log(o.constructor === MaClasse); // Affiche true
La notion de propriété¶
Une propriété est accessible comme un attribut sauf que sa lecture ou sa modification sont réalisées par l’appel à une fonction. Il s’agit la plupart du temps d’une valeur calculée ou dérivée d’autres attributs de l’objet.
Pour une déclaration littérale d’un objet, on utilise les mots-clés get
et set
:
let utilisateur = {
prenom: "David",
nom: "Gayerie",
get nom_complet() {
return this.prenom + " " + this.nom;
},
set nom_complet(v) {
let t = v.split(" ");
this.prenom = t[0];
this.nom = t[1];
}
}
console.log(utilisateur.nom_complet); // affiche David Gayerie
utilisateur.nom_complet = "John Doe";
console.log(utilisateur.prenom, utilisateur.nom); // affiche John Doe
Il est également possible de définir dynamiquement une propriété sur un objet grâce à la méthode Object.defineProperty.
Note
Depuis ES6, il existe aussi la méthode Reflect.defineProperty.
L’héritage¶
En JavaScript, un objet peut être créé à partir d’une fonction de construction. Cette dernière est associé à un prototype qui est un objet représentant l’objet parent.
Par défaut, le prototype correspond à un objet qui ne contient que la propriété
constructor
qui désigne la fonction elle-même.
Pour créer un héritage, il suffit de modifier l’objet prototype pour lui assigner un objet parent.
function Parent() {
this.nom = "parent";
}
function Enfant() {
}
Enfant.prototype = new Parent;
let e = new Enfant;
console.log(e.nom); // Affiche parent
Lorsqu’on accède à un attribut, une propriété ou une méthode, l’interpréteur commence
par le rechercher dans l’objet et, s’il n’existe pas, il recherche récursivement
dans l’objet prototype. Dans l’exemple précédent, on peut bien dire que l’attribut
nom
est hérité de l’objet prototype Parent
.
Le mécanisme d’héritage est très souple en JavaScript puisqu’on peut accéder
à l’objet prototype d’un objet avec la méthode Object.getPrototypeOf(o)
mais
on peut également changer dynamiquement le prototype d’un objet grâce à la méthode
Object.setPrototypeOf(o, p)
.
La notion de classe depuis ES6¶
Depuis ES6, la notion de classe a été introduite car elle est plus largement utilisée dans les langages de programmation objet. Cependant, cela ne change rien dans le principe de programmation objet par prototype de JavaScript. Il s’agit plus d’un sucre syntaxique.
class Utilisateur {
constructor(prenom, nom) {
this.prenom = prenom;
this.nom = nom;
}
print() {
console.log(prenom, nom);
}
}
let u = new Utilisateur("David", "Gayerie");
u.print();
La méthode nommée constructor
correspond à la fonction de construction et
toutes les autres méthodes déclarées dans la classe sont ajoutées au prototype
de la fonction de construction.
Il est également possible de déclarer des propriétés grâce aux mots-clés get
et set
.
class Utilisateur {
constructor(prenom, nom) {
this.prenom = prenom;
this.nom = nom;
}
get nom_complet() {
return `${this.prenom} ${this.nom}`;
}
set nom_complet(v) {
let t = v.split(" ");
this.prenom = t[0];
this.nom = t[1];
}
}
La déclaration d’une méthode de générateur doit commencer par *
:
class Compteur {
* compter(max) {
for (let c = 0; c <= max; ++c) {
yield c;
}
}
}
let compteur = new Compteur();
for (let i of compteur.compter(10)) {
console.log(i);
}
Une méthode avec le mot-clé static
désigne une fonction qui est directement
ajoutée à la fonction de construction et non au prototype. Il s’agit donc
d’une fonction accessible à travers le nom de la classe et dans laquelle on ne
peut pas utiliser le mot-clé this
:
class Utilisateur {
constructor(prenom, nom) {
this.prenom = prenom;
this.nom = nom;
}
static meme_famille(u1, u2) {
return u1.nom === u2.nom;
}
}
let u1 = new Utilisateur("David", "Gayerie");
let u2 = new Utilisateur("Eric", "Gayerie");
console.log(Utilisateur.meme_famille(u1, u2)); // Affiche true
L’héritage avec les classes¶
Pour préciser l’héritage, on utilise le mot-clé extends
.
class Animal {
mange(a) {
console.log(`mange ${a}`);
}
}
class Chien extends Animal {
}
let c = new Chien;
c.mange("un os"); // affiche mange un os
Utilisation du mot-clé super¶
Pour faciliter l’héritage, ES6 introduit le mot-clé super
. Lorsqu’il est
utilisé dans un constructeur, cela permet d’appeler le constructeur de la classe
parente (en lui passant éventuellement des paramètres). Il peut également servir
à appeler une méthode déclarée dans la superclasse.
class Personne {
constructor(prenom, nom) {
this.prenom = prenom;
this.nom = nom;
}
print() {
console.log(this.prenom, this.nom);
}
}
class Utilisateur extends Personne {
constructor(id, prenom, nom) {
super(prenom, nom);
this.id = id;
}
print() {
console.log(id);
super.print();
}
}