Clases, herencia y polimorfismo
Hasta ahora has usado objetos literales ({}) y funciones constructoras.
Las clases son azúcar sintáctico sobre el mismo prototipo, pero con una
sintaxis más clara y mejor soporte para herencia. Son el eje de la
Programación Orientada a Objetos (POO).
1.1 · Definir una clase
class Coche {
// el constructor se ejecuta al hacer new Coche()
constructor(marca, modelo, anyo) {
// propiedades públicas
this.marca = marca;
this.modelo = modelo;
this.anyo = anyo;
this.encendido = false;
}
// métodos: se definen sin function ni =>
arrancar() {
this.encendido = true;
console.log(`${this.marca} ${this.modelo} arrancado`);
}
apagar() {
this.encendido = false;
console.log("Motor apagado");
}
info() {
return `${this.anyo} ${this.marca} ${this.modelo} — ${this.encendido ? "encendido" : "apagado"}`;
}
}
const miCoche = new Coche("Seat", "León", 2024);
miCoche.arrancar(); // Seat León arrancado
console.log(miCoche.info()); // 2024 Seat León — encendido
this se refiere a la
instancia (el objeto creado). Fuera de la clase, su valor depende de cómo se llame al método.1.2 · Encapsulación: campos privados
No todo debe ser visible desde fuera. Los campos privados se marcan con
# y solo se accede desde dentro de la clase:
class CuentaBancaria {
#saldo; // privado: solo accesible dentro de la clase
constructor(titular, saldoInicial) {
this.titular = titular;
this.#saldo = saldoInicial;
}
ingresar(cantidad) {
if (cantidad <= 0) throw new Error("Cantidad no válida");
this.#saldo += cantidad;
}
retirar(cantidad) {
if (cantidad > this.#saldo) throw new Error("Fondos insuficientes");
this.#saldo -= cantidad;
}
get saldo() { return this.#saldo; }
}
const cuenta = new CuentaBancaria("Ana", 1000);
cuenta.ingresar(500);
console.log(cuenta.saldo); // 1500 (a través del getter)
// cuenta.#saldo = 999999; ← SyntaxError: privado
1.3 · Herencia: extends y super
Una clase puede heredar de otra. La subclase tiene acceso a todos los métodos del padre y puede añadir o redefinir los suyos:
class Vehiculo {
constructor(marca, ruedas) {
this.marca = marca;
this.ruedas = ruedas;
}
descripcion() {
return `${this.marca} con ${this.ruedas} ruedas`;
}
}
class CocheElectrico extends Vehiculo {
#bateria; // privado de la subclase
constructor(marca, bateriaKwh) {
super(marca, 4); // llama al constructor del padre
this.#bateria = bateriaKwh;
}
// sobrescribir un método del padre
descripcion() {
return `${super.descripcion()} · ${this.#bateria} kWh`;
}
autonomia() {
return this.#bateria * 7; // ≈ 7 km por kWh
}
}
const tesla = new CocheElectrico("Tesla", 75);
console.log(tesla.descripcion()); // Tesla con 4 ruedas · 75 kWh
console.log(tesla.autonomia()); // 525
1.4 · Polimorfismo
Distintas clases implementan el mismo método con comportamientos diferentes. El código que consume esos objetos no necesita saber de qué tipo son:
class Gato {
hablar() { return "Miau"; }
}
class Perro {
hablar() { return "Guau"; }
}
class Pato {
hablar() { return "Cuac"; }
}
const animales = [new Gato(), new Perro(), new Pato()];
// cada animal responde a .hablar() a su manera
animales.forEach((a) => console.log(a.hablar()));
// → "Miau", "Guau", "Cuac"
Módulos ES: organizar proyectos reales
Cuando un proyecto crece, todo en un solo archivo se vuelve inmanejable. Los módulos ES (ECMAScript Modules) permiten dividir el código en archivos independientes que se importan donde se necesitan.
2.1 · Estructura de un proyecto con módulos
mi-tienda/
├── index.html
├── js/
│ ├── main.js ← punto de entrada (type="module")
│ ├── Producto.js ← clase Producto
│ ├── Carrito.js ← clase Carrito (importa Producto)
│ └── utils.js ← funciones auxiliares
└── css/
└── styles.css
2.2 · Exportar y importar
export class Producto {
constructor(nombre, precio) {
this.nombre = nombre;
this.precio = precio;
}
formatear() {
return `${this.nombre}: ${this.precio.toFixed(2)} €`;
}
}
import { Producto } from "./Producto.js";
import { formatearEuro } from "./utils.js";
const p = new Producto("Auriculares", 49.99);
console.log(p.formatear());
// → "Auriculares: 49.99 €"
2.3 · Tipos de exportación
| Forma | Sintaxis | Cuándo usarla |
|---|---|---|
| Named export | export const fn = … | Varias utilidades en un archivo (lo más habitual) |
| Default export | export default class … | Una exportación principal por archivo |
| Re-export | export { X } from "./X.js" | Agrupar varios módulos en un barrel file |
// utils.js
export default function formatearEuro(n) {
return n.toFixed(2) + " €";
}
// main.js — importas el nombre que quieras
import formatear from "./utils.js";
2.4 · El atributo type="module"
Para que los módulos funcionen, el <script> debe incluir
type="module". Esto activa el modo estricto automáticamente y aísla el
alcance de cada archivo:
<!-- ¡sin type="module" no funcionan los imports! -->
<script type="module" src="js/main.js"></script>
file:// en el navegador. Necesitas un servidor local (live-server, VS Code
Live Server, python -m http.server o similar).Patrones de diseño en JavaScript
Un patrón de diseño es una solución probada a un problema de diseño común. No son librerías niFrameworks: son ideas reutilizables. Veremos tres que se usan constantemente en JavaScript.
3.1 · Patrón Módulo (IIFE)
Antes de existir import/export, se usaba una IIFE
(Immediately Invoked Function Expression) para encapsular código y exponer solo lo necesario:
const Contador = (() => {
let cuenta = 0; // privado: no se puede acceder desde fuera
return { // solo exponemos lo que queremos
incrementar() { cuenta++; },
decrementar() { cuenta--; },
valor() { return cuenta; },
};
})();
Contador.incrementar();
Contador.incrementar();
console.log(Contador.valor()); // 2
// Contador.cuenta → undefined (privado)
3.2 · Patrón Fábrica
Una fábrica es una función que devuelve objetos sin necesidad de
new. Es ideal cuando la creación del objeto requiere lógica condicional o
configuración:
function crearUsuario(nombre, rol = "alumno") {
if (!nombre) throw new Error("Nombre obligatorio");
const permisos = {
admin: ["leer", "escribir", "borrar"],
profesor: ["leer", "escribir"],
alumno: ["leer"],
};
return {
nombre,
rol,
permisos: permisos[rol] || permisos.alumno,
tienePermiso(accion) {
return this.permisos.includes(accion);
},
toString() {
return `${this.nombre} (${this.rol})`;
},
};
}
const admin = crearUsuario("Carlos", "admin");
console.log(admin.tienePermiso("borrar")); // true
const alumno = crearUsuario("Lucía");
console.log(alumno.tienePermiso("borrar")); // false
3.3 · Patrón Observador (pub/sub)
Un objeto (sujeto) notifica a una lista de suscriptores (observadores) cuando algo cambia. Es la base de los eventos del navegador y de frameworks como Vue o React ( state management).
class EventBus {
#listeners = {};
on(evento, callback) {
if (!this.#listeners[evento])
this.#listeners[evento] = [];
this.#listeners[evento].push(callback);
}
off(evento, callback) {
this.#listeners[evento] =
this.#listeners[evento]?.filter((fn) => fn !== callback) || [];
}
emit(evento, datos) {
this.#listeners[evento]?.forEach((fn) => fn(datos));
}
}
// Uso
const bus = new EventBus();
bus.on("pedido", (item) =>
console.log(`Añadido: ${item}`));
bus.on("pedido", (item) =>
console.log(`Total actualizado`));
bus.emit("pedido", "Auriculares");
// → "Añadido: Auriculares"
// → "Total actualizado"
Testing: escribe código que no se rompe
Un test es un script que ejecuta tu código con datos de prueba y comprueba que el resultado es el esperado. Si algo cambia y se rompe, el test falla y lo sabes antes de llegar a producción.
4.1 · ¿Por qué testear?
4.2 · Unit testing con Jest
Jest es el framework de testing más usado en JavaScript. Se instala con npm y ejecuta tests en Node.js.
npm init -y
npm install --save-dev jest
{
"scripts": {
"test": "jest"
}
}
export function sumar(a, b) {
return a + b;
}
export function capitalizar(str) {
if (!str) return "";
return str[0].toUpperCase() + str.slice(1);
}
import { sumar, capitalizar } from "./utils.js";
test("2 + 3 = 5", () => {
expect(sumar(2, 3)).toBe(5);
});
test("capitaliza la primera letra", () => {
expect(capitalizar("hola")).toBe("Hola");
});
test("string vacío devuelve vacío", () => {
expect(capitalizar("")).toBe("");
});
4.3 · Matchers más usados
| Matcher | Comprueba que… | Ejemplo |
|---|---|---|
.toBe(x) | igualdad estricta (===) | expect(2+2).toBe(4) |
.toEqual(obj) | igualdad profunda de objetos | expect({a:1}).toEqual({a:1}) |
.toBeTruthy() | que sea truthy | expect("ok").toBeTruthy() |
.toContain(x) | que un array contenga x | expect([1,2]).toContain(2) |
.toThrow() | que lance un error | expect(() => f()).toThrow() |
.toHaveLength(n) | longitud de array/string | expect("hi").toHaveLength(2) |
4.4 · Describe: agrupar tests
describe("capitalizar", () => {
test("primera letra en mayúscula", () => {
expect(capitalizar("hello")).toBe("Hello");
});
test("string vacío", () => {
expect(capitalizar("")).toBe("");
});
test("ya está capitalizado", () => {
expect(capitalizar("Hola")).toBe("Hola");
});
test("un solo carácter", () => {
expect(capitalizar("a")).toBe("A");
});
});
test().
Si un test falla, sabes exactamente qué se rompió.✍️ Retos tipo entrevista técnica
Crea una carpeta bloque8. Cada ejercicio es un archivo JavaScript (o HTML
con <script type="module">). Estos retos son los que más aparecen en
entrevistas de front-end: practícalos hasta que salgan fluidos.
Sección A · Clases y POO
1. bank-account.js — Cuenta bancaria
Crea una clase CuentaBancaria con campo privado #saldo,
método ingresar(cantidad), retirar(cantidad) (que lance error
si no hay fondos), y un getter saldo. Escribe al menos 3 tests con Jest.
- No se puede acceder a
#saldodesde fuera de la clase ingresar(-5)lanza error "Cantidad no válida"retirar(200)con saldo 100 lanza "Fondos insuficientes"
Ver solución
// bank-account.js
export class CuentaBancaria {
#saldo;
constructor(saldoInicial = 0) {
if (saldoInicial < 0) throw new Error("Saldo inicial no válido");
this.#saldo = saldoInicial;
}
get saldo() { return this.#saldo; }
ingresar(cantidad) {
if (cantidad <= 0) throw new Error("Cantidad no válida");
this.#saldo += cantidad;
}
retirar(cantidad) {
if (cantidad <= 0) throw new Error("Cantidad no válida");
if (cantidad > this.#saldo) throw new Error("Fondos insuficientes");
this.#saldo -= cantidad;
}
}
// bank-account.test.js
import { CuentaBancaria } from "./bank-account.js";
test("saldo inicial", () => {
expect(new CuentaBancaria(100).saldo).toBe(100);
});
test("ingresar incrementa saldo", () => {
const c = new CuentaBancaria(100);
c.ingresar(50);
expect(c.saldo).toBe(150);
});
test("ingresar cantidad negativa lanza error", () => {
expect(() => new CuentaBancaria(0).ingresar(-5)).toThrow("Cantidad no válida");
});
test("retirar sin fondos lanza error", () => {
const c = new CuentaBancaria(100);
expect(() => c.retirar(200)).toThrow("Fondos insuficientes");
});
test("retirar descuenta del saldo", () => {
const c = new CuentaBancaria(200);
c.retirar(70);
expect(c.saldo).toBe(130);
});2. vehicle-inheritance.js — Herencia de vehículos
Crea una clase base Vehiculo (marca, color, encender/apagar) y dos subclases:
Coche (nº de puertas) y Moto (cilindrada). Ambas deben
sobrescribir un método descripcion(). Demuestra polimorfismo con un array de
vehículos mixtos.
Ver solución
class Vehiculo {
#encendido = false;
constructor(marca, color) {
this.marca = marca;
this.color = color;
}
get estaEncendido() { return this.#encendido; }
encender() { this.#encendido = true; }
apagar() { this.#encendido = false; }
descripcion() {
return `${this.marca} ${this.color} — ${this.estaEncendido ? "encendido" : "apagado"}`;
}
}
class Coche extends Vehiculo {
constructor(marca, color, puertas) {
super(marca, color);
this.puertas = puertas;
}
descripcion() {
return `${super.descripcion()} · ${this.puertas} puertas`;
}
}
class Moto extends Vehiculo {
constructor(marca, color, cc) {
super(marca, color);
this.cc = cc;
}
descripcion() {
return `${super.descripcion()} · ${this.cc} cc`;
}
}
// Polimorfismo
const flota = [
new Coche("Seat", "rojo", 5),
new Moto("Honda", "negra", 600),
new Coche("Tesla", "blanco", 4),
];
flota.forEach(v => {
v.encender();
console.log(v.descripcion());
});3. stack.js — Implementar una pila (Stack)
Implementa la estructura de datos Stack (pila) usando una clase:
push(item), pop(), peek() (mirar el último sin
sacarlo), isEmpty(), size (getter). Usa un array privado
#items.
Ver solución
export class Stack {
#items = [];
push(item) { this.#items.push(item); }
pop() {
if (this.isEmpty()) throw new Error("Stack vacío");
return this.#items.pop();
}
peek() {
if (this.isEmpty()) throw new Error("Stack vacío");
return this.#items[this.#items.length - 1];
}
isEmpty() { return this.#items.length === 0; }
get size() { return this.#items.length; }
}
// test
const s = new Stack();
s.push(10);
s.push(20);
expect(s.peek()).toBe(20);
expect(s.pop()).toBe(20);
expect(s.size).toBe(1);
expect(s.isEmpty()).toBe(false);Sección B · Módulos y patrones
4. event-bus.js — EventBus completo
Implementa la clase EventBus vista en teoría (con campos privados):
on(evento, fn), off(evento, fn), emit(evento, datos).
Escribe tests que comprueben que los listeners se ejecutan y que off los
elimina correctamente.
Ver solución
export class EventBus {
#listeners = {};
on(evento, fn) {
(this.#listeners[evento] ??= []).push(fn);
}
off(evento, fn) {
this.#listeners[evento] =
this.#listeners[evento]?.filter(f => f !== fn) ?? [];
}
emit(evento, datos) {
this.#listeners[evento]?.forEach(fn => fn(datos));
}
}
// tests
const bus = new EventBus();
let recibido = null;
const handler = (d) => { recibido = d; };
bus.on("test", handler);
bus.emit("test", 42);
expect(recibido).toBe(42);
bus.off("test", handler);
bus.emit("test", 99);
expect(recibido).toBe(42); // no cambió: handler eliminado5. product-factory.js — Patrón fábrica
Crea una función fábrica crearProducto(tipo, nombre, precio) que devuelva
objetos con métodos info() y aplicarDescuento(pct). Para
tipo "digital", añade propiedad descargable: true. Para
tipo "fisico", añade peso (obligatorio como tercer argumento).
Ver solución
export function crearProducto(tipo, nombre, precio, extras = {}) {
if (!["digital", "fisico"].includes(tipo))
throw new Error(`Tipo "${tipo}" no soportado`);
const producto = {
tipo,
nombre,
precio,
...extras,
aplicarDescuento(pct) {
this.precio = +(this.precio * (1 - pct / 100)).toFixed(2);
},
info() {
let base = `${this.nombre}: ${this.precio.toFixed(2)} € (${this.tipo})`;
if (this.tipo === "digital") base += " [descargable]";
if (this.tipo === "fisico") base += ` [${this.peso} kg]`;
return base;
},
};
if (tipo === "fisico" && extras.peso == null)
throw new Error("Peso obligatorio para productos físicos");
return producto;
}
// test
const e = crearProducto("digital", "Ebook", 9.99);
expect(e.descargable).toBe(true);
e.aplicarDescuento(20);
expect(e.precio).toBe(7.99);
const f = crearProducto("fisico", "Libro", 19.99, { peso: 0.4 });
expect(f.peso).toBe(0.4);6. shopping-cart.js — Carrito con Patrón Observador
Crea una clase Carrito que use internamente un EventBus para
notificar cuando se añade ("añadido") o elimina ("eliminado")
un producto. Métodos: agregar(producto), eliminar(nombre),
vaciar(), getter total.
Ver solución
import { EventBus } from "./event-bus.js";
export class Carrito {
#items = [];
#bus = new EventBus();
on(evento, fn) { this.#bus.on(evento, fn); }
off(evento, fn) { this.#bus.off(evento, fn); }
agregar(producto) {
this.#items.push(producto);
this.#bus.emit("añadido", producto);
}
eliminar(nombre) {
const idx = this.#items.findIndex(p => p.nombre === nombre);
if (idx === -1) return null;
const [elim] = this.#items.splice(idx, 1);
this.#bus.emit("eliminado", elim);
return elim;
}
vaciar() { this.#items = []; }
get total() {
return +this.#items.reduce((s, p) => s + p.precio, 0).toFixed(2);
}
get items() { return [...this.#items]; }
}
// test
const carrito = new Carrito();
let avisos = 0;
carrito.on("añadido", () => avisos++);
carrito.agregar({ nombre: "Camiseta", precio: 20 });
carrito.agregar({ nombre: "Gorra", precio: 15 });
expect(avisos).toBe(2);
expect(carrito.total).toBe(35);
carrito.eliminar("Camiseta");
expect(carrito.total).toBe(15);
expect(carrito.items.length).toBe(1);Sección C · Retos de entrevista
7. flatten.js — Aplanar arrays anidados
Escribe una función aplanar(arr) que tome un array con cualquier nivel
de anidamiento y devuelva uno plano. Sin Array.flat() (solo para
practicar recursividad).
aplanar([1, [2, [3, [4]]]])→[1, 2, 3, 4]aplanar([])→[]- No usar flat ni flatMap
Ver solución
export function aplanar(arr) {
const resultado = [];
for (const item of arr) {
if (Array.isArray(item)) {
resultado.push(...aplanar(item));
} else {
resultado.push(item);
}
}
return resultado;
}
// tests
test("aplanar arrays anidados", () => {
expect(aplanar([1, [2, [3, [4]]]])).toEqual([1, 2, 3, 4]);
});
test("array vacío", () => {
expect(aplanar([])).toEqual([]);
});
test("array ya plano", () => {
expect(aplanar([1, 2, 3])).toEqual([1, 2, 3]);
});8. debounce.js — Implementar debounce
Escribe una función debounce(fn, delay) que devuelva una nueva función:
solo ejecuta fn cuando pasan delay ms sin invocarla. Muy usada
en buscadores (no pedir a la API en cada tecla).
Ver solución
export function debounce(fn, delay) {
let timerId = null;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => fn.apply(this, args), delay);
};
}
// test (usando fake timers de Jest)
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
test("no ejecuta hasta que pasa el delay", () => {
const fn = jest.fn();
const debounced = debounce(fn, 300);
debounced();
debounced();
debounced();
expect(fn).not.toHaveBeenCalled();
jest.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledTimes(1);
});9. deep-clone.js — Clonar objetos profundos
Escribe una función deepClone(obj) que haga una copia profunda de un
objeto (incluidos objetos anidados y arrays). Sin structuredClone (para
practicar).
deepClone({ a: { b: 1 } })debe devolver un objeto nuevo cona.b === 1- El clon no debe compartir referencias con el original
- Manejar arrays y objetos anidados
Ver solución
export function deepClone(obj) {
if (obj === null || typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(item => deepClone(item));
const clon = {};
for (const key of Object.keys(obj)) {
clon[key] = deepClone(obj[key]);
}
return clon;
}
// tests
test("clon profundo de objetos anidados", () => {
const original = { a: { b: { c: 42 } }, d: [1, [2, 3]] };
const clon = deepClone(original);
expect(clon).toEqual(original);
expect(clon).not.toBe(original);
expect(clon.a).not.toBe(original.a);
expect(clon.a.b).not.toBe(original.a.b);
expect(clon.d).not.toBe(original.d);
});
test("primitivos se devuelven tal cual", () => {
expect(deepClone(42)).toBe(42);
expect(deepClone("hola")).toBe("hola");
expect(deepClone(null)).toBe(null);
});10. 🏆 RETO INTEGRADOR — task-manager
Construye un gestor de tareas completo usando todo lo aprendido en el bloque. Debe estar dividido en módulos y pasar tests.
- Task.js: clase
Taskcon id, título, completada y prioridad ("baja","media","alta"). - TaskManager.js: clase con un array privado de tareas y métodos
agregar(titulo, prioridad),completar(id),eliminar(id),filtrarPorPrioridad(p),buscarPorTitulo(texto). - EventBus integrado: emite
"tarea-creada","tarea-completada","tarea-eliminada". - Tests: al menos 8 tests cubriendo los métodos principales.
- Bonus: método
exportarJSON()que devuelva las tareas como string JSON.
Ver una solución posible
// Task.js
let nextId = 1;
export class Task {
#completada = false;
constructor(titulo, prioridad = "media") {
if (!titulo) throw new Error("Título obligatorio");
if (!["baja", "media", "alta"].includes(prioridad))
throw new Error("Prioridad no válida");
this.id = nextId++;
this.titulo = titulo;
this.prioridad = prioridad;
}
get completada() { return this.#completada; }
completar() { this.#completada = true; }
toJSON() {
return {
id: this.id,
titulo: this.titulo,
prioridad: this.prioridad,
completada: this.completada,
};
}
}
// TaskManager.js
import { Task } from "./Task.js";
import { EventBus } from "./event-bus.js";
export class TaskManager {
#tareas = [];
#bus = new EventBus();
on(evt, fn) { this.#bus.on(evt, fn); }
off(evt, fn) { this.#bus.off(evt, fn); }
agregar(titulo, prioridad) {
const tarea = new Task(titulo, prioridad);
this.#tareas.push(tarea);
this.#bus.emit("tarea-creada", tarea);
return tarea;
}
completar(id) {
const tarea = this.#tareas.find(t => t.id === id);
if (!tarea) throw new Error("Tarea no encontrada");
tarea.completar();
this.#bus.emit("tarea-completada", tarea);
return tarea;
}
eliminar(id) {
const idx = this.#tareas.findIndex(t => t.id === id);
if (idx === -1) throw new Error("Tarea no encontrada");
const [elim] = this.#tareas.splice(idx, 1);
this.#bus.emit("tarea-eliminada", elim);
return elim;
}
filtrarPorPrioridad(p) {
return this.#tareas.filter(t => t.prioridad === p);
}
buscarPorTitulo(texto) {
const min = texto.toLowerCase();
return this.#tareas.filter(t =>
t.titulo.toLowerCase().includes(min));
}
get tareas() { return [...this.#tareas]; }
exportarJSON() {
return JSON.stringify(
this.#tareas.map(t => t.toJSON()), null, 2);
}
}
// task-manager.test.js
import { TaskManager } from "./TaskManager.js";
let mgr;
beforeEach(() => { mgr = new TaskManager(); });
test("agregar tarea", () => {
const t = mgr.agregar("Estudiar JS", "alta");
expect(t.titulo).toBe("Estudiar JS");
expect(t.prioridad).toBe("alta");
expect(mgr.tareas.length).toBe(1);
});
test("completar tarea", () => {
const t = mgr.agregar("Hacer ejercicio");
mgr.completar(t.id);
expect(t.completada).toBe(true);
});
test("eliminar tarea", () => {
const t = mgr.agregar("Tarea temporal");
mgr.eliminar(t.id);
expect(mgr.tareas.length).toBe(0);
});
test("filtrar por prioridad", () => {
mgr.agregar("A", "baja");
mgr.agregar("B", "alta");
mgr.agregar("C", "alta");
expect(mgr.filtrarPorPrioridad("alta").length).toBe(2);
});
test("buscar por título", () => {
mgr.agregar("Estudiar CSS");
mgr.agregar("Estudiar JS");
mgr.agregar("Comprar pan");
expect(mgr.buscarPorTitulo("estudiar").length).toBe(2);
});
test("completar tarea inexistente lanza error", () => {
expect(() => mgr.completar(999)).toThrow("Tarea no encontrada");
});
test("emite evento al agregar", () => {
let emitido = null;
mgr.on("tarea-creada", (t) => { emitido = t; });
const t = mgr.agregar("Nueva");
expect(emitido).toBe(t);
});
test("exportarJSON devuelve string válido", () => {
mgr.agregar("Test");
const json = mgr.exportarJSON();
expect(typeof json).toBe("string");
const arr = JSON.parse(json);
expect(arr.length).toBe(1);
});📝 Examen global del curso
Este examen cubre los ocho bloques del curso de JavaScript. 20 preguntas, una sola respuesta correcta por pregunta. Necesitas 70 % (14/20) para superarlo. Supéralo y habrás completado el itinerario completo de JavaScript de Técnico Web.
🧪 Parte B · Laboratorio: proyecto final integrador
Aplica todo lo aprendido en los ocho bloques para construir un gestor de tareas completo con interfaz web. Abre el laboratorio: