Programación Orientada a Objetos en JS
Aprende a usar clases con constructores, getters/setters y métodos. Aunque las clases no son obligatorias en JavaScript, son la forma recomendada de organizar código orientado a objetos.
1. ¿Qué son las Clases?
Las clases en JavaScript son "azúcar sintáctica" sobre el sistema de prototipos. Fueron introducidas en ES6 para hacer la programación orientada a objetos más familiar, especialmente para desarrolladores de Java.
¿Por qué usar clases?
- Código más organizado y reutilizable
- Sintaxis más clara y similar a otros lenguajes
- Encapsulación de datos y comportamiento
- Facilita la herencia y el polimorfismo
Sintaxis Básica: Comparación con Java
Java
1public class Persona {2 private String nombre;3 private int edad;4 5 public Persona(String nombre, int edad) {6 this.nombre = nombre;7 this.edad = edad;8 }9 10 public String getNombre() {11 return this.nombre;12 }13 14 public void setNombre(String nombre) {15 this.nombre = nombre;16 }17}JavaScript
1class Persona {2 constructor(nombre, edad) {3 this.nombre = nombre;4 this.edad = edad;5 }6 7 // Getter8 get nombre() {9 return this._nombre;10 }11 12 // Setter13 set nombre(valor) {14 this._nombre = valor;15 }16}2. Constructor
El constructor es un método especial que se ejecuta automáticamente cuando creas una instancia de la clase con new.
Diferencia con Java: Solo puedes tener UN constructor. No hay sobrecarga de constructores.
1class Coche {2 constructor(marca, modelo, año) {3 this.marca = marca;4 this.modelo = modelo;5 this.año = año;6 this.kilometraje = 0; // Valor por defecto7 }8}9 10// Crear instancias11const miCoche = new Coche("Toyota", "Corolla", 2023);12console.log(miCoche.marca); // "Toyota"13console.log(miCoche.kilometraje); // 0Parámetros opcionales
Puedes usar parámetros por defecto como en funciones normales:
constructor(marca = "Genérica", modelo = "Estándar")
3. Getters y Setters
Los getters y setters permiten controlar cómo se accede y modifica una propiedad. Son especialmente útiles para:
- Validación: Asegurar que los datos sean correctos
- Propiedades computadas: Calcular valores dinámicamente
- Encapsulación: Ocultar la implementación interna
1class Persona {2 constructor(nombre, edad) {3 this._nombre = nombre;4 this._edad = edad;5 }6 7 // Getter - se usa como propiedad, no como método8 get nombre() {9 return this._nombre.toUpperCase();10 }11 12 // Setter con validación13 set edad(valor) {14 if (valor < 0) {15 console.log("❌ La edad no puede ser negativa");16 return;17 }18 if (valor > 150) {19 console.log("❌ La edad no es realista");20 return;21 }22 this._edad = valor;23 }24 25 get edad() {26 return this._edad;27 }28 29 // Propiedad computada (solo getter)30 get mayorDeEdad() {31 return this._edad >= 18;32 }33}34 35const juan = new Persona("Juan", 25);36 37// Usar getters (SIN paréntesis)38console.log(juan.nombre); // "JUAN"39console.log(juan.mayorDeEdad); // true40 41// Usar setters (como asignación)42juan.edad = 30; // ✅ Válido43juan.edad = -5; // ❌ Error: edad negativaConvención con guión bajo
Usamos _nombre como convención para indicar que es una propiedad "privada". El guión bajo es solo una convención, no hace la propiedad privada realmente.
4. Métodos de Instancia
Los métodos son funciones definidas dentro de una clase que operan sobre los datos de la instancia.
1class CuentaBancaria {2 constructor(titular, saldoInicial = 0) {3 this.titular = titular;4 this._saldo = saldoInicial;5 }6 7 // Métodos8 depositar(cantidad) {9 if (cantidad <= 0) {10 console.log("❌ La cantidad debe ser positiva");11 return false;12 }13 this._saldo += cantidad;14 console.log(`✅ Depósito: $${cantidad}. Nuevo saldo: $${this._saldo}`);15 return true;16 }17 18 retirar(cantidad) {19 if (cantidad > this._saldo) {20 console.log("❌ Saldo insuficiente");21 return false;22 }23 this._saldo -= cantidad;24 console.log(`✅ Retiro: $${cantidad}. Nuevo saldo: $${this._saldo}`);25 return true;26 }27 28 get saldo() {29 return this._saldo;30 }31 32 mostrarInfo() {33 console.log(`Titular: ${this.titular}, Saldo: $${this._saldo}`);34 }35}36 37const cuenta = new CuentaBancaria("Ana García", 1000);38cuenta.depositar(500); // ✅ Depósito: $500. Nuevo saldo: $150039cuenta.retirar(200); // ✅ Retiro: $200. Nuevo saldo: $130040cuenta.mostrarInfo(); // Titular: Ana García, Saldo: $13005. El Contexto de 'this'
En JavaScript, this NO funciona como en Java. El valor de this depende de cómo se llama la función, no de dónde se define.
Problema común: Perder el contexto de this en callbacks y event listeners.
✅ Cuándo mantiene el contexto
1class Contador {2 constructor() {3 this.count = 0;4 }5 6 incrementar() {7 this.count++;8 console.log(`Count: ${this.count}`);9 }10 11 mostrarValor() {12 console.log(`Valor actual: ${this.count}`);13 }14}15 16const c = new Contador();17 18// ✅ Llamada directa - this funciona correctamente19c.incrementar(); // Count: 120c.mostrarValor(); // Valor actual: 121 22// ✅ Múltiples llamadas - this siempre apunta a 'c'23c.incrementar(); // Count: 224c.incrementar(); // Count: 3❌ Cuándo pierde el contexto
1class Temporizador {2 constructor(nombre) {3 this.nombre = nombre;4 }5 6 saludar() {7 console.log(`¡Hola! Soy ${this.nombre}`);8 }9}10 11const temp = new Temporizador("Ana");12 13// ✅ Llamada directa funciona14temp.saludar(); // "¡Hola! Soy Ana"15 16// ❌ Pasar el método como callback - PIERDE el contexto17setTimeout(temp.saludar, 1000);18// ❌ ERROR: Cannot read property 'nombre' of undefined19// Razón: this es undefined, no apunta a 'temp'20 21// ❌ Asignar método a variable - PIERDE el contexto22const funcionSaludo = temp.saludar;23funcionSaludo();24// ❌ ERROR: Cannot read property 'nombre' of undefined¿Por qué pierde el contexto?
Cuando pasas un método como callback, JavaScript solo pasa la función, no el objeto. Por eso this se vuelve undefined (o el objeto global en modo no estricto).
🔧 Cómo recuperar el contexto
Solución 1: Arrow Function en el callback
1class Temporizador {2 constructor(nombre) {3 this.nombre = nombre;4 }5 6 saludar() {7 console.log(`¡Hola! Soy ${this.nombre}`);8 }9}10 11const temp = new Temporizador("Ana");12 13// ✅ Arrow function mantiene el contexto14setTimeout(() => temp.saludar(), 1000);15// ✅ Funciona después de 1 segundo: "¡Hola! Soy Ana"16 17// ✅ También puedes usarlo múltiples veces18setTimeout(() => temp.saludar(), 2000);19setTimeout(() => temp.saludar(), 3000);Solución 2: Método bind()
1class Temporizador {2 constructor(nombre) {3 this.nombre = nombre;4 }5 6 saludar() {7 console.log(`¡Hola! Soy ${this.nombre}`);8 }9}10 11const temp = new Temporizador("Ana");12 13// ✅ bind() crea una nueva función con 'this' fijado a 'temp'14setTimeout(temp.saludar.bind(temp), 1000);15// ✅ Funciona: "¡Hola! Soy Ana"16 17// Puedes guardar la función con bind18const saludoFijo = temp.saludar.bind(temp);19setTimeout(saludoFijo, 2000); // ✅ También funciona bind() crea una nueva función donde this siempre apunta al objeto que especifiques.
Solución 3: Arrow Function como propiedad de clase
1class Temporizador {2 constructor(nombre) {3 this.nombre = nombre;4 }5 6 // Arrow function como propiedad (NO como método)7 saludar = () => {8 console.log(`¡Hola! Soy ${this.nombre}`);9 }10}11 12const temp = new Temporizador("Ana");13 14// ✅ Ahora puedes pasarlo directamente sin problemas15setTimeout(temp.saludar, 1000);16// ✅ Funciona: "¡Hola! Soy Ana"17 18// Las arrow functions capturan 'this' del constructor19const funcionSaludo = temp.saludar;20funcionSaludo(); // ✅ También funciona directamente
Esta es la solución más moderna. Las arrow functions NO tienen su propio this, heredan el del contexto donde se crearon (el constructor).
Solución 4: Variable 'self' (antigua, pero válida)
1class Temporizador {2 constructor(nombre) {3 this.nombre = nombre;4 5 // Guardar referencia a 'this'6 const self = this;7 8 this.saludar = function() {9 console.log(`¡Hola! Soy ${self.nombre}`);10 };11 }12}13 14const temp = new Temporizador("Ana");15 16// ✅ Funciona porque 'self' mantiene la referencia17setTimeout(temp.saludar, 1000);18// ✅ "¡Hola! Soy Ana"Este patrón era común antes de ES6. Hoy es mejor usar arrow functions, pero aún lo verás en código legacy.
📊 Comparación con Java
Java
- ✅
thissiempre apunta al objeto - ✅ No pierdes el contexto en callbacks
- ✅ Comportamiento predecible
- ❌ Menos flexible
JavaScript
- ⚠️
thisdepende de cómo se llama - ❌ Pierdes el contexto en callbacks
- ❌ Debes usar bind() o arrow functions
- ✅ Más flexible para programación funcional
Recomendación
Para evitar problemas con this, la mejor práctica es usar arrow functions como propiedades de clase (Solución 3) cuando definas métodos que se usarán como callbacks.
6. Métodos Estáticos
Los métodos estáticos pertenecen a la clase, no a las instancias. Se llaman directamente sobre la clase.
Similar a Java: Se usa la palabra clave static.
1class Matematicas {2 // Método estático3 static sumar(a, b) {4 return a + b;5 }6 7 static factorial(n) {8 if (n <= 1) return 1;9 return n * this.factorial(n - 1);10 }11 12 // Constante estática13 static PI = 3.14159;14}15 16// Usar métodos estáticos (sobre la clase, NO sobre instancias)17console.log(Matematicas.sumar(5, 3)); // 818console.log(Matematicas.factorial(5)); // 12019console.log(Matematicas.PI); // 3.1415920 21// ❌ ERROR: No puedes llamar métodos estáticos desde instancias22const mat = new Matematicas();23// mat.sumar(2, 3); // ❌ Esto dará error¿Cuándo usar métodos estáticos?
- Utilidades que no dependen de datos de instancia
- Métodos factory (para crear instancias)
- Constantes compartidas
7. Herencia
La herencia permite crear clases basadas en otras clases. Usamos extends para heredar y super para llamar al constructor de la clase padre.
Similar a Java: Mismas palabras clave, misma sintaxis.
1// Clase padre2class Animal {3 constructor(nombre) {4 this.nombre = nombre;5 }6 7 hacerSonido() {8 console.log(`${this.nombre} hace un sonido`);9 }10}11 12// Clase hija13class Perro extends Animal {14 constructor(nombre, raza) {15 super(nombre); // Llama al constructor del padre16 this.raza = raza;17 }18 19 // Sobrescribir método20 hacerSonido() {21 console.log(`${this.nombre} ladra: ¡Guau guau!`);22 }23 24 // Método propio25 mostrarRaza() {26 console.log(`${this.nombre} es un ${this.raza}`);27 }28}29 30const miPerro = new Perro("Rex", "Labrador");31miPerro.hacerSonido(); // Rex ladra: ¡Guau guau!32miPerro.mostrarRaza(); // Rex es un LabradorPuedes
- Heredar de una clase
- Sobrescribir métodos
- Añadir nuevos métodos
- Usar
superpara acceder al padre
No puedes
- Herencia múltiple (solo una clase padre)
- Modificadores de acceso reales (public, private)
- Sobrecarga de métodos
Organizar Clases en Archivos Separados
Al igual que en Java, en JavaScript puedes organizar tus clases en archivos separados usando export e import.
Esto hace tu código más organizado, reutilizable y fácil de mantener.
Java
1// Animal.java2package animales;3 4public class Animal {5 protected String nombre;6 7 public Animal(String nombre) {8 this.nombre = nombre;9 }10}11 12// Perro.java13package animales;14import animales.Animal;15 16public class Perro extends Animal {17 public Perro(String nombre) {18 super(nombre);19 }20}JavaScript
1// Animal.js2export class Animal {3 constructor(nombre) {4 this.nombre = nombre;5 }6}7 8// Perro.js9import { Animal } from './Animal.js';10 11export class Perro extends Animal {12 constructor(nombre) {13 super(nombre);14 }15}Ejemplo Completo
📄 Animal.js
1// Exportar la clase Animal2export class Animal {3 constructor(nombre) {4 this.nombre = nombre;5 }6 7 hacerSonido() {8 console.log(`${this.nombre} hace un sonido`);9 }10}11 12// También puedes exportar por defecto13// export default Animal;📄 Perro.js
1// Importar Animal desde Animal.js2import { Animal } from './Animal.js';3 4export class Perro extends Animal {5 constructor(nombre, raza) {6 super(nombre);7 this.raza = raza;8 }9 10 hacerSonido() {11 console.log(`${this.nombre} ladra: ¡Guau guau!`);12 }13}📄 main.js
1// Importar las clases que necesitamos2import { Animal } from './Animal.js';3import { Perro } from './Perro.js';4 5// Usar las clases6const miAnimal = new Animal("Genérico");7miAnimal.hacerSonido(); // "Genérico hace un sonido"8 9const miPerro = new Perro("Rex", "Labrador");10miPerro.hacerSonido(); // "Rex ladra: ¡Guau guau!"Named Exports (Recomendado)
Puedes exportar múltiples clases desde un archivo:
1// animales.js2export class Perro { }3export class Gato { }4export class Pajaro { }5 6// main.js7import { Perro, Gato } from './animales.js';Default Export
Un solo export principal por archivo:
1// Animal.js2export default class Animal { }3 4// main.js5import Animal from './Animal.js';6// Puedes usar cualquier nombre7import MiAnimal from './Animal.js';Importante: Extensión .js
En JavaScript, debes incluir la extensión .js en los imports. En Java no se incluye la extensión .java.
8. Diferencias Clave con Java
Campos Privados con #
JavaScript no tenía campos privados reales hasta ES2022. Ahora puedes usar # para crear campos verdaderamente privados:
1class Usuario {2 #contraseña; // Campo privado (con #)3 4 constructor(nombre, contraseña) {5 this.nombre = nombre; // Público6 this.#contraseña = contraseña; // Privado7 }8 9 verificarContraseña(intento) {10 return this.#contraseña === intento;11 }12}13 14const user = new Usuario("Ana", "secreto123");15console.log(user.nombre); // ✅ "Ana"16// console.log(user.#contraseña); // ❌ ERROR: Campo privadoNo hay Sobrecarga de Métodos
En Java puedes tener múltiples métodos con el mismo nombre pero diferentes parámetros. En JavaScript, el último método sobrescribe al anterior:
1class Calculadora {2 // ❌ NO funciona como en Java3 sumar(a, b) {4 return a + b;5 }6 7 // Este sobrescribe al anterior8 sumar(a, b, c) {9 return a + b + c;10 }11}12 13const calc = new Calculadora();14// calc.sumar(2, 3); // ❌ Error: falta el tercer parámetro15 16// ✅ Solución: Usar parámetros opcionales17class CalculadoraMejor {18 sumar(a, b, c = 0) {19 return a + b + c;20 }21}22 23const calc2 = new CalculadoraMejor();24console.log(calc2.sumar(2, 3)); // 525console.log(calc2.sumar(2, 3, 4)); // 99. Ejercicios Prácticos
Ejercicio 1: Clase Producto
Crea una clase Producto con:
- Constructor que reciba nombre y precio
- Getter y setter para precio (validar que sea positivo)
- Método
aplicarDescuento(porcentaje) - Getter
precioConIVAque calcule el precio con 21% IVA
Ver solución
1class Producto {2 constructor(nombre, precio) {3 this.nombre = nombre;4 this._precio = precio;5 }6 7 get precio() {8 return this._precio;9 }10 11 set precio(valor) {12 if (valor < 0) {13 console.log("❌ El precio no puede ser negativo");14 return;15 }16 this._precio = valor;17 }18 19 aplicarDescuento(porcentaje) {20 if (porcentaje < 0 || porcentaje > 100) {21 console.log("❌ Porcentaje inválido");22 return;23 }24 this._precio -= this._precio * (porcentaje / 100);25 }26 27 get precioConIVA() {28 return this._precio * 1.21;29 }30}31 32// Probar33const laptop = new Producto("Laptop", 1000);34console.log(laptop.precio); // 100035laptop.aplicarDescuento(10); // Descuento del 10%36console.log(laptop.precio); // 90037console.log(laptop.precioConIVA); // 1089Ejercicio 2: Herencia de Vehículos
Crea una clase Vehiculo y una clase Moto que herede de ella:
Vehiculo: constructor(marca, modelo), métodoinfo()Moto: añade propiedadcilindrada, sobrescribeinfo()- Crea un método estático en
Vehiculollamadocomparar(v1, v2)
Ver solución
1class Vehiculo {2 constructor(marca, modelo) {3 this.marca = marca;4 this.modelo = modelo;5 }6 7 info() {8 return `${this.marca} ${this.modelo}`;9 }10 11 static comparar(v1, v2) {12 return v1.marca === v2.marca && v1.modelo === v2.modelo;13 }14}15 16class Moto extends Vehiculo {17 constructor(marca, modelo, cilindrada) {18 super(marca, modelo);19 this.cilindrada = cilindrada;20 }21 22 info() {23 return `${super.info()} - ${this.cilindrada}cc`;24 }25}26 27// Probar28const moto1 = new Moto("Yamaha", "R1", 1000);29const moto2 = new Moto("Honda", "CBR", 600);30 31console.log(moto1.info()); // "Yamaha R1 - 1000cc"32console.log(Vehiculo.comparar(moto1, moto2)); // falseEjercicio 3: Clase Wallet con propiedades privadas
Crea una clase Wallet (billetera) con:
- Propiedad privada (convención)
_saldo - Métodos
depositar(cantidad)yretirar(cantidad) - Getter
saldoque devuelva el saldo - El saldo nunca debe poder ser negativo
Ver solución
1class Wallet {2 constructor(saldoInicial = 0) {3 this._saldo = saldoInicial >= 0 ? saldoInicial : 0;4 }5 6 depositar(cantidad) {7 if (cantidad <= 0) {8 console.log("❌ La cantidad debe ser positiva");9 return false;10 }11 this._saldo += cantidad;12 console.log(`✅ Depositado: $${cantidad}`);13 return true;14 }15 16 retirar(cantidad) {17 if (cantidad > this._saldo) {18 console.log("❌ Saldo insuficiente");19 return false;20 }21 this._saldo -= cantidad;22 console.log(`✅ Retirado: $${cantidad}`);23 return true;24 }25 26 get saldo() {27 return this._saldo;28 }29}30 31// Probar32const miWallet = new Wallet(1000);33miWallet.depositar(500); // ✅ Depositado: $50034console.log(miWallet.saldo); // 150035miWallet.retirar(200); // ✅ Retirado: $20036miWallet.retirar(2000); // ❌ Saldo insuficiente37 38// Técnicamente podrías acceder a _saldo, pero por convención no deberías39console.log(miWallet._saldo); // 1300 (funciona, pero es mala práctica)Ejercicio 4: Factory con Métodos Estáticos
Crea una clase Usuario con métodos estáticos factory:
- Método estático
crearAdmin(nombre)que cree un usuario con rol "admin" - Método estático
crearInvitado()que cree un usuario con nombre "Invitado"
Ver solución
1class Usuario {2 constructor(nombre, rol = "usuario") {3 this.nombre = nombre;4 this.rol = rol;5 this.fechaCreacion = new Date();6 }7 8 // Factory methods9 static crearAdmin(nombre) {10 return new Usuario(nombre, "admin");11 }12 13 static crearInvitado() {14 return new Usuario("Invitado", "invitado");15 }16 17 mostrarInfo() {18 console.log(`Usuario: ${this.nombre}, Rol: ${this.rol}`);19 }20}21 22// Probar23const admin = Usuario.crearAdmin("María");24const invitado = Usuario.crearInvitado();25 26admin.mostrarInfo(); // Usuario: María, Rol: admin27invitado.mostrarInfo(); // Usuario: Invitado, Rol: invitado