GUÍA COMPLETA DE CLASES EN JAVASCRIPT 2025
Introducción a las Clases en JavaScript
Las clases en JavaScript, introducidas en ECMAScript 2015 (ES6), ofrecen una sintaxis clara y estructurada para implementar la programación orientada a objetos. Aunque JavaScript utiliza un modelo basado en prototipos, las clases proporcionan una abstracción que simplifica la creación de objetos, la herencia y la gestión de propiedades y métodos. Este tutorial explora en detalle cómo funcionan las clases, sus características avanzadas y casos prácticos para aplicarlas en proyectos de programación modernos en 2025. A través de ejemplos de código, se ilustrarán conceptos como constructores, herencia, métodos estáticos, getters, setters y encapsulación, todo adaptado a un contexto de desarrollo web y tecnología.
Las clases son una herramienta poderosa para estructurar código, especialmente en aplicaciones complejas como interfaces de usuario dinámicas, APIs o sistemas modulares. En este artículo, se desglosarán los componentes clave de las clases y se presentarán ejemplos que demuestran su utilidad en escenarios reales, desde la gestión de datos hasta la creación de patrones de diseño.
Declaración y Creación de Clases
Una clase en JavaScript se declara usando la palabra clave class, seguida de un nombre que, por convención, comienza con mayúscula. Dentro de la clase, se define un constructor que inicializa las propiedades del objeto cuando se crea una instancia. El constructor es opcional, pero es común incluirlo para establecer valores iniciales.
class Persona {
constructor(nombre, edad) {
this.nombre = nombre;
this.edad = edad;
}
saludar() {
return `Hola, mi nombre es ${this.nombre} y tengo ${this.edad} años.`;
}
}
const juan = new Persona("Juan", 30);
console.log(juan.saludar());
Hola, mi nombre es Juan y tengo 30 años.
En este ejemplo, la clase Persona define un constructor que acepta nombre y edad, y un método saludar que retorna un mensaje. La instancia juan se crea con el operador new, que invoca el constructor y retorna un objeto con las propiedades y métodos definidos. Este enfoque es ideal para modelar entidades en aplicaciones, como usuarios en un sistema de gestión.
Métodos de Instancia
Los métodos de instancia, como saludar en el ejemplo anterior, son funciones asociadas a cada instancia de la clase. Estos métodos tienen acceso a las propiedades de la instancia a través de this y pueden realizar operaciones específicas. Los métodos de instancia son útiles para definir comportamientos que dependen del estado del objeto.
class CuentaBancaria {
constructor(titular, saldo = 0) {
this.titular = titular;
this.saldo = saldo;
}
depositar(cantidad) {
if (cantidad > 0) {
this.saldo += cantidad;
return `Depósito de ${cantidad} realizado. Nuevo saldo: ${this.saldo}`;
}
return "Cantidad inválida";
}
retirar(cantidad) {
if (cantidad > 0 && cantidad <= this.saldo) {
this.saldo -= cantidad;
return `Retiro de ${cantidad} realizado. Nuevo saldo: ${this.saldo}`;
}
return "Fondos insuficientes o cantidad inválida";
}
}
const cuenta = new CuentaBancaria("Ana", 1000);
console.log(cuenta.depositar(500));
console.log(cuenta.retirar(200));
Depósito de 500 realizado. Nuevo saldo: 1500
Retiro de 200 realizado. Nuevo saldo: 1300
En este caso, la clase CuentaBancaria incluye métodos de instancia depositar y retirar, que modifican el saldo y validan las operaciones. Este tipo de estructura es común en aplicaciones financieras o de comercio electrónico, donde se necesita gestionar estados dinámicos.
Métodos Estáticos
Los métodos estáticos se definen con la palabra clave static y se invocan directamente en la clase, no en una instancia. Son útiles para funciones utilitarias que no dependen del estado de un objeto específico. Por ejemplo, un método estático puede realizar cálculos o validaciones relacionadas con la clase.
class Calculadora {
static sumar(a, b) {
return a + b;
}
static esPar(numero) {
return numero % 2 === 0;
}
}
console.log(Calculadora.sumar(5, 3));
console.log(Calculadora.esPar(4));
8
true
Aquí, sumar y esPar son métodos estáticos que no requieren una instancia de Calculadora. Este enfoque es práctico para crear utilidades reutilizables, como validadores o funciones matemáticas, que pueden integrarse en aplicaciones de análisis de datos o herramientas de desarrollo.
Herencia entre Clases
La herencia permite que una clase derive de otra, heredando sus propiedades y métodos. Se utiliza la palabra clave extends para establecer la relación, y el método super invoca el constructor de la clase padre. La herencia es fundamental para reutilizar código y modelar jerarquías en aplicaciones.
class Vehiculo {
constructor(marca, modelo) {
this.marca = marca;
this.modelo = modelo;
}
describir() {
return `${this.marca} ${this.modelo}`;
}
}
class Coche extends Vehiculo {
constructor(marca, modelo, puertas) {
super(marca, modelo);
this.puertas = puertas;
}
describir() {
return `${super.describir()} con ${this.puertas} puertas`;
}
}
const miCoche = new Coche("Toyota", "Corolla", 4);
console.log(miCoche.describir());
Toyota Corolla con 4 puertas
En este ejemplo, la clase Coche hereda de Vehiculo y extiende su funcionalidad. El método describir en Coche sobrescribe el método de la clase padre, pero usa super para incluir la descripción base. Este patrón es útil en sistemas de inventario o aplicaciones de gestión de activos, donde se modelan diferentes tipos de entidades con características compartidas.
Getters y Setters
Los getters y setters permiten controlar el acceso y la modificación de propiedades. Un getter se define con la palabra clave get y retorna un valor calculado o formateado, mientras que un setter, definido con set, valida o transforma los datos antes de asignarlos.
class Estudiante {
constructor(nombre) {
this._nombre = nombre;
}
get nombre() {
return this._nombre.toUpperCase();
}
set nombre(nuevoNombre) {
if (nuevoNombre.length > 0) {
this._nombre = nuevoNombre;
} else {
console.log("El nombre no puede estar vacío");
}
}
}
const estudiante = new Estudiante("Carlos");
console.log(estudiante.nombre);
estudiante.nombre = "María";
console.log(estudiante.nombre);
estudiante.nombre = "";
CARLOS
MARÍA
El nombre no puede estar vacío
En este caso, el getter nombre retorna el nombre en mayúsculas, y el setter valida que el nuevo nombre no esté vacío. Este enfoque es útil para garantizar la integridad de los datos en aplicaciones educativas o de gestión de usuarios, donde las propiedades deben cumplir ciertas reglas.
Propiedades Privadas
A partir de 2022, JavaScript soporta propiedades privadas usando el prefijo #. Estas propiedades solo son accesibles dentro de la clase, lo que permite implementar encapsulación y proteger el estado interno del objeto.
class Producto {
#precio;
constructor(nombre, precio) {
this.nombre = nombre;
this.#precio = precio;
}
obtenerPrecio() {
return this.#precio;
}
establecerPrecio(nuevoPrecio) {
if (nuevoPrecio >= 0) {
this.#precio = nuevoPrecio;
return `Precio actualizado a ${this.#precio}`;
}
return "Precio inválido";
}
}
const producto = new Producto("Laptop", 1200);
console.log(producto.obtenerPrecio());
console.log(producto.establecerPrecio(1500));
console.log(producto.#precio); // Error: Propiedad privada
1200
Precio actualizado a 1500
Uncaught SyntaxError: Private field '#precio' must be declared in an enclosing class
La propiedad #precio solo puede accederse a través de los métodos obtenerPrecio y establecerPrecio, lo que asegura que el precio no se modifique directamente. Este mecanismo es esencial en aplicaciones de comercio electrónico, donde la manipulación incorrecta de datos puede tener consecuencias significativas.
Caso de Uso: Gestión de Tareas
Para ilustrar cómo las clases pueden aplicarse en un proyecto real, consideremos una aplicación de gestión de tareas. Esta aplicación permite crear tareas, marcarlas como completadas y filtrarlas según su estado. Las clases ofrecen una forma estructurada de modelar las tareas y la lógica asociada.
class Tarea {
#id;
#completada;
constructor(titulo, descripcion) {
this.#id = Math.random().toString(36).slice(2);
this.titulo = titulo;
this.descripcion = descripcion;
this.#completada = false;
}
get id() {
return this.#id;
}
get completada() {
return this.#completada;
}
marcarCompletada() {
this.#completada = true;
return `Tarea ${this.titulo} marcada como completada`;
}
}
class GestorTareas {
#tareas;
constructor() {
this.#tareas = [];
}
agregarTarea(titulo, descripcion) {
const tarea = new Tarea(titulo, descripcion);
this.#tareas.push(tarea);
return tarea;
}
obtenerTareas(completadas = null) {
if (completadas === null) {
return this.#tareas;
}
return this.#tareas.filter((tarea) => tarea.completada === completadas);
}
static contarTareas(tareas) {
return tareas.length;
}
}
const gestor = new GestorTareas();
const tarea1 = gestor.agregarTarea(
"Estudiar JavaScript",
"Repasar clases y prototipos"
);
const tarea2 = gestor.agregarTarea(
"Diseñar UI",
"Crear mockups para la aplicación"
);
console.log(tarea1.marcarCompletada());
console.log(GestorTareas.contarTareas(gestor.obtenerTareas()));
console.log(gestor.obtenerTareas(true));
Tarea Estudiar JavaScript marcada como completada
2
[Tarea { id: "...", titulo: "Estudiar JavaScript", descripcion: "Repasar clases y prototipos", #completada: true }]
En este ejemplo, la clase Tarea modela una tarea con un identificador único, título, descripción y estado de completitud. La clase GestorTareas maneja una colección de tareas, permitiendo agregarlas, filtrarlas y contarlas. El método estático contarTareas proporciona una utilidad para obtener el número total de tareas. Este diseño es escalable y puede integrarse en una aplicación web con una interfaz de usuario, utilizando frameworks como React o Vue.js.
Encapsulación y Modularidad
La encapsulación, lograda mediante propiedades privadas y métodos bien definidos, asegura que el estado interno de un objeto no se modifique de forma inesperada. En el ejemplo de gestión de tareas, las propiedades #id y #completada son privadas, lo que previene accesos no autorizados. Esta práctica mejora la modularidad del código, ya que cada clase tiene una responsabilidad clara y su implementación interna está protegida.
Por ejemplo, si quisiéramos extender el gestor de tareas para incluir categorías, podríamos crear una nueva clase Categoria y asociarla a las tareas sin modificar la lógica existente.
class Categoria {
#nombre;
constructor(nombre) {
this.#nombre = nombre;
}
get nombre() {
return this.#nombre;
}
}
class TareaExtendida extends Tarea {
constructor(titulo, descripcion, categoria) {
super(titulo, descripcion);
this.categoria = categoria;
}
describir() {
return `${this.titulo} (${this.categoria.nombre})`;
}
}
const categoria = new Categoria("Trabajo");
const tarea = new TareaExtendida(
"Enviar informe",
"Informe mensual",
categoria
);
console.log(tarea.describir());
Enviar informe (Trabajo)
Este ejemplo muestra cómo la herencia y la encapsulación permiten extender la funcionalidad sin alterar el código original, siguiendo principios de diseño como la responsabilidad única y el abierto/cerrado.
Clases como Base para Patrones de Diseño
Las clases son fundamentales para implementar patrones de diseño en JavaScript, como el patrón Singleton, Factory o Observer. Por ejemplo, el patrón Singleton asegura que solo exista una instancia de una clase, lo cual es útil para gestionar recursos compartidos, como una conexión a una base de datos.
class DatabaseConnection {
#instance;
constructor() {
if (!DatabaseConnection.#instance) {
this.url = "mongodb://localhost:27017";
DatabaseConnection.#instance = this;
}
return DatabaseConnection.#instance;
}
conectar() {
return `Conectado a ${this.url}`;
}
}
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
console.log(db1 === db2);
console.log(db1.conectar());
true
Conectado a mongodb://localhost:27017
En este caso, la clase DatabaseConnection implementa el patrón Singleton, asegurando que todas las instancias sean la misma. Este patrón es común en aplicaciones que requieren un único punto de acceso a recursos externos, como APIs o bases de datos.
Conclusiones
Las clases en JavaScript, introducidas en ES6 y mejoradas en versiones posteriores, ofrecen una forma robusta y legible de implementar programación orientada a objetos. A través de constructores, métodos de instancia, métodos estáticos, herencia, getters, setters y propiedades privadas, las clases permiten estructurar aplicaciones de manera modular y mantenible. Los ejemplos presentados, como la gestión de cuentas bancarias, tareas y conexiones a bases de datos, demuestran cómo las clases pueden aplicarse en escenarios reales, desde aplicaciones web hasta sistemas backend. En 2025, con el auge de frameworks modernos y la necesidad de código escalable, dominar las clases es esencial para cualquier desarrollador. Al combinar clases con patrones de diseño y buenas prácticas de encapsulación, es posible crear soluciones robustas que satisfagan las demandas de proyectos tecnológicos complejos.