Retour au cours

frontend / javascript

Classes ES6

Leçon 61 exercice

Explication

Ce que vous allez apprendre

  • Écrire une classe avec constructeur, méthodes et propriétés
  • Mettre en place un héritage avec extends et super()
  • Expliquer pourquoi super() doit être appelé avant tout accès à this
  • Déclarer un champ réellement privé avec #champ, et pourquoi c'est différent de _champ
  • Composer plusieurs comportements avec le pattern des mixins, sans héritage multiple

Dans quel contexte ?

Une équipe backend Node.js découvre qu'un champ censé être privé, _solde sur une classe CompteBancaire, est modifié directement depuis un autre module avec compte._solde = 999999, contournant toute la logique de validation du dépôt/retrait. La convention _ n'a jamais été qu'une convention de nommage : rien ne l'empêche techniquement d'être lue ou modifiée de l'extérieur. La correction consiste à migrer vers un vrai champ privé #solde, qui lève une erreur de syntaxe si on tente d'y accéder hors de la classe.

1. Une nouvelle syntaxe, pas un nouveau mécanisme

Les classes ES6 n'introduisent aucun nouveau paradigme en JavaScript : elles habillent le système de prototypes vu à la leçon précédente d'une syntaxe plus proche de ce qu'on trouve en Java ou en Python. L'objectif est la lisibilité : regrouper au même endroit un constructeur, des méthodes et des propriétés.

Prérequis

Cette leçon s'appuie directement sur la leçon précédente sur les prototypes : une classe JavaScript reste, sous le capot, une fonction constructeur avec un prototype. Si la notion de chaîne de prototypes n'est pas claire, relisez-la avant de continuer.

2. Hériter avec extends et super

Le mot-clé extends met en place l'héritage entre classes. super() permet à une classe fille d'appeler le constructeur de sa classe parente — une étape obligatoire avant de pouvoir utiliser this dans le constructeur enfant, sous peine d'erreur.

3. Pourquoi super() est réellement nécessaire

Ce n'est pas un simple détail de syntaxe : super() initialise réellement l'objet parent avant que la classe enfant ne puisse y ajouter ses propres champs. Sans cet appel, l'objet enfant n'existerait tout simplement pas encore.

4. La vraie nouveauté : les champs privés

Une fonctionnalité mérite une attention particulière : les champs privés, notés avec un # devant leur nom. Contrairement à la convention historique _champ (qui reste techniquement accessible depuis l'extérieur), un champ #champ est réellement inaccessible en dehors de la classe.

NotationAccessible depuis l'extérieur ?Nature
champOuiPropriété publique normale
_champOui (juste une convention)Propriété publique "à ne pas toucher"
#champNon (erreur de syntaxe)Vraie propriété privée, imposée par le moteur

Piège fréquent

Croire que _champ protège une donnée est une erreur classique de code review : rien n'empêche instance._champ = "n'importe quoi" depuis n'importe où. Seul #champ offre une vraie garantie, vérifiée par le moteur JavaScript lui-même.

5. Composer sans hiérarchie rigide

JavaScript ne supporte pas l'héritage multiple nativement, mais le pattern des "mixins" — des fonctions qui retournent une classe étendue — permet de composer plusieurs comportements sans construire une hiérarchie rigide.

Cette leçon boucle la compréhension de la programmation orientée objet en JS, amorcée avec les prototypes à la leçon précédente.

Commandes & code

Classes ES6

La syntaxe moderne pour structurer la programmation orientée objet en JS.

js
class CompteBancaire {
    // Champs de classe (class fields, standard depuis ES2022)
    #solde;                    // champ prive : accessible SEULEMENT depuis l'interieur de la classe
    static tauxInteret = 0.02;   // propriete statique, partagee par la classe

    constructor(titulaire, soldeInitial = 0) {
        this.titulaire = titulaire;
        this.#solde = soldeInitial;
    }

    deposer(montant) {
        if (montant <= 0) throw new Error("Montant invalide");
        this.#solde += montant;
        return this;              // permet le chainage de methodes
    }

    retirer(montant) {
        if (montant > this.#solde) throw new Error("Solde insuffisant");
        this.#solde -= montant;
        return this;
    }

    get solde() {                   // getter : accessible comme une propriete
        return this.#solde;
    }

    static creerVide(titulaire) {     // methode statique : constructeur alternatif
        return new CompteBancaire(titulaire, 0);
    }

    toString() {
        return `Compte de ${this.titulaire} : ${this.#solde} EUR`;
    }
}

const compte = new CompteBancaire("Alice", 1000);
compte.deposer(500).retirer(200);     // chainage grace au "return this"
console.log(compte.solde);              // 1300
console.log(`${compte}`);                 // utilise toString()
// console.log(compte.#solde);           // SyntaxError hors de la classe : vraie encapsulation

// Heritage avec extends / super
class CompteEpargne extends CompteBancaire {
    #tauxBonus;

    constructor(titulaire, soldeInitial, tauxBonus) {
        super(titulaire, soldeInitial);      // DOIT etre appele avant tout usage de "this"
        this.#tauxBonus = tauxBonus;
    }

    appliquerInterets() {
        const interets = this.solde * (CompteBancaire.tauxInteret + this.#tauxBonus);
        this.deposer(interets);
        return interets;
    }
}

const epargne = new CompteEpargne("Bob", 1000, 0.01);
epargne.appliquerInterets();
console.log(epargne.solde);

// Polymorphisme : override d'une methode
class CompteVIP extends CompteBancaire {
    retirer(montant) {
        console.log("Retrait VIP : pas de limite de decouvert");
        this.deposer(0);   // exemple simplifie
        return this;
    }
}

// Methodes et champs statiques prives
class CompteurInstances {
    static #nombre = 0;
    constructor() {
        CompteurInstances.#nombre++;
    }
    static get total() {
        return CompteurInstances.#nombre;
    }
}
new CompteurInstances();
new CompteurInstances();
console.log(CompteurInstances.total);    // 2

// Static initialization blocks (ES2022) : logique d'init complexe au niveau classe
class Configuration {
    static valeurs;
    static {
        // s'execute une seule fois au chargement de la classe
        Configuration.valeurs = { env: "production", version: "1.0" };
    }
}

// instanceof et verification de type avec les classes
console.log(epargne instanceof CompteBancaire);    // true
console.log(epargne instanceof CompteEpargne);       // true

// Mixins : simuler l'heritage multiple (JS ne le supporte pas nativement)
const Horodatable = (Base) => class extends Base {
    constructor(...args) {
        super(...args);
        this.creeLe = new Date();
    }
};
class CompteHorodate extends Horodatable(CompteBancaire) {}
const ch = new CompteHorodate("Cy", 500);
console.log(ch.creeLe instanceof Date);    // true

Résumé

  • Les champs privés (#champ) offrent une vraie encapsulation, contrairement à la convention _champ.
  • super() doit être appelé avant tout usage de this dans le constructeur d'une sous-classe.
  • Les mixins simulent l'héritage multiple via des fonctions qui retournent des classes étendues.
  • Les blocs d'initialisation statique (static { }, ES2022) exécutent une logique complexe au chargement.

Exercices pratiques

1 disponible
1

Mission : sécuriser le compte bancaire VIP

Objectif : Corriger une fausse encapsulation avec convention `_champ`, implémenter un héritage correct avec super(), et garantir un vrai champ privé.

Contexte

Un développeur junior de Technologik a écrit cette classe pour un compte bancaire, en pensant que le solde était protégé :

js
class CompteBancaire {
  constructor(titulaire, solde) {
    this.titulaire = titulaire;
    this._solde = solde;
  }
}
const c = new CompteBancaire("Alice", 1000);
c._solde = -99999; // personne n'empêche ça !

Un audit de sécurité interne signale que n'importe quel code peut modifier _solde directement, en contournant toute validation. Il faut aussi créer une classe CompteVIP qui hérite de CompteBancaire et ajoute une méthode retirer sans plafond, tout en restant valide.

Résoudre l’exercice →