Classes et modules
Cours · JavaScript : rendre le Web interactif (école d'ingénieur), chapitre 6 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun
Au chapitre précédent, un livre était un objet littéral, écrit à la main. Pour la Librairie du Campus, cela suffit tant qu'il s'agit de données. Mais le panier, lui, a un comportement : on y ajoute un livre, on en retire un, il calcule son total, il refuse une quantité nulle, et personne ne doit pouvoir modifier ses lignes sans passer par lui. C'est exactement ce que fait une classe en Java : regrouper un état et les méthodes qui le protègent.
JavaScript a des classes depuis 2015, avec une syntaxe volontairement proche de celle de Java : class, constructor, extends, super, static, des champs privés. Mais sous cette syntaxe familière, le mécanisme est différent : il n'y a pas de types, pas de surcharge, et l'héritage passe par des prototypes. Ce chapitre présente les classes, ce qu'elles cachent, et les modules, qui permettent de ranger chaque classe dans son fichier, comme en Java chaque classe publique a le sien.
Objectifs du chapitre
- Écrire une classe : constructeur, méthodes, champs publics et privés, accesseurs, membres statiques.
- Hériter avec
extendsetsuper; redéfinir une méthode ; tester avecinstanceof. - Comprendre ce qu'une classe produit réellement : une fonction et un prototype partagé.
- Éviter de perdre
thisen passant une méthode comme rappel. - Découper un programme en modules :
export,import, export par défaut.
6.1 De l'objet littéral à la classe
Pour fabriquer plusieurs livres de même forme, on peut écrire une fonction qui rend un objet littéral, une fabrique :
function creerLivre(titre, prix) {
return {
titre,
prix,
etiquette() {
return `${this.titre} — ${this.prix} €`;
},
};
}
const a = creerLivre("Analyse MPSI", 45);
const b = creerLivre("Le Petit Prince", 8.5);
console.log(a.etiquette());
console.log(a.etiquette === b.etiquette);
Analyse MPSI — 45 €
false
Cela fonctionne, mais chaque appel crée une nouvelle fonction etiquette : cent livres, cent copies de la même méthode (d'où le false). La classe résout ce problème et donne un nom au type :
class Livre {
constructor(titre, prix) {
this.titre = titre;
this.prix = prix;
}
etiquette() {
return `${this.titre} — ${this.prix} €`;
}
remise(pourcentage) {
return new Livre(this.titre, Math.round(this.prix * (100 - pourcentage)) / 100);
}
}
const a = new Livre("Analyse MPSI", 45);
const b = new Livre("Le Petit Prince", 8.5);
console.log(a.etiquette());
console.log(a.remise(10).etiquette());
console.log(a.etiquette === b.etiquette);
console.log(a instanceof Livre, typeof Livre);
console.log(a);
Analyse MPSI — 45 €
Analyse MPSI — 40.5 €
true
true function
Livre { titre: 'Analyse MPSI', prix: 45 }
Une déclaration class Nom { ... } définit un constructeur, la méthode constructor, appelée par new Nom(...) sur un objet neuf que this désigne, et des méthodes, partagées par tous les objets créés. objet instanceof Nom teste si un objet a été créé par cette classe (ou une sous-classe).
Les différences avec Java sont importantes :
- aucune déclaration de type : les propriétés (
this.titre) sont créées en les affectant ; - un seul constructeur et pas de surcharge : une seconde méthode de même nom remplace la première ; on simule plusieurs signatures avec des valeurs par défaut ou un objet d'options ;
thisest obligatoire pour accéder à une propriété ou appeler une méthode de l'objet, même à l'intérieur de la classe ;typeof Livrevaut"function": une classe est une fonction, que l'on ne peut appeler qu'avecnew.
class Livre {
constructor(titre) {
this.titre = titre;
}
}
const l = Livre("Analyse MPSI");
console.log(l);
Uncaught TypeError: Class constructor Livre cannot be invoked without 'new'
6.2 Champs privés, accesseurs, membres statiques
6.2.1 Les champs privés
Une propriété ordinaire est publique : n'importe quel code peut la lire et la modifier. Depuis 2022, un nom commençant par # déclare un champ privé, inaccessible hors de la classe. Ce n'est pas une convention : le moteur l'interdit.
class Stock {
#quantite;
static #nombreDeStocks = 0;
constructor(quantite) {
if (!Number.isInteger(quantite) || quantite < 0) {
throw new RangeError(`Quantité invalide : ${quantite}`);
}
this.#quantite = quantite;
Stock.#nombreDeStocks += 1;
}
get quantite() {
return this.#quantite;
}
get epuise() {
return this.#quantite === 0;
}
retirer(n) {
if (n > this.#quantite) {
throw new RangeError(`Stock insuffisant : ${this.#quantite} < ${n}`);
}
this.#quantite -= n;
}
static get nombre() {
return Stock.#nombreDeStocks;
}
}
const java = new Stock(12);
java.retirer(3);
console.log(java.quantite, java.epuise);
try {
java.quantite = 100;
} catch (erreur) {
console.log(erreur.message);
}
console.log(Object.keys(java), JSON.stringify(java));
new Stock(0);
console.log(Stock.nombre);
try {
java.retirer(50);
} catch (erreur) {
console.log(erreur.name, "-", erreur.message);
}
9 false
Cannot set property quantite of #<Stock> which has only a getter
[] {}
2
RangeError - Stock insuffisant : 9 < 50
- Les champs se déclarent dans le corps de la classe, avec ou sans valeur initiale (
#quantite;ou#reserve = 0;). - Un champ privé n'apparaît ni dans
Object.keysni dansJSON.stringify: vu de l'extérieur, l'objet est vide. - Le constructeur valide ses arguments et lève une exception (
throw new RangeError(...)) plutôt que de créer un objet incohérent ; les exceptions sont détaillées au chapitre 11.
La suite du cours est sur Adloun
Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.
Lire la suite du coursEn accès libre, sans inscription.