Aprende leyendo en orden

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
Líneas que llegan a #balance y líneas que no
Cuerpo de class PrepaidCard
  • #balance = 0 — un nombre que solo se puede usar dentro de este cuerpo
Cuerpo de charge(amount)
  • Si amount > 20000, hace throw
  • Solo los montos que pasan hacen this.#balance += amount
Cuerpo de get balance()
  • return this.#balance — solo devuelve el valor, nunca lo cambia
Líneas fuera de la clase
  • card.charge(5000) — cambia el saldo a través de charge
  • card.balance — lee 8000 a través del getter
  • Aquí no se puede escribir el nombre #balance
Solo puedes escribir #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.

Vas a hacer que el saldo de puntos de un socio solo pueda cambiar con compras y canjes. PointAccount y orders ya están declarados.

① Convierte el saldo en un campo que no se pueda leer ni escribir desde fuera de la clase.

② Agrega points, que solo lee el saldo.

③ Procesa los pedidos en orden y muestra el saldo cada vez.

④ Asigna 999 al nombre anterior, _points, y muestra «Saldo: X pt».

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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);
Hasta dónde llega una asignación desde afuera
card._balance= 999999Ejecuta desdela primera línea"Iniciando laprueba" apareceSe asigna:saldo 999999card.#balance= 999999Se revisa lasintaxis al cargarHay un nombre #fuera de la claseEl error apareceantes de ejecutar
La fila de arriba llega hasta la asignación, y la salida anterior sigue apareciendo. La fila de abajo se detiene antes de ejecutar la primera línea.

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.

La página de la tienda sumaba usos restantes a un cupón de visita escribiendo directamente en el nombre con #. Cámbialo por llamadas a un método de la clase. VisitCoupon y coupon ya están declarados.

① Usa el cupón dos veces y muestra «Usos restantes: X».

② Agrega addUses a la clase de forma que los usos restantes nunca pasen de 5.

③ Suma 2 usos y muestra los usos restantes.

④ Suma 4 usos más y muestra los usos restantes.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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)
Qué llamadas llegan hasta #withdraw
#withdraw(amount)solo descuentacard.pay(480)card.pay(5000)card.withdraw(5000)480 no superael saldo de 30005000 superael saldo de 2520No hay un métodowithdraw (sin #)Pasa por#withdraw: 2520No llega a#withdraw: 2520Lanza unTypeError
Solo la columna izquierda, que pasa la comprobación, llega al paso que descuenta el saldo. El nombre withdraw, sin el #, no existe en la instancia.

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).

Vas a calcular la tarifa actual del alquiler de una bicicleta y el cargo extra por extender el tiempo con la misma fórmula. BikeRental y rides ya están declarados.

① Agrega calcFee, que calcula la tarifa, como un método que no se pueda llamar desde afuera.

② Usando ①, agrega una propiedad fee que devuelva la tarifa actual y un método extraFee que devuelva el cargo extra por una extensión.

③ Muestra «X min: X yenes (+X yenes con 10 min más)» para cada alquiler.

④ Lee calcFee del primer alquiler desde afuera y muestra su tipo.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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}
Lo que lee JSON.stringify
prepaidCard (una instancia con #balance)
Claves de propiedad
  • Ninguna clave (get balance() es un método escrito en la clase)
  • JSON.stringify(prepaidCard) da {}
Campos privados
  • #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.

FormaAcceso desde afueraCon JSON.stringify
Nombre con _ (_balance)Lectura y escritura sin controlesAparece: {"_balance":3000}
Campo privado (#balance)SyntaxError al cargarNo aparece ({})
Método privado (#withdraw)#withdraw(): SyntaxError / withdraw(): TypeErrorNo aparece (es un método)
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1¿Qué pasa si ejecutas un código que tiene card.#balance = 0; fuera de la clase, después de un console.log?

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?