Programación Orientada a Objetos
Aprende a organizar tu código utilizando el paradigma orientado a objetos en JavaScript, desde prototipos hasta la sintaxis moderna de clases.
Cristian Escalante
Última actualización: 22 de abril de 2025
Introducción a la Programación Orientada a Objetos
La Programación Orientada a Objetos (POO) es un paradigma de programación que organiza el código en torno a objetos en lugar de funciones y lógica. Este enfoque permite:
- Modelar el código basándose en entidades del mundo real
- Reutilizar código mediante herencia
- Encapsular datos y comportamientos
- Crear código más mantenible y escalable
- Implementar abstracciones que simplifican problemas complejos
JavaScript es un lenguaje multiparadigma que soporta programación orientada a objetos, aunque su implementación difiere de lenguajes como Java o C++. Tradicionalmente, JavaScript utilizaba prototipos en lugar de clases, pero desde ES6 (2015) incluye una sintaxis de clases que facilita la POO.
Objetos en JavaScript
Los objetos son colecciones de propiedades (datos) y métodos (funciones) que representan entidades.
Creación de objetos
Notación literal
const persona = {
// Propiedades (datos)
nombre: 'Ana',
edad: 28,
// Métodos (funciones)
saludar() {
return `Hola, soy ${this.nombre} y tengo ${this.edad} años.`;
},
cumplirAnios() {
this.edad++;
return `¡Ahora tengo ${this.edad} años!`;
}
};
console.log(persona.saludar()); // "Hola, soy Ana y tengo 28 años."
console.log(persona.cumplirAnios()); // "¡Ahora tengo 29 años!"
Constructor de objetos
function Persona(nombre, edad) {
this.nombre = nombre;
this.edad = edad;
this.saludar = function() {
return `Hola, soy ${this.nombre} y tengo ${this.edad} años.`;
};
}
const persona1 = new Persona('Carlos', 35);
const persona2 = new Persona('Laura', 27);
console.log(persona1.saludar()); // "Hola, soy Carlos y tengo 35 años."
console.log(persona2.saludar()); // "Hola, soy Laura y tengo 27 años."
Object.create()
const personaPrototipo = {
saludar() {
return `Hola, soy ${this.nombre} y tengo ${this.edad} años.`;
},
cumplirAnios() {
this.edad++;
return `¡Ahora tengo ${this.edad} años!`;
}
};
const persona = Object.create(personaPrototipo);
persona.nombre = 'Miguel';
persona.edad = 40;
console.log(persona.saludar()); // "Hola, soy Miguel y tengo 40 años."
Prototipos en JavaScript
El sistema de herencia de JavaScript se basa en prototipos, no en clases (aunque ahora existe una sintaxis de clases).
Cadena de prototipos
Cada objeto en JavaScript tiene una propiedad interna [[Prototype]]
que apunta a otro objeto o a null
. Cuando se intenta acceder a una propiedad que no existe en un objeto, JavaScript busca en su prototipo, luego en el prototipo del prototipo, y así sucesivamente.
// Crear un constructor
function Animal(nombre) {
this.nombre = nombre;
}
// Añadir métodos al prototipo
Animal.prototype.hacerSonido = function() {
return 'Algún sonido';
};
// Crear un objeto
const perro = new Animal('Rex');
console.log(perro.nombre); // "Rex" (propiedad del objeto)
console.log(perro.hacerSonido()); // "Algún sonido" (método del prototipo)
// Verificar la cadena de prototipos
console.log(perro.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
Herencia prototípica
// Constructor padre
function Animal(nombre) {
this.nombre = nombre;
}
Animal.prototype.hacerSonido = function() {
return 'Algún sonido';
};
// Constructor hijo
function Perro(nombre, raza) {
// Llamar al constructor padre
Animal.call(this, nombre);
this.raza = raza;
}
// Heredar el prototipo
Perro.prototype = Object.create(Animal.prototype);
// Restaurar el constructor
Perro.prototype.constructor = Perro;
// Sobreescribir un método
Perro.prototype.hacerSonido = function() {
return 'Guau guau';
};
// Añadir métodos específicos
Perro.prototype.ladrar = function() {
return `${this.nombre} está ladrando fuerte!`;
};
const miPerro = new Perro('Rex', 'Pastor Alemán');
console.log(miPerro.nombre); // "Rex"
console.log(miPerro.raza); // "Pastor Alemán"
console.log(miPerro.hacerSonido()); // "Guau guau"
console.log(miPerro.ladrar()); // "Rex está ladrando fuerte!"
Clases en ES6+
ES6 introdujo una sintaxis de clases que simplifica la POO en JavaScript, aunque internamente sigue utilizando el sistema de prototipos.
Declaración de clases
class Animal {
// Constructor (método especial)
constructor(nombre) {
this.nombre = nombre;
}
// Métodos
hacerSonido() {
return 'Algún sonido';
}
describir() {
return `Este animal se llama ${this.nombre}`;
}
}
const gato = new Animal('Michi');
console.log(gato.nombre); // "Michi"
console.log(gato.hacerSonido()); // "Algún sonido"
Herencia con extends
class Animal {
constructor(nombre) {
this.nombre = nombre;
}
hacerSonido() {
return 'Algún sonido';
}
}
class Perro extends Animal {
constructor(nombre, raza) {
// Llamar al constructor padre
super(nombre);
this.raza = raza;
}
// Sobreescribir método del padre
hacerSonido() {
return 'Guau guau';
}
// Método específico
ladrar() {
return `${this.nombre} está ladrando!`;
}
}
const miPerro = new Perro('Rex', 'Pastor Alemán');
console.log(miPerro.nombre); // "Rex"
console.log(miPerro.hacerSonido()); // "Guau guau"
console.log(miPerro.ladrar()); // "Rex está ladrando!"
Expresiones de clase
Las clases también pueden definirse como expresiones:
// Clase anónima
const Animal = class {
constructor(nombre) {
this.nombre = nombre;
}
hacerSonido() {
return 'Algún sonido';
}
};
// Clase con nombre
const Vehiculo = class VehiculoClase {
constructor(marca) {
this.marca = marca;
}
};
const coche = new Vehiculo('Toyota');
console.log(coche.marca); // "Toyota"
Características avanzadas de clases
Propiedades y métodos estáticos
Los miembros estáticos pertenecen a la clase en sí, no a las instancias.
class MathUtils {
// Propiedad estática
static PI = 3.14159;
// Método estático
static sumar(a, b) {
return a + b;
}
static calcularAreaCirculo(radio) {
return MathUtils.PI * radio * radio;
}
}
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.sumar(5, 3)); // 8
console.log(MathUtils.calcularAreaCirculo(2)); // 12.56636
// No se puede acceder desde una instancia
const utils = new MathUtils();
// console.log(utils.PI); // undefined
Propiedades de clase
Existen varias formas de definir propiedades en clases:
class Producto {
// Propiedades públicas (ES2022+)
nombre;
precio;
// Propiedad con valor inicial
disponible = true;
// Propiedad privada (ES2022+)
#codigo;
constructor(nombre, precio, codigo) {
this.nombre = nombre;
this.precio = precio;
this.#codigo = codigo;
}
mostrarInfo() {
return `${this.nombre} - $${this.precio} (${this.#codigo})`;
}
// Getter
get precioConIVA() {
return this.precio * 1.21;
}
// Setter
set cambiarPrecio(nuevoPrecio) {
if (nuevoPrecio > 0) {
this.precio = nuevoPrecio;
}
}
}
const laptop = new Producto('Laptop', 1000, 'LT-001');
console.log(laptop.nombre); // "Laptop"
console.log(laptop.mostrarInfo()); // "Laptop - $1000 (LT-001)"
// console.log(laptop.#codigo); // Error: propiedad privada
// Usar getter
console.log(laptop.precioConIVA); // 1210
// Usar setter
laptop.cambiarPrecio = 1200;
console.log(laptop.precio); // 1200
Campos privados y encapsulación
JavaScript ahora soporta campos privados con el prefijo #
:
class CuentaBancaria {
// Campos privados
#saldo;
#movimientos = [];
#numeroCuenta;
constructor(titular, saldoInicial) {
this.titular = titular; // Propiedad pública
this.#saldo = saldoInicial;
this.#numeroCuenta = this.#generarNumeroCuenta();
}
// Método privado
#generarNumeroCuenta() {
return Math.floor(Math.random() * 1000000000);
}
// Método privado
#registrarMovimiento(tipo, cantidad) {
this.#movimientos.push({
tipo,
cantidad,
fecha: new Date()
});
}
// Métodos públicos
depositar(cantidad) {
if (cantidad <= 0) return false;
this.#saldo += cantidad;
this.#registrarMovimiento('depósito', cantidad);
return true;
}
retirar(cantidad) {
if (cantidad <= 0 || cantidad > this.#saldo) return false;
this.#saldo -= cantidad;
this.#registrarMovimiento('retiro', cantidad);
return true;
}
// Getter público para acceder a propiedad privada
get saldo() {
return this.#saldo;
}
get numeroCuenta() {
// Ocultar parte del número para seguridad
const numStr = this.#numeroCuenta.toString();
return '****' + numStr.slice(-4);
}
get historialMovimientos() {
// Devolver copia para evitar modificaciones
return [...this.#movimientos];
}
}
const cuenta = new CuentaBancaria('Ana García', 1000);
console.log(cuenta.titular); // "Ana García"
console.log(cuenta.saldo); // 1000
console.log(cuenta.numeroCuenta); // "****1234" (ejemplo)
cuenta.depositar(500);
cuenta.retirar(200);
console.log(cuenta.saldo); // 1300
// Ver historial
console.log(cuenta.historialMovimientos);
// [{tipo: "depósito", cantidad: 500, fecha: ...}, {tipo: "retiro", cantidad: 200, fecha: ...}]
// Intentos de acceso directo a propiedades privadas
// console.log(cuenta.#saldo); // Error de sintaxis
// cuenta.#registrarMovimiento('hack', 1000); // Error de sintaxis
Patrones de diseño con POO en JavaScript
Patrón Singleton
Garantiza que una clase tenga una única instancia y proporciona un punto de acceso global a ella.
class Configuracion {
static #instancia;
constructor() {
// Si ya existe una instancia, devolver esa
if (Configuracion.#instancia) {
return Configuracion.#instancia;
}
this.tema = 'claro';
this.idioma = 'es';
this.notificaciones = true;
// Guardar la instancia
Configuracion.#instancia = this;
}
cambiarTema(tema) {
this.tema = tema;
console.log(`Tema cambiado a: ${tema}`);
}
cambiarIdioma(idioma) {
this.idioma = idioma;
console.log(`Idioma cambiado a: ${idioma}`);
}
}
// Prueba
const config1 = new Configuracion();
const config2 = new Configuracion();
console.log(config1 === config2); // true (misma instancia)
config1.cambiarTema('oscuro');
console.log(config2.tema); // "oscuro" (se refleja en todas las referencias)
Patrón Factory
Crea objetos sin especificar la clase exacta que se creará.
// Clases de productos
class Boton {
constructor(texto, color) {
this.texto = texto;
this.color = color;
}
render() {
return `<button style="color: ${this.color}">${this.texto}</button>`;
}
}
class Input {
constructor(placeholder, tipo) {
this.placeholder = placeholder;
this.tipo = tipo;
}
render() {
return `<input type="${this.tipo}" placeholder="${this.placeholder}">`;
}
}
class Label {
constructor(texto, forAttr) {
this.texto = texto;
this.forAttr = forAttr;
}
render() {
return `<label for="${this.forAttr}">${this.texto}</label>`;
}
}
// Factory
class UIFactory {
crearElemento(tipo, ...args) {
switch (tipo) {
case 'boton':
return new Boton(...args);
case 'input':
return new Input(...args);
case 'label':
return new Label(...args);
default:
throw new Error(`Tipo de elemento no soportado: ${tipo}`);
}
}
}
// Uso
const factory = new UIFactory();
const botonGuardar = factory.crearElemento('boton', 'Guardar', 'blue');
const inputNombre = factory.crearElemento('input', 'Escribe tu nombre', 'text');
const labelNombre = factory.crearElemento('label', 'Nombre:', 'nombre');
console.log(botonGuardar.render()); // <button style="color: blue">Guardar</button>
console.log(inputNombre.render()); // <input type="text" placeholder="Escribe tu nombre">
console.log(labelNombre.render()); // <label for="nombre">Nombre:</label>
Patrón Observer
Define una dependencia uno a muchos entre objetos, de manera que cuando un objeto cambia su estado, todos sus dependientes son notificados automáticamente.
class Observable {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} recibió: ${JSON.stringify(data)}`);
}
}
// Ejemplo: Sistema de notificaciones
class NotificationSystem extends Observable {
constructor() {
super();
this.notifications = [];
}
addNotification(message) {
const notification = {
id: Date.now(),
message,
date: new Date()
};
this.notifications.push(notification);
this.notify(notification);
}
}
// Uso
const notificationSystem = new NotificationSystem();
// Crear observadores
const mobileApp = new Observer('Aplicación móvil');
const webApp = new Observer('Aplicación web');
const emailService = new Observer('Servicio de email');
// Suscribir observadores
notificationSystem.subscribe(mobileApp);
notificationSystem.subscribe(webApp);
notificationSystem.subscribe(emailService);
// Añadir notificación
notificationSystem.addNotification('¡Nuevo mensaje recibido!');
// Todos los observadores reciben la notificación
// Cancelar suscripción
notificationSystem.unsubscribe(emailService);
// Nueva notificación
notificationSystem.addNotification('Actualización disponible');
// Solo mobileApp y webApp reciben la notificación
Ejemplo práctico: Sistema de gestión de tareas
Vamos a crear un sistema de gestión de tareas utilizando POO:
// Clase base para elementos del sistema
class Item {
constructor(titulo, descripcion) {
this.titulo = titulo;
this.descripcion = descripcion;
this.fechaCreacion = new Date();
this.id = Item.generarId();
}
static generarId() {
return Math.random().toString(36).substr(2, 9);
}
actualizar(datos) {
Object.assign(this, datos);
}
obtenerInfo() {
return {
id: this.id,
titulo: this.titulo,
descripcion: this.descripcion,
fechaCreacion: this.fechaCreacion
};
}
}
// Clase para tareas
class Tarea extends Item {
constructor(titulo, descripcion, prioridad = 'normal') {
super(titulo, descripcion);
this.completada = false;
this.prioridad = prioridad;
this.fechaVencimiento = null;
}
completar() {
this.completada = true;
this.fechaCompletado = new Date();
}
establecerVencimiento(fecha) {
this.fechaVencimiento = new Date(fecha);
}
estaVencida() {
if (!this.fechaVencimiento) return false;
return new Date() > this.fechaVencimiento;
}
obtenerInfo() {
return {
...super.obtenerInfo(),
completada: this.completada,
prioridad: this.prioridad,
fechaVencimiento: this.fechaVencimiento,
vencida: this.estaVencida()
};
}
}
// Clase para proyectos (contenedores de tareas)
class Proyecto extends Item {
constructor(titulo, descripcion) {
super(titulo, descripcion);
this.tareas = [];
}
agregarTarea(tarea) {
this.tareas.push(tarea);
return tarea.id;
}
eliminarTarea(id) {
const indice = this.tareas.findIndex(tarea => tarea.id === id);
if (indice !== -1) {
this.tareas.splice(indice, 1);
return true;
}
return false;
}
buscarTarea(id) {
return this.tareas.find(tarea => tarea.id === id);
}
obtenerTareas(filtro = {}) {
let resultado = [...this.tareas];
if (filtro.completada !== undefined) {
resultado = resultado.filter(tarea => tarea.completada === filtro.completada);
}
if (filtro.prioridad) {
resultado = resultado.filter(tarea => tarea.prioridad === filtro.prioridad);
}
if (filtro.vencidas) {
resultado = resultado.filter(tarea => tarea.estaVencida());
}
return resultado;
}
obtenerResumen() {
const total = this.tareas.length;
const completadas = this.tareas.filter(tarea => tarea.completada).length;
const pendientes = total - completadas;
const vencidas = this.tareas.filter(tarea => !tarea.completada && tarea.estaVencida()).length;
return {
total,
completadas,
pendientes,
vencidas,
progreso: total ? Math.round((completadas / total) * 100) : 0
};
}
}
// Clase para gestionar usuarios
class Usuario {
#password;
constructor(nombre, email, password) {
this.nombre = nombre;
this.email = email;
this.#password = password;
this.proyectos = [];
}
verificarPassword(password) {
return this.#password === password;
}
cambiarPassword(oldPassword, newPassword) {
if (this.verificarPassword(oldPassword)) {
this.#password = newPassword;
return true;
}
return false;
}
crearProyecto(titulo, descripcion) {
const proyecto = new Proyecto(titulo, descripcion);
this.proyectos.push(proyecto);
return proyecto;
}
eliminarProyecto(id) {
const indice = this.proyectos.findIndex(proyecto => proyecto.id === id);
if (indice !== -1) {
this.proyectos.splice(indice, 1);
return true;
}
return false;
}
obtenerProyectos() {
return this.proyectos.map(proyecto => ({
id: proyecto.id,
titulo: proyecto.titulo,
descripcion: proyecto.descripcion,
resumen: proyecto.obtenerResumen()
}));
}
}
// Clase para gestionar el sistema completo
class SistemaGestionTareas {
constructor() {
this.usuarios = [];
}
registrarUsuario(nombre, email, password) {
// Verificar si el email ya existe
if (this.usuarios.some(user => user.email === email)) {
throw new Error('El email ya está registrado');
}
const usuario = new Usuario(nombre, email, password);
this.usuarios.push(usuario);
return usuario;
}
iniciarSesion(email, password) {
const usuario = this.usuarios.find(user => user.email === email);
if (usuario && usuario.verificarPassword(password)) {
return usuario;
}
return null;
}
}
// Uso del sistema
const sistema = new SistemaGestionTareas();
// Registrar usuario
const usuario = sistema.registrarUsuario('Ana García', 'ana@ejemplo.com', 'password123');
// Crear proyecto
const proyectoTrabajo = usuario.crearProyecto('Proyecto Trabajo', 'Tareas relacionadas con el trabajo');
// Crear tareas
const tarea1 = new Tarea('Preparar presentación', 'Crear slides para la reunión', 'alta');
tarea1.establecerVencimiento('2023-12-15');
const tarea2 = new Tarea('Enviar email', 'Contactar al cliente sobre el proyecto');
const tarea3 = new Tarea('Revisar documentación', 'Leer la documentación técnica', 'baja');
// Agregar tareas al proyecto
proyectoTrabajo.agregarTarea(tarea1);
proyectoTrabajo.agregarTarea(tarea2);
proyectoTrabajo.agregarTarea(tarea3);
// Completar una tarea
tarea2.completar();
// Obtener información
console.log('Resumen del proyecto:', proyectoTrabajo.obtenerResumen());
// { total: 3, completadas: 1, pendientes: 2, vencidas: 0, progreso: 33 }
console.log('Tareas pendientes:', proyectoTrabajo.obtenerTareas({ completada: false }));
console.log('Tareas de alta prioridad:', proyectoTrabajo.obtenerTareas({ prioridad: 'alta' }));
// Proyectos del usuario
console.log('Proyectos del usuario:', usuario.obtenerProyectos());