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.
- El constructor nos permite inicializar las propiedades.
- 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.
- Con los métodos
setNombre
ysetEdad
, controlamos mejor los cambios en las propiedades. - 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.
extends
indica queEmpleado
hereda dePersona
.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.
- Los métodos estáticos se invocan directamente desde la clase, no desde la instancia.
- 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.
- El uso de
get
yset
permite una sintaxis más natural, como si accediéramos directamente a la propiedad. - 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.
- El polimorfismo consiste en redefinir métodos en clases hijas para que se comporten diferente.
mostrarSueldo()
cambia la lógica en la claseGerente
.
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.
- La composición permite que una clase use otra clase como parte de su definición, en lugar de usar herencia.
- 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.
- Al usar módulos en JavaScript, cada archivo puede exportar e importar clases.
- 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.
- El método privado
#calcularIntereses
no puede llamarse fuera de la clase. - 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
- El patrón Singleton asegura que haya solamente una instancia de la clase.
- 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!