Las clases de JavaScript han evolucionado mucho desde su introducción en ES6. Las propuestas que llegaron con ES2022 campos privados, métodos privados y bloques de inicialización estática convierten el sistema de clases en una herramienta madura para encapsular lógica compleja sin recurrir a convenciones frágiles como el prefijo _.
Campos privados con #
Los campos privados se declaran con el prefijo # y son inaccesibles desde fuera de la clase, incluso en subclases. A diferencia de los campos con underscore, el motor JavaScript los protege realmente a nivel de lenguaje.
class CuentaBancaria {
#saldo = 0;
#titular;
constructor(titular, saldoInicial = 0) {
this.#titular = titular;
this.#saldo = saldoInicial;
}
depositar(cantidad) {
if (cantidad <= 0) throw new Error('La cantidad debe ser positiva');
this.#saldo += cantidad;
return this;
}
retirar(cantidad) {
if (cantidad > this.#saldo) throw new Error('Saldo insuficiente');
this.#saldo -= cantidad;
return this;
}
get saldo() {
return this.#saldo;
}
}
const cuenta = new CuentaBancaria('Ana', 100);
cuenta.depositar(50).retirar(30);
console.log(cuenta.saldo); // 120
// Esto lanza un SyntaxError, no solo falla silenciosamente:
// console.log(cuenta.#saldo);
Métodos privados
Los métodos privados también usan el prefijo # y permiten extraer lógica interna sin exponerla en la interfaz pública de la clase:
class ValidadorFormulario {
#reglas = [];
#esEmail(valor) {
return /^[^s@]+@[^s@]+.[^s@]+$/.test(valor);
}
#esLongitudValida(valor, min, max) {
return valor.length >= min && valor.length <= max;
}
validar(datos) {
const errores = [];
if (!this.#esEmail(datos.email)) {
errores.push('Email no válido');
}
if (!this.#esLongitudValida(datos.password, 8, 64)) {
errores.push('La contraseña debe tener entre 8 y 64 caracteres');
}
return { valido: errores.length === 0, errores };
}
}
const v = new ValidadorFormulario();
console.log(v.validar({ email: '[email protected]', password: 'secreto123' }));
// { valido: true, errores: [] }
Campos y métodos estáticos privados
Los campos estáticos privados son útiles para implementar patrones como Singleton o llevar contadores internos que no deben quedar expuestos:
class IdGenerador {
static #contador = 0;
static #incrementar() {
return ++IdGenerador.#contador;
}
static siguiente() {
return `ID-${IdGenerador.#incrementar().toString().padStart(6, '0')}`;
}
static total() {
return IdGenerador.#contador;
}
}
console.log(IdGenerador.siguiente()); // "ID-000001"
console.log(IdGenerador.siguiente()); // "ID-000002"
console.log(IdGenerador.total()); // 2
Static initialization blocks
Los bloques de inicialización estática permiten ejecutar lógica compleja cuando la clase se define, sin necesidad de IIFE externos ni métodos auxiliares. Es el lugar correcto para inicializar campos estáticos que dependen de otros campos estáticos o de cálculos:
class Configuracion {
static #valores;
static #entorno;
static {
Configuracion.#entorno = process.env.NODE_ENV ?? 'development';
Configuracion.#valores = {
apiUrl: Configuracion.#entorno === 'production'
? 'https://api.produccion.com'
: 'http://localhost:3000',
timeout: Configuracion.#entorno === 'production' ? 5000 : 30000,
debug: Configuracion.#entorno !== 'production',
};
}
static get(clave) {
return Configuracion.#valores[clave];
}
static get entorno() {
return Configuracion.#entorno;
}
}
console.log(Configuracion.get('apiUrl'));
console.log(Configuracion.entorno);
Getters y setters con validación
Los accessors (get / set) combinados con campos privados permiten validar datos al asignarlos y controlar exactamente qué se expone al exterior:
class Temperatura {
#celsius;
constructor(celsius) {
this.celsius = celsius; // Usa el setter
}
set celsius(valor) {
if (typeof valor !== 'number') throw new TypeError('Debe ser un número');
if (valor < -273.15) throw new RangeError('Por debajo del cero absoluto');
this.#celsius = valor;
}
get celsius() { return this.#celsius; }
get fahrenheit() { return this.#celsius * 9/5 + 32; }
get kelvin() { return this.#celsius + 273.15; }
toString() {
return `${this.#celsius}°C / ${this.fahrenheit}°F / ${this.kelvin}K`;
}
}
const t = new Temperatura(100);
console.log(t.toString()); // "100°C / 212°F / 373.15K"
t.celsius = -10;
console.log(t.fahrenheit); // 14
Herencia con campos privados
Un punto importante: los campos privados no son accesibles desde subclases. Esto es intencional; la subclase debe usar la API pública o protegida que exponga la clase padre:
class Animal {
#nombre;
#energia = 100;
constructor(nombre) {
this.#nombre = nombre;
}
comer(cantidad) {
this.#energia = Math.min(100, this.#energia + cantidad);
}
// Método protegido convencionalmente (no privado)
get nombre() { return this.#nombre; }
get energia() { return this.#energia; }
}
class Perro extends Animal {
#raza;
constructor(nombre, raza) {
super(nombre);
this.#raza = raza;
}
info() {
// Accede a través de getters públicos del padre, no a #nombre directamente
return `${this.nombre} (${this.#raza}) - Energía: ${this.energia}%`;
}
}
const perro = new Perro('Rex', 'Labrador');
perro.comer(20);
console.log(perro.info()); // "Rex (Labrador) - Energía: 100%"
Error habitual: hasOwnProperty vs campos privados
Un error común al empezar con campos privados es intentar comprobar si un campo privado existe usando in. Esto en realidad sí funciona en ES2022 y se llama ergonomic brand checks:
class ClavePrivada {
#secreto = 42;
static esMiInstancia(obj) {
// Forma correcta de comprobar si un objeto tiene el campo privado
return #secreto in obj;
}
}
console.log(ClasePrivada.esMiInstancia(new ClavePrivada())); // true
console.log(ClasePrivada.esMiInstancia({})); // false
Los campos y métodos privados son ya una característica madura de JavaScript con soporte en todos los navegadores modernos y Node.js desde la versión 12. Su uso sistemático hace las clases más predecibles, facilita el refactoring interno sin romper la API pública y elimina convenciones ad hoc como los prefijos _ o __.
