HDP115

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.

CE

Cristian Escalante

Última actualización: 22 de abril de 2025

javascript
programación web
desarrollo frontend

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());
Almacenamiento en el Navegador
Aprende a almacenar datos en el navegador utilizando localSt...
Módulos y Bundlers
Aprende a organizar y optimizar tu código JavaScript utiliza...

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de Python

Aprende los conceptos básicos de Python

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025