Apprenez en lisant dans l'ordre

Champs privés (#) et encapsulation

Les champs et méthodes privés en # sont inaccessibles hors de la classe. Au programme : la SyntaxError levée avant l'exécution et la différence avec la convention _.

Stocker le solde d'une carte prépayée dans _balance ne le protège pas : n'importe quel endroit du code de la page peut écrire card._balance = 999999, et la valeur est enregistrée telle quelle. Le _ en tête de nom n'est qu'une convention qui signifie « à ne pas toucher depuis l'extérieur » ; il n'empêche ni les affectations ni les appels.

Cet article traite des champs privés et des méthodes privées, qu'on ne peut ni lire ni modifier depuis l'extérieur de la classe.

Cacher le solde dans la classe — les champs privés

Tu veux que le solde ne puisse changer qu'en passant par charge, qui applique une limite de 20000 yens par recharge. Avec _balance, si le code d'un écran de remboursement écrit card._balance += 30000, rien ne l'arrête, et un montant supérieur à la limite entre dans le solde.

Un champ privé (un champ déclaré dans une classe avec un # en tête de nom, qu'on ne peut lire et modifier qu'à l'intérieur du corps de cette classe) se déclare sous la forme #balance = 0; et s'utilise sous la forme this.#balance.

Le # fait partie du nom : this.balance et this.#balance sont donc deux noms différents.

class PrepaidCard {
  #balance = 0;                              // Solde. Impossible à lire ou modifier hors de la classe

  charge(amount) {
    if (amount > 20000) {
      throw new Error(`Chaque recharge est limitée à 20000 yens : ${amount}`);
    }
    this.#balance += amount;                 // Dans la classe, donc modifiable
  }

  get balance() { return this.#balance; }    // N'exposer qu'un getter pour la lecture
}

const card = new PrepaidCard();
card.charge(5000);                           // charge est le seul moyen de modifier le solde
card.charge(3000);
console.log(card.balance);                   // 8000
Les lignes qui atteignent #balance ou non
Corps de class PrepaidCard
  • #balance = 0 — un nom utilisable seulement dans ce corps
Corps de charge(amount)
  • Si amount > 20000, throw arrête tout
  • Seuls les montants acceptés arrivent à this.#balance += amount
Corps de get balance()
  • return this.#balance — renvoie la valeur sans jamais la modifier
Lignes hors de la classe
  • card.charge(5000) — modifie le solde via charge
  • card.balance — lit 8000 via le getter
  • Le nom #balance ne peut pas être écrit ici
On ne peut écrire #balance qu'à l'intérieur du corps de la classe. Pour le code extérieur, charge est le seul moyen de modifier le solde.

Grâce à l'encapsulation (une conception qui regroupe les données et le code qui les modifie dans une classe, et limite l'usage extérieur à des points d'entrée définis), le seul moyen de modifier le solde de ce PrepaidCard est charge, et le seul moyen de le lire est le getter balance.

Un nom en # non déclaré provoque une SyntaxError

Un nom en # doit être déclaré dans le corps de la classe, sous la forme #balance = 0; ou #balance;, avant d'être utilisé. Si tu affectes this.#balance = 0; dans le constructor sans l'avoir déclaré, tu obtiens une SyntaxError, même si la ligne se trouve dans la classe. Une simple affectation ne crée pas un nom en #.

Tu fais en sorte que le solde de points d'un membre ne puisse changer que lors des achats et des utilisations de points. PointAccount et orders sont déjà déclarés.

① Transforme le solde en champ qu'on ne peut ni lire ni modifier depuis l'extérieur de la classe.

② Ajoute points, qui se contente de lire le solde.

③ Traite les commandes dans l'ordre et affiche le solde à chaque fois.

④ Affecte 999 à l'ancien nom, _points, et affiche « Solde : X points ».

(Si le code s'exécute correctement, une explication s'affiche.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

L'écrire depuis l'extérieur échoue avant toute exécution — SyntaxError

Pour un test, tu veux mettre le solde à 999999 depuis le code de la page. Si tu écris card.#balance = 999999, comme tu l'aurais fait avec _balance, cette ligne ne passe pas. Pire : même les lignes situées au-dessus, qui auraient dû s'afficher avant, n'affichent rien.

Écrire card.#balance hors de la classe lève SyntaxError: Private field '#balance' must be declared in an enclosing class dès le chargement du code, comme une redéclaration de let. Le message signifie « un champ privé doit être déclaré dans la classe qui l'entoure ».

class PrepaidCard {
  #balance = 0;

  charge(amount) { this.#balance += amount; }
  get balance() { return this.#balance; }
}

console.log("Début du test");   // Cette ligne ne s'affiche pas non plus

const card = new PrepaidCard();
card.charge(5000);

// Avec _balance, les lignes s'exécutent dans l'ordre, mais un nom en # bloque le code dès le chargement
card.#balance = 999999;             // SyntaxError: Private field '#balance' must be declared in an enclosing class
console.log(card.balance);
Jusqu'où va une affectation faite depuis l'extérieur
card._balance= 999999Exécute à partirde la 1re ligne« Début du test »s'afficheAffectation OK :solde 999999card.#balance= 999999Syntaxe vérifiéeau chargementUn nom en #hors de la classeErreur trouvéeavant l'exécution
La ligne du haut va jusqu'à l'affectation, et l'affichage précédent apparaît bien. La ligne du bas s'arrête avant même d'exécuter la première ligne.

Si tu obtiens une SyntaxError qui commence par Private field sans que rien ne s'affiche, cherche un nom en # écrit hors de la classe. Réécris toute ligne qui modifie la valeur depuis l'extérieur pour qu'elle appelle une méthode de la classe, comme charge.

Sur la page d'un magasin, une ligne ajoutait des utilisations à un coupon en modifiant directement le champ en #. Remplace-la par des appels à une méthode de la classe. VisitCoupon et coupon sont déjà déclarés.

① Utilise le coupon deux fois et affiche « Utilisations restantes : X ».

② Ajoute à la classe addUses, qui augmente les utilisations restantes sans jamais dépasser 5.

③ Ajoute 2 utilisations et affiche les utilisations restantes.

④ Ajoute encore 4 utilisations et affiche les utilisations restantes.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Garder les étapes internes hors de portée — les méthodes privées

Supposons que tu extraies de pay le code qui diminue le solde pour en faire une méthode classique, withdraw. Le code extérieur à la classe peut alors lui aussi appeler card.withdraw(5000), sauter la vérification du solde faite dans pay, et faire passer le solde sous zéro.

Si tu en fais une méthode privée (une méthode définie avec un # en tête, qu'on ne peut appeler que depuis les méthodes de la même classe, sous la forme this.#withdraw(price)), l'étape que tu as sortie ne peut plus être appelée depuis l'extérieur. Si tu changes plus tard son nom ou ses paramètres, les modifications restent confinées à la classe.

class PrepaidCard {
  #balance = 3000;

  // Une étape qui se contente de diminuer le solde. pay vérifie d'abord qu'il est suffisant
  #withdraw(amount) { this.#balance -= amount; }

  pay(price) {
    if (price > this.#balance) {
      return `Solde insuffisant : ${price}`;
    }
    this.#withdraw(price);                     // Dans la classe, donc appelable
    return `Solde après paiement : ${this.#balance}`;
  }
}

const card = new PrepaidCard();
console.log(card.pay(480));                    // Solde après paiement : 2520
console.log(card.pay(5000));                   // Solde insuffisant : 5000
console.log(typeof card.withdraw);             // undefined (l'appeler lève une TypeError)
Les appels qui vont jusqu'à #withdraw
#withdraw(amount)diminue le soldecard.pay(480)card.pay(5000)card.withdraw(5000)480 ne dépasse pasle solde de 30005000 dépassele solde de 2520Aucune méthodewithdraw (sans #)Passe par#withdraw : 2520N'atteint pas#withdraw : 2520Lève uneTypeError
Seule la colonne de gauche, qui passe la vérification, atteint l'étape qui diminue le solde. Le nom withdraw, sans #, n'existe pas sur l'instance.

pay(5000) sort par un return dès la vérification du solde et n'atteint donc jamais #withdraw. Écrire card.#withdraw(5000) hors de la classe provoque une SyntaxError dès le chargement du code, comme pour #balance ; la TypeError de la colonne de droite vient de l'appel d'un autre nom, sans #.

Appeler une méthode privée avec this.

Même dans la classe, si tu omets this. et écris #withdraw(price);, tu obtiens SyntaxError: Unexpected identifier '#withdraw' avant toute exécution. Comme pour les champs privés, on l'appelle sous la forme this.#withdraw(price).

Tu calcules le prix actuel d'une location de vélo et le supplément pour une prolongation avec la même formule. BikeRental et rides sont déjà déclarés.

① Ajoute calcFee, qui calcule le prix, sous forme de méthode qu'on ne peut pas appeler depuis l'extérieur.

② En utilisant ①, ajoute une propriété fee qui renvoie le prix actuel et une méthode extraFee qui renvoie le supplément pour une prolongation.

③ Affiche « X min : X yens (+X yens pour 10 min de plus) » pour chaque location.

④ Lis calcFee sur la première location depuis l'extérieur et affiche son type.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Ce qui reste dans le JSON — la différence entre _ et #

Tu remplaces _balance par #balance dans une classe de carte prépayée. Les affectations depuis l'extérieur sont désormais bloquées, mais si tu enregistres les instances sous forme de chaînes avec JSON.stringify, le solde n'apparaît plus dans les données enregistrées.

Un nom qui commence par _ reste une propriété ordinaire : il apparaît donc dans JSON.stringify. Les champs en # ne sont pas des clés de propriété : ils sont exclus du résultat. Un getter est défini sur la classe, pas sur l'instance : ce n'est pas non plus une clé, et balance n'apparaît pas.

// Avant le changement : une propriété ordinaire préfixée par _
class LegacyPrepaidCard {
  constructor() { this._balance = 3000; }
}

// Après le changement : un champ privé en # et un getter pour le lire
class PrepaidCard {
  #balance = 3000;
  get balance() { return this.#balance; }
}

const legacyCard = new LegacyPrepaidCard();
const prepaidCard = new PrepaidCard();

console.log(JSON.stringify(legacyCard));                         // {"_balance":3000}
console.log(JSON.stringify(prepaidCard));                        // {}
console.log(JSON.stringify({ balance: prepaidCard.balance }));   // {"balance":3000}
Ce que lit JSON.stringify
prepaidCard (une instance avec #balance)
Clés de propriété
  • Aucune clé (get balance() est une méthode écrite dans la classe)
  • JSON.stringify(prepaidCard) donne {}
Champs privés
  • #balance — 3000
  • Seules les lignes de PrepaidCard peuvent le lire
#balance appartient bien à l'instance, mais ce n'est pas une clé de propriété. JSON.stringify ne lit que les clés de propriété.

Même si tu obtiens {}, le solde n'a pas disparu : prepaidCard.balance lit toujours 3000. Pour l'enregistrer, passe un objet construit à partir de la valeur du getter, comme dans la dernière ligne de l'exemple. Le tableau ci-dessous compare les trois façons de faire vues dans cet article.

Façon de faireAccès depuis l'extérieurRésultat de JSON.stringify
Nom en _ (_balance)Lecture et écriture, sans vérificationInclus : {"_balance":3000}
Champ privé (#balance)SyntaxError au chargementExclu ({})
Méthode privée (#withdraw)#withdraw() : SyntaxError / withdraw() : TypeErrorExclue, c'est une méthode
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Que se passe-t-il si tu exécutes un code qui contient card.#balance = 0; hors de la classe, après un console.log ?

Question 2Sur un card qui possède #withdraw, que se passe-t-il si tu appelles card.withdraw(500) depuis l'extérieur ?

Question 3Que donne JSON.stringify pour un card qui a #balance = 3000 et seulement un getter ?