El problema del guion bajo
Durante años, JavaScript no tuvo campos privados de verdad. La convención era ponerle un guion bajo al nombre de la propiedad y rezar para que nadie la tocara:
_count parece privado, pero no lo es. Cualquiera puede leerlo, sobrescribirlo o borrarlo. El guion bajo es solo un cartelito educado en la puerta; la puerta, en realidad, está abierta de par en par.
El JavaScript moderno resolvió esto con campos privados de verdad, marcados con #.
El # lo hace privado
Antepón # al nombre del campo tanto en la declaración como en cada punto donde lo uses:
Desde dentro de la clase, this.#count funciona sin problema. Desde fuera, directamente no existe:
El # forma parte del nombre del campo, literal. No es una palabra clave modificadora como private en otros lenguajes: es un sigilo que el parser utiliza para buscar una ranura separada y protegida dentro del objeto. Por eso el error salta en tiempo de parseo, antes incluso de que el código se ejecute.
Métodos privados y getters en JavaScript
Los campos no son lo único que puedes marcar como privado. Los métodos, getters y setters también admiten el prefijo #:
#assertPositive es un método auxiliar interno. No forma parte de la API pública, así que al hacerlo realmente privado te aseguras de que nadie lo llame por accidente desde fuera, y tampoco puedan depender de él. Eso te deja las manos libres para renombrarlo o eliminarlo más adelante.
Campos y métodos privados estáticos
Los miembros estáticos también pueden ser privados. Basta con anteponerles # igual que antes:
Los estáticos privados viven en la propia clase, no en las instancias. Son ideales para contadores, cachés o configuración que no debería filtrarse fuera de la clase.
Las subclases no pueden verlos
Aquí es donde se tropieza mucha gente que viene de Java o C#. Los campos privados de JavaScript son privados por clase, no privados por instancia. Una subclase no puede meter mano a los campos privados de su padre:
En JavaScript no existe protected. Si una subclase necesita acceder a los datos, la clase padre tiene que exponerlos mediante un método, un getter o (menos habitual) un campo no privado. Es una decisión de diseño intencional: lo privado es privado de verdad, y la herencia no abre agujeros en esa barrera.
Comprobar un campo privado con in
A veces te interesa verificar si un objeto pertenece realmente a tu clase — lo que se conoce como brand check. Dentro de la clase, el operador in funciona con los nombres de campos privados:
Como solo Wallet puede crear objetos con #balance, la expresión #balance in obj es una forma fiable de comprobar que obj es realmente una instancia de Wallet. En algunos casos límite resulta más rápida y segura que instanceof, ya que los campos privados no se pueden falsificar desde fuera.
Ojo con esto: los objetos planos no los tienen
Los campos privados viven en las instancias creadas por el constructor de la clase. Si intentas usar uno en un objeto que no se creó con new, se lanza un error:
Llamar al método con un this que no sea una instancia de Point lanza un error en tiempo de ejecución. Ese es el mecanismo que hay detrás del brand check que vimos antes: los campos privados están atados a la clase concreta que los declaró, no a cualquier objeto que casualmente tenga la misma pinta.
Cuándo usar # en campos privados
Usa campos privados por defecto siempre que un trozo de estado o un método auxiliar no forme parte de la API pública de la clase. Las razones:
- Libertad para refactorizar. Quien consume la clase no puede depender de detalles internos que literalmente no puede ver.
- Encapsulación de verdad. Nada de lecturas, escrituras ni borrados accidentales desde fuera.
- Autocompletado más limpio. Los editores no sugieren miembros privados a quien llama desde fuera.
Reserva las propiedades públicas para lo que realmente forma parte de la interfaz. Si quieres exponer un campo privado solo para lectura, usa un getter (get name()). Y olvídate ya de la convención del guion bajo (_field): era un apaño para cubrir un hueco que el propio lenguaje ya ha resuelto.
#celsius es el almacenamiento oculto; celsius y fahrenheit son vistas de solo lectura. Quien use la clase no puede corromper el estado interno, y la clase queda libre de cambiar más adelante cómo guarda ese valor.
Siguiente: prototipos
Las clases son, en gran parte, azúcar sintáctica sobre el sistema de prototipos de JavaScript: el modelo más antiguo y fundamental sobre el que realmente se apoya el lenguaje. Entender los prototipos te aclara por qué this se comporta como lo hace, cómo funciona de verdad la herencia y qué hay detrás de un extends en una clase. De eso trata la siguiente página.
Preguntas frecuentes
¿Cómo se declara un campo privado en JavaScript?
Añade # delante del nombre del campo, tanto al declararlo como cada vez que lo uses. Por ejemplo: class Counter { #count = 0; increment() { this.#count++; } } crea un campo genuinamente privado. Ojo: el # forma parte del nombre, no es un operador.
¿Qué diferencia hay entre #field y _field en JavaScript?
#field y _field en JavaScript?_field es solo una convención de nombres: la propiedad sigue siendo pública y cualquiera puede leerla o modificarla. En cambio, #field lo impone el propio lenguaje: el código fuera de la clase literalmente no puede acceder a él, y si lo intentas lanza un SyntaxError en tiempo de parseo. Si quieres privacidad de verdad, usa #.
¿Las subclases pueden acceder a los campos privados de la clase padre?
No. Los campos privados están limitados a la clase que los declara, y ni siquiera las subclases pueden tocarlos. Si una subclase necesita acceder a ellos, la clase padre tiene que exponer un método o un getter. Es más estricto que el protected de otros lenguajes, y está hecho así a propósito.
¿Se puede comprobar si un objeto tiene un campo privado?
Sí, con el operador in dentro de la clase: #field in obj devuelve true si obj tiene ese campo privado. Resulta muy útil para hacer brand checks, es decir, confirmar que un objeto realmente es una instancia de tu clase antes de llamar a sus métodos.