Les Classes

Présentation

Une classe décrit un type d'objet. Elle définit un ensemble de données et de comportements, puis permet de créer des objets concrets à partir de ce modèle. Ces objets sont appelés des instances.

Quand un projet grandit, les variables et les fonctions liées à une même responsabilité ont tendance à se disperser. Une classe permet de les regrouper dans un bloc cohérent, réutilisable autant de fois que nécessaire.

Une classe encourage aussi un principe appelé encapsulation qui consiste à exposer une interface claire tout en gardant certains détails internes hors de portée du reste du code. L'objectif est de rendre l'objet plus simple à utiliser et plus difficile à mettre dans un état incohérent.

Initiation aux classes

Déclarer une classe revient à utiliser le mot-clé class, suivi d'un nom. Par convention, ce nom utilise le PascalCase, c'est-à-dire que toutes les lettres sont en minuscules, sauf la première lettre de chaque mot composant le nom qui est en majuscule (ex.: PersonnageJoueur).

Une classe sert à définir à l'avance le modèle des objets que l'on voudra créer. Elle permet de regrouper dans un même bloc les données que ces objets devront mémoriser, ainsi que les actions qu'ils pourront réaliser.

Une classe permet de définir à l'avance quelles données un objet possédera et quelles actions il pourra effectuer. Une classe peut contenir plusieurs éléments :

  • Un champ est une donnée prévue dans la classe pour être mémorisée dans chaque objet créé. On peut le voir comme une variable attachée à l'objet.
  • Le constructeur est une partie spéciale de la classe, nommée constructor. Il s'exécute automatiquement lors de la création de l'objet. Il reçoit souvent des arguments au moment de l'instanciation et les utilise pour donner des valeurs de départ aux champs. Si aucun constructeur n'est défini, JavaScript utilise automatiquement un constructeur par défaut vide.
  • Une méthode est une fonction placée dans la classe. Elle définit une action que l'objet peut effectuer.

L'instanciation correspond au moment où l'on crée un objet concret à partir de la classe, grâce au mot-clé new. La classe joue le rôle de modèle, et l'instance est l'objet réellement créé en mémoire.

Une même classe peut donc servir à créer plusieurs objets distincts. Lors de l'instanciation, les champs déclarés dans la classe deviennent des propriétés de l'objet créé. Chaque instance possède donc ses propres propriétés et ses propres valeurs, même si toutes ont été créées à partir du même modèle.


                    // Création de la classe "Lux"
                    class Lux
                    {
                        // Champs d'instance
                        nom = 'Lux';
                        niveau;

                        // Le constructeur est appelé lors du "new"
                        constructor(niveau = 1)
                        {
                            // "this" représente l'instance en cours de création
                            this.niveau = niveau;
                        }

                        // Méthode d'instance
                        afficherNiveau()
                        {
                            console.log(`${this.nom} est de niveau ${this.niveau} !`);
                        }
                    }

                    // Instanciation de la classe "Lux" pour le joueur 1
                    const joueur1 = new Lux(1);

                    // Instanciation de la classe "Lux" pour le joueur 2
                    const joueur2 = new Lux(5);

                    // Appel de la méthode "afficherNiveau()" pour chaque instance
                    joueur1.afficherNiveau(); // Affiche "Lux est de niveau 1 !"
                    joueur2.afficherNiveau(); // Affiche "Lux est de niveau 5 !"
                
  1. Une classe Lux est créée. Elle décrit un type d'objet.
  2. Deux instances sont créées avec new. Les champs déclarés dans la classe deviennent alors des propriétés de chaque objet créé. Chaque instance possède donc ses propres valeurs.
  3. La méthode est appelée via la notation point. Elle s'exécute sur l'instance ciblée.

Les propriétés appartiennent à chaque instance, c'est-à-dire que deux objets issus de la même classe peuvent contenir des valeurs différentes.

Les méthodes déclarées avec la syntaxe de définition de méthode dans le corps d'une classe, comme afficherNiveau() dans l'exemple précédent, sont partagées via le prototype. Toutes les instances réutilisent donc la même fonction, ce qui évite de dupliquer cette méthode dans chaque objet et réduit la consommation de mémoire.

Quand une méthode doit accéder aux propriétés de l'objet, elle passe par this. Cette référence désigne l'instance sur laquelle la méthode est appelée.


                    // Création de la classe "Lux".
                    class Lux
                    {
                        nom = 'Lux';
                        niveau;
                        pointsDeVie;

                        constructor(niveau = 1)
                        {
                            this.niveau = niveau;

                            // L'initialisation de "this.pointsDeVie" dépend du résultat de la méthode "calculerPointsDeVieInitiaux()".
                            // Le préfixe "this" indique que la méthode est appelée sur l'instance en cours de création.
                            this.pointsDeVie = this.calculerPointsDeVieInitiaux();
                        }

                        calculerPointsDeVieInitiaux()
                        {
                            return 560 + (this.niveau * 100);
                        }

                        afficherPointsDeVie()
                        {
                            console.log(`Le nombre de points de vie de ${this.nom} est ${this.pointsDeVie} !`);
                        }
                    }

                    const joueur1 = new Lux(1);
                    const joueur2 = new Lux(2);

                    joueur1.afficherPointsDeVie(); // Affiche "Le nombre de points de vie de Lux est 660 !"
                    joueur2.afficherPointsDeVie(); // Affiche "Le nombre de points de vie de Lux est 760 !"
                

Encapsulation et Contrôle d'Accès

Public et privé

Par défaut, les éléments déclarés sans symbole particulier dans une classe sont publics. Cela concerne notamment les champs et les méthodes, qui peuvent être utilisés depuis l'extérieur de l'instance avec la notation point.

Lorsqu'un élément ne doit pas faire partie de l'interface publique de l'objet, JavaScript permet de le rendre privé grâce au préfixe #. Cela peut concerner un champ, pour protéger un état interne, mais aussi une méthode, si elle ne doit être utilisée qu'à l'intérieur de la classe.

Les éléments privés doivent être déclarés directement dans le corps de la classe. Ils ne peuvent pas être ajoutés dynamiquement depuis l'extérieur et restent accessibles uniquement à l'intérieur de la classe qui les déclare.


                    class Lux
                    {
                        // Champs privés : accessibles uniquement depuis le code de cette classe.
                        #nom = 'Lux';
                        #niveau;
                        #pointsDeVie;

                        constructor(niveau = 1)
                        {
                            this.#niveau = niveau;
                            this.#pointsDeVie = this.#calculerPointsDeVieInitiaux();
                        }

                        afficherPointsDeVie()
                        {
                            console.log(`Le nombre de points de vie de ${this.#nom} est ${this.#pointsDeVie} !`);
                        }

                        // Méthode privée : utilisable uniquement à l'intérieur de la classe.
                        #calculerPointsDeVieInitiaux()
                        {
                            return 560 + (this.#niveau * 100);
                        }
                    }

                    const joueur1 = new Lux(1);
                    joueur1.afficherPointsDeVie(); // Affiche "Le nombre de points de vie de Lux est 660 !"

                    // Tentative d'accès direct à un champ privé.
                    console.log(joueur1.#nom); // SyntaxError
                

Quand rendre un champ privé ?

Contrairement à des langages comme Java ou C# où la recommandation classique est de tout rendre privé par défaut, JavaScript adopte une approche plus pragmatique. Le préfixe # comporte des contraintes spécifiques qu'il faut connaître avant de l'appliquer partout.

Pas d'accès depuis les classes enfants

Les champs privés sont scopés à la classe qui les déclare, pas à la hiérarchie. Si une classe parente déclare #pointsDeVie, ses classes enfants ne pourront pas y accéder directement, même via this. JavaScript ne propose pas d'équivalent au mot-clé protected d'autres langages. Une convention courante consiste à préfixer certains membres par un underscore _ pour signaler une intention de protection, sans imposer de vraie restriction d'accès.

Invisibilité dans les outils de parcours d'objets

Les champs privés n'apparaissent pas dans Object.keys(), dans une boucle for...in, ni dans l'opérateur de décomposition { ...objet }. Pour un débutant, cela peut provoquer une surprise lors de manipulations courantes sur des objets.

Incompatibilité avec JSON.stringify()

Les champs privés sont ignorés lors de la sérialisation en JSON. C'est une contrainte importante à comprendre, car elle touche directement les échanges avec un serveur.

Lorsqu'une application web envoie des données à une API via fetch(), elle doit généralement convertir un objet JavaScript en texte JSON pour le placer dans le corps de la requête. C'est le rôle de JSON.stringify(). Or, si l'objet à envoyer est une instance d'une classe dont tous les champs sont privés, JSON.stringify() produira un objet vide {}, car il ne voit aucun champ accessible.


                    class Personnage
                    {
                        #nom = 'Lux';
                        #niveau = 5;
                    }

                    const perso = new Personnage();

                    console.log(JSON.stringify(perso)); // Affiche "{}"
                    // Les champs #nom et #niveau sont ignorés.
                

Pour contourner ce problème, il faut ajouter manuellement une méthode toJSON() à la classe. JSON.stringify() l'appelle automatiquement si elle existe, et utilise ce qu'elle retourne comme données à sérialiser.


                    class Personnage
                    {
                        #nom = 'Lux';
                        #niveau = 5;

                        toJSON()
                        {
                            return {
                                nom: this.#nom,
                                niveau: this.#niveau
                            };
                        }
                    }

                    const perso = new Personnage();

                    console.log(JSON.stringify(perso)); // Affiche '{"nom":"Lux","niveau":5}'
                

La question à se poser n'est donc pas "ai-je oublié de mettre un # ?", mais plutôt "cette propriété a-t-elle besoin d'une protection ?". Un champ qui représente un état interne pouvant mettre l'objet en incohérence (des points de vie, un compteur, une référence à un timer) gagne à être privé. Un champ qui stocke une simple donnée sans invariant à protéger (un nom, une couleur, une option de configuration) peut rester public sans problème.

Les accesseurs (getters)

Pour autoriser une lecture contrôlée d'un champ privé, une classe peut définir un accesseur avec le mot-clé get. Il se lit comme une propriété ordinaire, mais exécute du code avant de retourner la valeur.


                    class Lux
                    {
                        #nom = 'Lux';

                        get nom()
                        {
                            return (this.#nom).toUpperCase();
                        }
                    }

                    const joueur1 = new Lux();

                    // Lire la propriété "nom" via l'accesseur.
                    console.log(joueur1.nom); // Affiche "LUX"

                    // Tentative d'accès direct au champ privé.
                    // console.log(joueur1.#nom); // SyntaxError
                

Les mutateurs (setters)

Un mutateur est défini avec set. Il s'utilise comme une affectation classique, mais permet de valider ou transformer la donnée avant de la stocker.


                    class Lux
                    {
                        #nom = 'Lux';

                        get nom()
                        {
                            return (this.#nom).toUpperCase();
                        }

                        set nom(nouveauNom)
                        {
                            // Retirer les espaces aux extrémités avant de stocker la valeur.
                            this.#nom = nouveauNom.trim();
                        }
                    }

                    const joueur1 = new Lux();

                    // Modifier "nom" en passant par le mutateur.
                    joueur1.nom = " lox   ";

                    // Lire "nom" en passant par l'accesseur.
                    console.log(joueur1.nom); // Affiche "LOX"
                

Quand utiliser des accesseurs et mutateurs ?

En Java ou C#, la recommandation classique est de systématiquement passer par des getters et setters, même quand ils ne font que lire ou écrire la valeur sans aucune logique. La raison tient à la façon dont ces langages gèrent l'interface d'une classe. Si une propriété est d'abord déclarée publique, puis remplacée plus tard par un getter, tout le code qui utilisait objet.nom doit être réécrit en objet.getNom(). Pour éviter ce refactoring coûteux, la convention consiste à tout encapsuler dès le départ.

En JavaScript, ce problème n'existe pas. On peut commencer avec un champ public, puis le remplacer plus tard par un getter/setter sans que le code appelant ne change. Dans les deux cas, l'écriture reste joueur1.nom. Le langage ne fait pas de distinction syntaxique entre l'accès à un champ et l'appel à un accesseur.

La recommandation en JavaScript est donc d'utiliser un getter ou un setter uniquement quand il y a une raison concrète de le faire, comme valider une entrée, transformer une valeur en sortie, calculer un résultat dérivé, ou contrôler l'accès à un champ privé. Si la propriété ne fait que stocker une valeur sans logique particulière, un champ public suffit. On pourra toujours ajouter un getter/setter plus tard si le besoin évolue, sans casser le code existant.

Le Contexte "this"

En JavaScript, this dépend de la manière dont une fonction est appelée. Dans une méthode appelée via la notation point (joueur1.afficherNiveau()), this désigne l'instance. Mais dès qu'une méthode est transmise comme fonction de rappel (callback), le contexte change.

Les gestionnaires d'événements du DOM illustrent bien ce comportement. Quand le navigateur appelle la fonction de rappel, il le fait avec son propre contexte. Dans ce cas, this ne désigne plus l'instance, mais l'élément DOM qui a déclenché l'événement. L'accès à un champ ou une autre méthode échoue alors, non pas parce que this est undefined, mais parce qu'il pointe sur le mauvais objet (ici, document).


                    class Lux
                    {
                        #nom = 'Lux';

                        constructor()
                        {
                            // La méthode est transmise comme callback.
                            // Quand le navigateur l'appellera, "this" ne sera plus l'instance
                            // mais l'élément DOM qui a déclenché l'événement (ici, "document").
                            document.addEventListener("click", this.#gererClicSouris);
                        }

                        #gererClicSouris(event)
                        {
                            // "this" désigne "document", pas l'instance de Lux.
                            // "document" n'a pas de champ "#nom", d'où l'erreur.
                            console.log(this.#nom); // TypeError
                        }
                    }
                

Deux techniques courantes permettent de corriger ce problème.

Solution 1 : fonction fléchée stockée dans un champ

Une fonction fléchée ne possède pas son propre this, elle capture celui du contexte dans lequel elle est créée (le this lexical). En stockant une fonction fléchée dans un champ de la classe, this reste celui de l'instance, quel que soit le contexte d'appel.


                    class Lux
                    {
                        #nom = 'Lux';

                        constructor()
                        {
                            // La référence est stable car elle est stockée dans le champ.
                            document.addEventListener("click", this.#gererClicSouris);
                        }

                        // Champ contenant une fonction fléchée.
                        // Le "this" est capturé au moment de la création de l'instance
                        // et reste stable quel que soit le contexte d'appel.
                        #gererClicSouris = (event) =>
                        {
                            console.log(this.#nom); // Affiche "Lux"
                        };
                    }
                

Solution 2 : bind()

bind() crée une nouvelle fonction dont le this est fixé définitivement. Il faut conserver la référence de cette fonction liée pour pouvoir retirer le listener plus tard.


                    class Lux
                    {
                        #nom = 'Lux';
                        #gererClicSourisLiee;

                        constructor()
                        {
                            // Créer une fonction liée une seule fois et mémoriser sa référence.
                            this.#gererClicSourisLiee = this.#gererClicSouris.bind(this);

                            // Utiliser la référence liée lors de l'inscription.
                            document.addEventListener("click", this.#gererClicSourisLiee);
                        }

                        #gererClicSouris(event)
                        {
                            console.log(this.#nom); // Affiche "Lux"
                        }
                    }
                

Quelle que soit la technique utilisée pour préserver le this, elle entraîne la création d'une fonction propre à chaque instance (elle n'est pas partagée via le prototype). C'est très pratique pour les callbacks, mais il faut garder à l'esprit ce coût mémoire si l'on crée un grand nombre d'instances.

La Méthode destroy()

JavaScript utilise un ramasse-miettes (garbage collector), c'est-à-dire que la mémoire est libérée automatiquement quand un objet n'est plus référencé. Cependant, certains éléments peuvent maintenir une référence vivante sans que cela soit visible (ex.: un écouteur d'événement, un setInterval, ou une souscription à un flux).

Une méthode destroy() sert à effectuer ce nettoyage explicite pour retirer les écouteurs, stopper les timers, ou annuler les abonnements (ex.: WebSocket). Elle s'avère utile dans les applications à page unique (SPA) ou les composants dynamiques, où des éléments sont créés et détruits au fil des interactions de l'utilisateur sans rechargement de page.


                    class Lux
                    {
                        // Conserver la référence du callback permet de retirer le listener.
                        #gererClicSouris;

                        constructor()
                        {
                            // Stocker la fonction dans un champ pour garder une référence stable.
                            this.#gererClicSouris = (event) =>
                            {
                                // Exécuter la logique en fonction de l'endroit cliqué
                                // boutons d'interface, endroit de la carte, etc.
                            };

                            document.addEventListener("click", this.#gererClicSouris);
                        }

                        // Nettoyage explicite avant de laisser partir l'objet.
                        destroy()
                        {
                            document.removeEventListener("click", this.#gererClicSouris);
                        }
                    }

                    // "let" est utilisé ici (au lieu de "const") car on doit pouvoir
                    // réassigner la variable à "null" après destruction.
                    let joueur1 = new Lux();

                    // Appel explicite de "destroy()" pour libérer les ressources.
                    joueur1.destroy();

                    // Supprimer la référence pour que le garbage collector puisse libérer l'objet.
                    joueur1 = null;
                

Interaction entre Instances

Une fois plusieurs instances créées, l'étape suivante consiste souvent à les faire interagir. Un objet peut recevoir un autre objet en paramètre, puis déclencher une action sur celui-ci.


                    class Lux
                    {
                        #nom = 'Lux';
                        #degatsPhysiques = 54;
                        #niveau;
                        #pointsDeVie;

                        constructor(niveau = 1)
                        {
                            this.#niveau = niveau;
                            this.#pointsDeVie = this.#calculerPointsDeVieInitiaux();
                        }

                        get nom()
                        {
                            return (this.#nom).toUpperCase();
                        }

                        set nom(nouveauNom)
                        {
                            this.#nom = nouveauNom.trim();
                        }

                        lancerAttaquePhysique(cible)
                        {
                            console.log(`${this.nom} attaque ${cible.nom} !`);
                            cible.encaisserDegatsPhysiques(this.nom, this.#degatsPhysiques);
                        }

                        encaisserDegatsPhysiques(nomAttaquant, degats)
                        {
                            console.log(`${this.nom} s'est fait toucher par ${nomAttaquant} !`);
                            this.#pointsDeVie -= degats;
                            console.log(`${this.nom} a reçu ${degats} de dégâts et il lui reste ${this.#pointsDeVie} points de vie !`);
                        }

                        #calculerPointsDeVieInitiaux()
                        {
                            return 560 + (this.#niveau * 100);
                        }
                    }

                    const joueur1 = new Lux();
                    const joueur2 = new Lux();
                    joueur2.nom = "Lox";

                    joueur1.lancerAttaquePhysique(joueur2);
                    /*
                        Affiche :
                            LUX attaque LOX !
                            LOX s'est fait toucher par LUX !
                            LOX a reçu 54 de dégâts et il lui reste 606 points de vie !
                    */
                    joueur1.lancerAttaquePhysique(joueur2);
                    /*
                        Affiche :
                            LUX attaque LOX !
                            LOX s'est fait toucher par LUX !
                            LOX a reçu 54 de dégâts et il lui reste 552 points de vie !
                    */
                

Héritage de Classe

L'héritage permet à une classe enfant de réutiliser les propriétés et méthodes d'une classe parente, puis d'y ajouter ce qui la rend spécifique.

Pour illustrer l'intérêt de ce mécanisme, observons deux classes qui partagent l'essentiel de leur code :


                    class Champion
                    {
                        #nom;
                        #niveau;
                        #pointsDeVie;
                        #degatsPhysiques;

                        constructor(nom, pointsDeVieBase, facteurPointsDeVie, degatsPhysiques, niveau = 1)
                        {
                            this.#nom = nom;
                            this.#niveau = niveau;
                            this.#pointsDeVie = this.#calculerPointsDeVieInitiaux(pointsDeVieBase, facteurPointsDeVie);
                            this.#degatsPhysiques = degatsPhysiques;
                        }

                        get nom()
                        {
                            return (this.#nom).toUpperCase();
                        }

                        set nom(nouveauNom)
                        {
                            this.#nom = nouveauNom.trim();
                        }

                        lancerAttaquePhysique(cible)
                        {
                            // Vérifier que la cible est bien un Champion (ou une sous-classe de Champion).
                            if (!(cible instanceof Champion))
                            {
                                throw new Error("La cible doit être un Champion.");
                            }

                            console.log(`${this.nom} attaque ${cible.nom} !`);

                            // Méthode publique : accessible entre classes différentes de la même hiérarchie.
                            cible.encaisserDegatsPhysiques(this.nom, this.#degatsPhysiques);
                        }

                        encaisserDegatsPhysiques(nomAttaquant, degats)
                        {
                            console.log(`${this.nom} s'est fait toucher par ${nomAttaquant} !`);
                            this.#pointsDeVie -= degats;
                            console.log(`${this.nom} a reçu ${degats} de dégâts et il lui reste ${this.#pointsDeVie} points de vie !`);
                        }

                        #calculerPointsDeVieInitiaux(pointsDeVieBase, facteurPointsDeVie)
                        {
                            return pointsDeVieBase + (this.#niveau * facteurPointsDeVie);
                        }
                    }
                

extends et super()

Les classes enfants utilisent le mot-clé extends pour hériter de la classe parente. Dans leur constructeur, elles doivent appeler super() avant toute utilisation de this. Cet appel exécute le constructeur de la classe parente et initialise la partie héritée de l'instance.


                    class Lux extends Champion
                    {
                        constructor(niveau = 1)
                        {
                            // "super()" transmet les arguments au constructeur de Champion.
                            // Arguments : nom, points de vie de base, facteur, dégâts physiques, niveau.
                            super('Lux', 560, 100, 54, niveau);
                        }

                        lancerEntraveDeLumiere()
                        {
                            // Compétence propre à Lux...
                        }
                    }

                    class Teemo extends Champion
                    {
                        constructor(niveau = 1)
                        {
                            super('Teemo', 598, 105, 54, niveau);
                        }

                        lancerFlechetteAveuglante(cible)
                        {
                            // Compétence propre à Teemo...
                        }
                    }
                

On peut maintenant instancier les deux classes enfants et les faire interagir sans problème :


                    const joueur1 = new Lux();
                    const joueur2 = new Teemo();

                    joueur1.lancerAttaquePhysique(joueur2);
                    /*
                        Affiche :
                            LUX attaque TEEMO !
                            TEEMO s'est fait toucher par LUX !
                            TEEMO a reçu 54 de dégâts et il lui reste 544 points de vie !
                    */
                    joueur2.lancerAttaquePhysique(joueur1);
                    /*
                        Affiche :
                            TEEMO attaque LUX !
                            LUX s'est fait toucher par TEEMO !
                            LUX a reçu 54 de dégâts et il lui reste 506 points de vie !
                    */
                

Surcharge de méthodes avec super

Une classe enfant peut redéfinir (surcharger) une méthode héritée pour modifier ou enrichir son comportement. Le mot-clé super permet alors d'appeler la version de la méthode définie dans la classe parente, avant ou après le comportement spécifique :


                    class Teemo extends Champion
                    {
                        #poisonActif = true;

                        constructor(niveau = 1)
                        {
                            super('Teemo', 598, 105, 54, niveau);
                        }

                        // Redéfinition de la méthode héritée.
                        lancerAttaquePhysique(cible)
                        {
                            // Appeler la version de la classe parente.
                            super.lancerAttaquePhysique(cible);

                            // Ajouter un comportement spécifique à Teemo.
                            if (this.#poisonActif)
                            {
                                console.log(`${cible.nom} est empoisonné !`);
                            }
                        }
                    }

                    const joueur1 = new Lux();
                    const joueur2 = new Teemo();

                    joueur2.lancerAttaquePhysique(joueur1);
                    /*
                        Affiche :
                            TEEMO attaque LUX !
                            LUX s'est fait toucher par TEEMO !
                            LUX a reçu 54 de dégâts et il lui reste 506 points de vie !
                            LUX est empoisonné !
                    */
                

Simuler une classe abstraite

Dans d'autres langages, une classe parente destinée uniquement à être étendue est appelée classe abstraite, ce qui implique qu'on ne peut pas l'instancier directement. JavaScript ne propose pas de mot-clé natif pour cela, mais on peut rendre cette intention explicite grâce à new.target, qui référence la classe effectivement instanciée. Par convention, certains projets signalent aussi cette intention en préfixant le nom de la classe par Abstract.


                    class AbstractChampion
                    {
                        constructor()
                        {
                            // "new.target" désigne la classe sur laquelle "new" a été appelé.
                            // Si c'est Champion directement, on bloque l'instanciation.
                            if (new.target === Champion)
                            {
                                throw new Error("Champion ne doit pas être instancié directement.");
                            }
                        }

                        // ...
                    }

                    // const test = new Champion(); // Error: Champion ne doit pas être instancié directement.
                    const joueur1 = new Lux();       // Fonctionne : new.target vaut Lux, pas Champion
                

Propriétés et Méthodes Statiques

Une propriété ou méthode statique appartient à la classe elle-même, pas à ses instances. Elle est utile pour stocker une information qui ne dépend pas d'un objet précis, ou pour regrouper des opérations utilitaires.

On peut par exemple s'en servir pour stocker la description d'un personnage, accessible sans avoir à créer une instance.


                    class Lux extends Champion
                    {
                        static lore = "Luxanna Crownguard est originaire de Demacia, un royaume isolationniste où la magie inspire la peur et la méfiance. Capable de plier la lumière à sa volonté, elle a grandi dans la crainte d'être un jour exilée et a été contrainte de dissimuler son pouvoir pour préserver l'honneur de sa famille. Cependant, l'optimisme et la ténacité de Lux l'ont conduite à accepter ses talents uniques, et elle les emploie désormais secrètement pour le bénéfice de sa patrie.";

                        constructor(niveau = 1)
                        {
                            super('Lux', 560, 100, 54, niveau);
                        }

                        static afficherLore()
                        {
                            // Dans une méthode statique, "this" désigne la classe (ici, Lux)
                            // et non une instance.
                            // Attention, comme pour les méthodes d'instance,
                            // si cette méthode est extraite et passée comme callback,
                            // "this" peut changer de contexte.
                            console.log(this.lore);
                        }
                    }

                    // Lire une propriété statique sans instancier.
                    console.log(Lux.lore); // Affiche : Luxanna Crownguard est originaire...

                    // Appeler une méthode statique.
                    Lux.afficherLore(); // Affiche : Luxanna Crownguard est originaire...
                

Une classe contenant uniquement des méthodes statiques peut servir d'outil utilitaire. Dans le contexte web, elle peut regrouper des opérations fréquentes sur le DOM :


                    class GestionDuDom
                    {
                        static creerBalise(balise, options = {})
                        {
                            // Destructuration de l'objet "options" pour récupérer les paramètres par nom,
                            // avec des valeurs par défaut (null ou objet vide) si certaines options ne sont pas fournies.
                            const { conteneur = null, texte = null, attributs = {} } = options;

                            // Créer un élément DOM.
                            const element = document.createElement(balise);

                            // Ajouter les attributs.
                            for (const attribut in attributs)
                            {
                                element.setAttribute(attribut, attributs[attribut]);
                            }

                            // Définir le texte si fourni.
                            if (texte !== null)
                            {
                                element.textContent = texte;
                            }

                            // Ajouter au conteneur si fourni.
                            if (conteneur !== null)
                            {
                                conteneur.appendChild(element);
                            }

                            // Renvoyer l'élément créé.
                            return element;
                        }

                        static ecouter(element, evenement, callback, options = false)
                        {
                            element.addEventListener(evenement, callback, options);

                            // Renvoyer une fonction "unsubscribe" pratique pour un destroy().
                            return function()
                            {
                                element.removeEventListener(evenement, callback, options);
                            };
                        }
                    }

                    // Créer une balise "div" et l'insérer dans la balise "main".
                    GestionDuDom.creerBalise(
                        "div",
                        {
                            "attributs":
                            {
                                "id": "galerie",
                                "class": "galerie-conteneur"
                            },
                            "conteneur": document.querySelector("main")
                        }
                    );