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 ?
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
#balance = 0— un nom utilisable seulement dans ce corps
- Si
amount > 20000,throwarrête tout - Seuls les montants acceptés arrivent à
this.#balance += amount
return this.#balance— renvoie la valeur sans jamais la modifier
card.charge(5000)— modifie le solde via chargecard.balance— lit 8000 via le getter- Le nom
#balancene peut pas être écrit ici
#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 #.
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);
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.
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)
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).
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}
- Aucune clé (
get balance()est une méthode écrite dans la classe) JSON.stringify(prepaidCard)donne{}
#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 faire | Accès depuis l'extérieur | Résultat de JSON.stringify |
|---|---|---|
| Nom en _ (_balance) | Lecture et écriture, sans vérification | Inclus : {"_balance":3000} |
| Champ privé (#balance) | SyntaxError au chargement | Exclu ({}) |
| Méthode privée (#withdraw) | #withdraw() : SyntaxError / withdraw() : TypeError | Exclue, c'est une méthode |
Vérification des connaissances
Répondez à chaque question une par une.
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 ?