10 Ejercicios resueltos de POO en JavaScript

Ejercicios resueltos de POO en JavaScript (programación orientada a objetos)  10 ejercicios resueltos + aprendizaje teorico

Aprende conceptos clave y descubre ejemplos prácticos de programación orientada a objetos en JavaScript, potenciando tus habilidades para crear aplicaciones sumamente robustas y altamente escalables.

Aprendizaje teórico

La programación orientada a objetos (POO) es un paradigma de desarrollo de software que organiza el código en torno a objetos, los cuales representan entidades con propiedades (atributos) y métodos (funciones). En JavaScript, aunque tradicionalmente se ha utilizado un modelo basado en prototipos, las clases introducidas en las versiones más recientes del lenguaje han facilitado la adopción de prácticas de POO de forma más clara y estructurada.

Algunos conceptos fundamentales de la POO en JavaScript son:

  • Clases: Plantillas que definen la estructura y el comportamiento de los objetos.
  • Objetos: Instancias de una clase que contienen sus propias propiedades y métodos.
  • Encapsulación: Agrupar datos (propiedades) y métodos (funciones) relacionados dentro de un objeto o clase, protegiendo la información interna.
  • Herencia: Permite crear nuevas clases basadas en clases existentes, compartiendo sus propiedades y métodos.
  • Polimorfismo: La capacidad de un método de comportarse de manera diferente según la clase que lo implemente.

Estos principios permiten escribir código más limpio, modular y fácil de mantener, ya que se separan responsabilidades y se promueve la reutilización.

Intento de resolución antes de la solución

Antes de consultar la solución, te animamos a intentar resolver cada ejercicio por tu cuenta. La mejor forma de aprender a programar es practicando y equivocándote, para luego corregir tus errores. A continuación, se plantearán los enunciados de manera breve; si lo deseas, puedes tomarte el tiempo necesario para pensar en tu propia implementación antes de comparar con nuestro código propuesto.

10 ejercicios resueltos

Ejercicio 1: Crear una clase simple

Enunciado
Define una clase Persona con las propiedades nombre y edad. Incluye un método que permita mostrar un mensaje de saludo con el nombre y la edad de la persona.

// Definimos la clase Persona
class Persona {
  // El constructor se ejecuta al crear una instancia de la clase
  constructor(nombre, edad) {
    this.nombre = nombre; // Asignamos el valor del parámetro nombre a la propiedad nombre
    this.edad = edad;     // Asignamos el valor del parámetro edad a la propiedad edad
  }

  // Método para mostrar un saludo
  saludar() {
    console.log(`Hola, me llamo ${this.nombre} y tengo ${this.edad} años.`);
  }
}

// Creamos una instancia de Persona y llamamos a su método saludar
const persona1 = new Persona('Laura', 25);
persona1.saludar(); // Imprime: Hola, me llamo Laura y tengo 25 años.
  1. El constructor nos permite inicializar las propiedades.
  2. El método saludar() devuelve un mensaje utilizando los datos de la instancia.

Ejercicio 2: Métodos para modificar propiedades

Enunciado
Usa la clase Persona del ejercicio anterior y agrega métodos para modificar el nombre y la edad después de que el objeto haya sido creado.

class Persona {
  constructor(nombre, edad) {
    this.nombre = nombre;
    this.edad = edad;
  }

  saludar() {
    console.log(`Hola, me llamo ${this.nombre} y tengo ${this.edad} años.`);
  }

  // Método para cambiar el nombre
  setNombre(nuevoNombre) {
    this.nombre = nuevoNombre;
  }

  // Método para cambiar la edad
  setEdad(nuevaEdad) {
    this.edad = nuevaEdad;
  }
}

const persona2 = new Persona('Carlos', 30);
persona2.saludar(); // Hola, me llamo Carlos y tengo 30 años.

// Modificamos sus propiedades
persona2.setNombre('Antonio');
persona2.setEdad(35);

persona2.saludar(); // Hola, me llamo Antonio y tengo 35 años.
  1. Con los métodos setNombre y setEdad, controlamos mejor los cambios en las propiedades.
  2. Esto ayuda a mantener la encapsulación al definir puntos de acceso controlados para modificar el estado del objeto.

Ejercicio 3: Herencia básica

Enunciado
Crea una clase Empleado que herede de Persona. La clase Empleado debe tener una propiedad adicional sueldo y un método para mostrarlo.

// Clase base Persona
class Persona {
  constructor(nombre, edad) {
    this.nombre = nombre;
    this.edad = edad;
  }

  saludar() {
    console.log(`Hola, me llamo ${this.nombre} y tengo ${this.edad} años.`);
  }
}

// Clase derivada Empleado que extiende de Persona
class Empleado extends Persona {
  constructor(nombre, edad, sueldo) {
    // Llamamos al constructor de la clase base con super
    super(nombre, edad);
    this.sueldo = sueldo;
  }

  mostrarSueldo() {
    console.log(`Mi sueldo es de $${this.sueldo}.`);
  }
}

// Creamos una instancia de Empleado
const empleado1 = new Empleado('María', 28, 2000);
empleado1.saludar();       // Hola, me llamo María y tengo 28 años.
empleado1.mostrarSueldo(); // Mi sueldo es de $2000.
  1. extends indica que Empleado hereda de Persona.
  2. super() llama al constructor de la clase padre para inicializar los atributos heredados.

Ejercicio 4: Métodos estáticos

Enunciado
Añade un método estático a la clase Persona llamado crearAnonimo(), que retorne un objeto de tipo Persona con un nombre genérico y edad 0.

class Persona {
  constructor(nombre, edad) {
    this.nombre = nombre;
    this.edad = edad;
  }

  saludar() {
    console.log(`Hola, me llamo ${this.nombre} y tengo ${this.edad} años.`);
  }

  // Método estático para crear una persona anónima
  static crearAnonimo() {
    return new Persona('Anónimo', 0);
  }
}

const anonimo = Persona.crearAnonimo();
anonimo.saludar(); // Hola, me llamo Anónimo y tengo 0 años.
  1. Los métodos estáticos se invocan directamente desde la clase, no desde la instancia.
  2. Sirven para crear utilidades o formas alternativas de generar instancias.

Ejercicio 5: Uso de getters y setters

Enunciado
En la clase Persona, reemplaza los métodos setNombre y setEdad por setters y crea getters correspondientes para acceder a las propiedades de forma controlada.

class Persona {
  constructor(nombre, edad) {
    this._nombre = nombre;
    this._edad = edad;
  }

  // Getter para nombre
  get nombre() {
    return this._nombre;
  }

  // Setter para nombre
  set nombre(valor) {
    if (valor.length > 0) {
      this._nombre = valor;
    } else {
      console.log('El nombre no puede estar vacío.');
    }
  }

  // Getter para edad
  get edad() {
    return this._edad;
  }

  // Setter para edad
  set edad(valor) {
    if (valor >= 0) {
      this._edad = valor;
    } else {
      console.log('La edad no puede ser negativa.');
    }
  }

  saludar() {
    console.log(`Hola, me llamo ${this._nombre} y tengo ${this._edad} años.`);
  }
}

// Probamos los getters y setters
const persona3 = new Persona('Sofía', 22);
console.log(persona3.nombre); // Sofía

persona3.nombre = '';         // El nombre no puede estar vacío.
persona3.nombre = 'Ana';      // Actualiza a Ana

persona3.edad = -1;           // La edad no puede ser negativa.
persona3.edad = 23;           // Actualiza a 23

persona3.saludar();           // Hola, me llamo Ana y tengo 23 años.
  1. El uso de get y set permite una sintaxis más natural, como si accediéramos directamente a la propiedad.
  2. El guion bajo (_nombre, _edad) es una convención para indicar propiedades internas.

Ejercicio 6: Polimorfismo

Enunciado
Crea otra clase llamada Gerente que herede de Empleado. Sobrescribe el método mostrarSueldo() para incluir un bono adicional en el cálculo del sueldo mostrado.

class Persona {
  constructor(nombre, edad) {
    this.nombre = nombre;
    this.edad = edad;
  }

  saludar() {
    console.log(`Hola, me llamo ${this.nombre} y tengo ${this.edad} años.`);
  }
}

class Empleado extends Persona {
  constructor(nombre, edad, sueldo) {
    super(nombre, edad);
    this.sueldo = sueldo;
  }

  mostrarSueldo() {
    console.log(`Mi sueldo es de $${this.sueldo}.`);
  }
}

// Clase Gerente que extiende de Empleado
class Gerente extends Empleado {
  constructor(nombre, edad, sueldo, bono) {
    super(nombre, edad, sueldo);
    this.bono = bono;
  }

  // Sobrescribimos mostrarSueldo para incluir el bono
  mostrarSueldo() {
    const sueldoConBono = this.sueldo + this.bono;
    console.log(`Mi sueldo, incluyendo bono, es de $${sueldoConBono}.`);
  }
}

const gerente1 = new Gerente('Roberto', 40, 3000, 500);
gerente1.saludar();        // Hola, me llamo Roberto y tengo 40 años.
gerente1.mostrarSueldo();  // Mi sueldo, incluyendo bono, es de $3500.
  1. El polimorfismo consiste en redefinir métodos en clases hijas para que se comporten diferente.
  2. mostrarSueldo() cambia la lógica en la clase Gerente.

Ejercicio 7: Composición de objetos

Enunciado
Crea una clase Dirección con propiedades como calle y ciudad. Luego, en la clase Persona, agrega una propiedad para asociar un objeto de tipo Dirección en lugar de heredar.

// Clase Dirección
class Direccion {
  constructor(calle, ciudad) {
    this.calle = calle;
    this.ciudad = ciudad;
  }
}

// Clase Persona con una propiedad de tipo Dirección
class Persona {
  constructor(nombre, edad, direccion) {
    this.nombre = nombre;
    this.edad = edad;
    this.direccion = direccion; // Guardamos un objeto de la clase Direccion
  }

  saludar() {
    console.log(`Hola, me llamo ${this.nombre}, tengo ${this.edad} años, y vivo en la calle ${this.direccion.calle}, ${this.direccion.ciudad}.`);
  }
}

// Creamos una instancia de Direccion y luego una instancia de Persona
const miDireccion = new Direccion('Av. Siempreviva 742', 'Springfield');
const persona4 = new Persona('Bart', 10, miDireccion);

persona4.saludar(); 
// Hola, me llamo Bart, tengo 10 años, y vivo en la calle Av. Siempreviva 742, Springfield.
  1. La composición permite que una clase use otra clase como parte de su definición, en lugar de usar herencia.
  2. Esto promueve la construcción de objetos más flexibles y un diseño modular.

Ejercicio 8: Clases como módulos

Enunciado
Imagina que deseas dividir tu aplicación. Crea dos archivos separados: persona.js con la clase Persona y empleado.js con la clase Empleado. Luego, impórtalos y úsalos en un archivo principal app.js.

// persona.js
export class Persona {
  constructor(nombre, edad) {
    this.nombre = nombre;
    this.edad = edad;
  }

  saludar() {
    console.log(`Hola, soy ${this.nombre} y tengo ${this.edad} años.`);
  }
}

// empleado.js
import { Persona } from './persona.js';

export class Empleado extends Persona {
  constructor(nombre, edad, puesto) {
    super(nombre, edad);
    this.puesto = puesto;
  }

  mostrarPuesto() {
    console.log(`Trabajo como ${this.puesto}.`);
  }
}

// app.js
import { Persona } from './persona.js';
import { Empleado } from './empleado.js';

const persona5 = new Persona('Lucía', 29);
persona5.saludar(); // Hola, soy Lucía y tengo 29 años.

const empleado2 = new Empleado('Javier', 35, 'Desarrollador');
empleado2.saludar();       // Hola, soy Javier y tengo 35 años.
empleado2.mostrarPuesto(); // Trabajo como Desarrollador.
  1. Al usar módulos en JavaScript, cada archivo puede exportar e importar clases.
  2. Esto facilita la organización y el mantenimiento del código en proyectos más grandes.

Ejercicio 9: Métodos privados (versión moderna)

Enunciado
Implementa un método privado en la clase Banco que calcule intereses internamente, y un método público que muestre los intereses sin exponer la lógica interna.

Nota: Los métodos privados con # se introdujeron en una versión más reciente de JavaScript y requieren configuraciones específicas en el entorno para funcionar.

class Banco {
  constructor(nombre) {
    this.nombre = nombre;
  }

  // Método privado (requiere configuración moderna en el entorno)
  #calcularIntereses(cantidad, tasa) {
    return cantidad * (tasa / 100);
  }

  // Método público para mostrar los intereses
  mostrarIntereses(cantidad, tasa) {
    const intereses = this.#calcularIntereses(cantidad, tasa);
    console.log(`En el banco ${this.nombre}, el interés es $${intereses} sobre un monto de $${cantidad}.`);
  }
}

const miBanco = new Banco('Banco Central');
miBanco.mostrarIntereses(1000, 5);
// En el banco Banco Central, el interés es $50 sobre un monto de $1000.
  1. El método privado #calcularIntereses no puede llamarse fuera de la clase.
  2. Este enfoque refuerza la encapsulación al ocultar los detalles de implementación.

Ejercicio 10: Patrón Singleton con clases

Enunciado
Crea una clase Configuracion que siga el patrón Singleton, de forma que solo se pueda crear una única instancia que represente la configuración global de tu aplicación.

class Configuracion {
  constructor(tema, idioma) {
    // Si ya existe una instancia, la retornamos
    if (Configuracion.instancia) {
      return Configuracion.instancia;
    }

    // Si no existe, creamos y asignamos
    this.tema = tema;
    this.idioma = idioma;
    Configuracion.instancia = this;
  }

  // Método para mostrar la configuración actual
  mostrarConfig() {
    console.log(`Tema: ${this.tema}, Idioma: ${this.idioma}`);
  }
}

// Creamos la primera instancia
const config1 = new Configuracion('Oscuro', 'ES');
config1.mostrarConfig(); // Tema: Oscuro, Idioma: ES

// Intentamos crear otra instancia con diferentes valores
const config2 = new Configuracion('Claro', 'EN');
config2.mostrarConfig(); // Seguirá mostrando: Tema: Oscuro, Idioma: ES

// Verificamos que ambas referencias apunten a la misma instancia
console.log(config1 === config2); // true
  1. El patrón Singleton asegura que haya solamente una instancia de la clase.
  2. Al guardar la instancia en Configuracion.instancia, devolvemos siempre la misma.

Preguntas frecuentes sobre POO en JavaScript

¿La programación orientada a objetos en JavaScript funciona igual que en otros lenguajes?

No exactamente. JavaScript tiene un modelo basado en prototipos, pero con la sintaxis de clases introducida en versiones modernas, se puede trabajar de manera similar a lenguajes orientados a objetos clásicos. Aun así, bajo el capó sigue existiendo el sistema de prototipos.

¿Cuál es la diferencia entre usar funciones constructoras y clases en JavaScript?

Las funciones constructoras eran la forma tradicional de crear objetos en JavaScript. Con la introducción de la sintaxis de clases (ES6 y versiones posteriores), el código se hace más legible y orientado a objetos. Internamente, siguen usándose prototipos, pero las clases ofrecen una forma más intuitiva de organizar el código.

¿Puedo utilizar la herencia y la composición al mismo tiempo?

Sí, de hecho, es muy común. La herencia sirve para extender funcionalidades de una clase padre, mientras que la composición consiste en agregar clases como propiedades para lograr modularidad. Combinar ambas técnicas puede dar lugar a diseños más flexibles.

¡Enhorabuena! Has dado un gran paso para consolidar tus conocimientos en programación orientada a objetos en JavaScript. Con la práctica constante, comprenderás cada vez mejor cómo aprovechar las ventajas de la herencia, la encapsulación y el polimorfismo para desarrollar aplicaciones escalables y mantenibles. ¡Sigue experimentando y no temas equivocarte!

Si quieres continuar explorando temas relacionados y seguir aprendiendo nuevas técnicas de desarrollo, visita nuestro blog para encontrar más artículos, tutoriales y recursos que te ayudarán a dominar la programación en JavaScript y otras tecnologías. ¡Te esperamos con más contenido educativo y ejercicios prácticos!

No pares de aprender¡Hay mucho más esperándote! Sigue aprendiendo dentro y fuera de nuestra web.
Comparte este artículo
Alejandro Nes
Alejandro Nes

¡Hola! Soy Alejandro Nes, desarrollador web con formación en informática y apasionado por la creación de contenido educativo. Aprendamos juntos a programar :)

Artículos: 16