Pregunta 1¿Qué pasa si ejecutas un código que tiene card.#balance = 0; fuera de la clase, después de un console.log?
Campos privados (#) y encapsulamiento
Los campos y métodos privados llevan un # inicial que impide leerlos y escribirlos desde fuera de la clase. El SyntaxError antes de ejecutar y la diferencia con la convención _.
Guardar el saldo de una tarjeta prepago en _balance no lo protege: si en cualquier parte del código de la página se escribe card._balance = 999999, ese valor entra tal cual. El _ al principio es solo una convención que significa «no lo toques desde afuera»; no impide asignaciones ni llamadas.
Este artículo trata los campos privados y los métodos privados, que no se pueden leer ni escribir desde fuera de la clase.
Ocultar el saldo dentro de la clase — campos privados
Quieres que el saldo cambie solo a través de charge, que aplica un límite de 20000 yenes por recarga. Con _balance, si el código de una pantalla de reembolsos escribe card._balance += 30000, nada lo impide, y un monto por encima del límite entra en el saldo.
Un campo privado (un campo de una clase cuyo nombre empieza con # y que solo se puede leer y escribir dentro del cuerpo de esa clase) se declara como #balance = 0; y se usa como this.#balance.
El # es parte del nombre, así que this.balance y this.#balance son nombres distintos.
class PrepaidCard {
#balance = 0; // Saldo. No se puede leer ni escribir fuera de la clase
charge(amount) {
if (amount > 20000) {
throw new Error(`Cada recarga tiene un límite de 20000 yenes: ${amount}`);
}
this.#balance += amount; // Dentro de la clase, así que se puede cambiar
}
get balance() { return this.#balance; } // Hacia afuera solo se expone un getter para leer
}
const card = new PrepaidCard();
card.charge(5000); // charge es la única forma de cambiar el saldo
card.charge(3000);
console.log(card.balance); // 8000
#balance = 0— un nombre que solo se puede usar dentro de este cuerpo
- Si
amount > 20000, hacethrow - Solo los montos que pasan hacen
this.#balance += amount
return this.#balance— solo devuelve el valor, nunca lo cambia
card.charge(5000)— cambia el saldo a través de chargecard.balance— lee 8000 a través del getter- Aquí no se puede escribir el nombre
#balance
#balance dentro del cuerpo de la clase. Para el código de afuera, charge es la única forma de cambiar el saldo.Gracias al encapsulamiento (un diseño que agrupa en una clase los datos y el código que los cambia, y limita el uso desde afuera a puntos de entrada definidos), la única forma de cambiar el saldo de este PrepaidCard es charge, y la única forma de leerlo es el getter balance.
Usar un nombre con # sin declararlo es un SyntaxError
Declara el nombre con # en el cuerpo de la clase, como #balance = 0; o #balance;, antes de usarlo. Si asignas this.#balance = 0; en el constructor sin haberlo declarado, obtienes un SyntaxError, aunque la línea esté dentro de la clase. Asignar a un nombre con # no lo crea.
Escribirlo desde afuera falla antes de ejecutar nada — SyntaxError
Para hacer una prueba, quieres poner el saldo en 999999 desde el código de la página. Si escribes card.#balance = 999999, como lo harías con _balance, esa línea no pasa. Y además dejan de mostrarse incluso las líneas de más arriba, que deberían mostrarse antes.
Escribir card.#balance fuera de la clase produce SyntaxError: Private field '#balance' must be declared in an enclosing class mientras se carga el código, igual que al volver a declarar un let. Significa «un campo privado debe declararse en la clase que lo contiene».
class PrepaidCard {
#balance = 0;
charge(amount) { this.#balance += amount; }
get balance() { return this.#balance; }
}
console.log("Iniciando la prueba"); // Esta línea tampoco se muestra
const card = new PrepaidCard();
card.charge(5000);
// Con _balance, las líneas se ejecutan desde arriba, pero un nombre con # detiene el código mientras se carga
card.#balance = 999999; // SyntaxError: Private field '#balance' must be declared in an enclosing class
console.log(card.balance);
Si obtienes un SyntaxError que empieza con Private field y no se muestra nada, busca un nombre con # escrito fuera de la clase. Reescribe cualquier línea que cambie el valor desde afuera para que llame a un método de la clase, como charge.
Dejar los pasos internos fuera de alcance — métodos privados
Supón que sacas de pay el código que descuenta el saldo y lo pasas a un método normal llamado withdraw. Ahora el código de fuera de la clase también puede llamar a card.withdraw(5000), saltarse la comprobación de saldo de pay y dejar el saldo en negativo.
Si lo conviertes en un método privado (un método definido con # al principio, que solo se puede llamar desde métodos de la misma clase, con la forma this.#withdraw(price)), el paso que separaste no se puede llamar desde afuera. Si más adelante cambias su nombre o sus parámetros, los cambios quedan dentro de la clase.
class PrepaidCard {
#balance = 3000;
// Un paso que solo descuenta el saldo. pay comprueba antes si alcanza
#withdraw(amount) { this.#balance -= amount; }
pay(price) {
if (price > this.#balance) {
return `Saldo insuficiente: ${price}`;
}
this.#withdraw(price); // Dentro de la clase, así que se puede llamar
return `Saldo después del pago: ${this.#balance}`;
}
}
const card = new PrepaidCard();
console.log(card.pay(480)); // Saldo después del pago: 2520
console.log(card.pay(5000)); // Saldo insuficiente: 5000
console.log(typeof card.withdraw); // undefined (llamarlo lanza un TypeError)
pay(5000) hace return en la comprobación del saldo, así que nunca llega a #withdraw. Escribir card.#withdraw(5000) fuera de la clase es un SyntaxError mientras se carga el código, igual que con #balance; el TypeError de la columna derecha viene de llamar a otro nombre, uno sin #.
Los métodos privados se llaman con this.
Incluso dentro de la clase, si omites this. y escribes #withdraw(price);, obtienes SyntaxError: Unexpected identifier '#withdraw' antes de que se ejecute nada. Igual que los campos privados, los métodos privados siempre se usan con la forma this.#withdraw(price).
Qué queda en el JSON — _ frente a #
Ahora imagina que cambias una clase de tarjeta prepago de _balance a #balance. Eso bloquea las asignaciones desde afuera, pero si guardabas las instancias como cadenas con JSON.stringify, el saldo desaparece de los datos que guardes después del cambio.
Un nombre con _ al principio es una propiedad normal que simplemente empieza con _, así que aparece en JSON.stringify. Los campos con # no son claves de propiedad, así que quedan fuera del resultado. Un getter se define en la clase, no en la instancia, así que tampoco es una clave, y balance no aparece.
// Antes del cambio: una propiedad normal con _ al principio
class LegacyPrepaidCard {
constructor() { this._balance = 3000; }
}
// Después del cambio: un campo privado con # y un getter para leerlo
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}
- Ninguna clave (
get balance()es un método escrito en la clase) JSON.stringify(prepaidCard)da{}
#balance— 3000- Solo las líneas dentro de PrepaidCard pueden leerlo
#balance pertenece a la instancia, pero no es una clave de propiedad. JSON.stringify solo lee las claves de propiedad.Aunque obtengas {}, el saldo no desapareció: prepaidCard.balance sigue leyendo 3000. Para guardarlo, pasa un objeto armado con el valor del getter, como en la última línea del ejemplo. La tabla de abajo compara las tres formas que usamos en este artículo.
| Forma | Acceso desde afuera | Con JSON.stringify |
|---|---|---|
| Nombre con _ (_balance) | Lectura y escritura sin controles | Aparece: {"_balance":3000} |
| Campo privado (#balance) | SyntaxError al cargar | No aparece ({}) |
| Método privado (#withdraw) | #withdraw(): SyntaxError / withdraw(): TypeError | No aparece (es un método) |
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2En un card que tiene #withdraw, ¿qué pasa si llamas a card.withdraw(500) desde afuera?
Pregunta 3¿Qué devuelve JSON.stringify para un card con #balance = 3000 y solo un getter?