Llevas media docena de lecciones rozando esta idea sin ponerle nombre. En Expresiones de Función y Funciones Flecha guardaste funciones dentro de un objeto de formateadores y de un array de validadores. En Ámbito y Closures escribiste listarSegun(filtro), que recibía una función, y crearFiltroPorResponsable(nombre), que devolvía una. Ese nombre es función de orden superior, y es la técnica que convierte el código repetitivo en código configurable. En esta lección la estudiarás a fondo: construirás tus propias versiones de forEach, map, filter y reduce para entender exactamente qué hacen por dentro, aprenderás a componer funciones en tuberías y terminarás montando un motor de informes para Taller Nómada al que se le enchufan un filtro, un transformador y un agregador distintos según lo que pida Marta.
Aviso de progresión: los arrays de JavaScript ya traen
forEach,map,filteryreducede serie. Aquí los implementarás a mano para entender el mecanismo; los métodos nativos, con toda su potencia y sus matices, llegan en Iteración sobre Arrays y Buscar, Ordenar y Agregar Datos. Hasta entonces, seguimos con bucles y arrays paralelos.
Contenido
- Qué es una función de orden superior
- Callbacks: el vocabulario
miForEach: hacer algo con cada elementomiMap: transformar cada elementomiFilter: quedarse con algunosmiReduce: reducirlo todo a un valor- Encadenar las cuatro sobre el backlog
- Funciones que devuelven funciones: fábricas y currying
- Composición de funciones:
componerypipe - Funciones como configuración
- Ejemplo integrador: el motor de informes de Taller Nómada
- Una nota sobre legibilidad
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué es una función de orden superior
La definición es corta y precisa:
Una función de orden superior (higher-order function, HOF) es una función que recibe una o más funciones como argumento, devuelve una función, o ambas cosas.
Todo lo demás es una función normal, o de primer orden. Y esto solo es posible porque, como viste en 03-02, en JavaScript las funciones son valores.
// De orden superior: RECIBE una función
function repetir(veces, accion) {
for (let i = 0; i < veces; i++) {
accion(i);
}
}
repetir(3, (n) => console.log(`Recordatorio ${n + 1} a Iván`));
// Recordatorio 1 a Iván
// Recordatorio 2 a Iván
// Recordatorio 3 a Iván
// De orden superior: DEVUELVE una función
function crearRecordatorio(persona) {
return (asunto) => `Recordatorio para ${persona}: ${asunto}`;
}
const avisarALucia = crearRecordatorio('Lucía');
console.log(avisarALucia('la web de reservas vence el 2 de octubre'));
// Recordatorio para Lucía: la web de reservas vence el 2 de octubre¿Qué se gana? Separar el esqueleto del detalle. repetir sabe repetir, pero no sabe qué hay que hacer en cada vuelta: eso se lo dices tú. El mismo esqueleto sirve para imprimir, para acumular o para validar.
flowchart LR
A["Esqueleto fijo<br/>(recorrer, repetir, ordenar)"] --> C["Función de orden superior"]
B["Detalle variable<br/>(qué hacer con cada elemento)"] --> C
C --> D["Comportamiento concreto"]
- Callbacks: el vocabulario
Una función que se pasa como argumento para que otra la llame se denomina callback (función de retrollamada). El vocabulario asociado, que conviene usar con precisión:
| Término | Significado | En el ejemplo |
|---|---|---|
| Función de orden superior | La que recibe o devuelve funciones | repetir |
| Callback | La función que se pasa para ser llamada | (n) => console.log(...) |
| Invocar el callback | Llamarlo desde dentro de la HOF | accion(i) |
| Firma del callback | Qué parámetros recibirá y qué debe devolver | (elemento, indice) => ... |
| Predicado | Callback que devuelve true/false |
(h) => h > 10 |
| Transformador | Callback que devuelve un valor nuevo | (h) => h * 2 |
| Comparador | Callback que devuelve -1, 0 o 1 |
(a, b) => a - b |
| Reductor | Callback que combina acumulador y elemento | (acc, h) => acc + h |
Un punto que confunde al principio: quien define el callback no decide cuándo se ejecuta ni con qué argumentos. Eso lo decide la función de orden superior. Tú escribes el "qué hacer"; ella pone el "cuándo" y el "con qué".
function conCadaHora(horas, callback) {
for (let i = 0; i < horas.length; i++) {
callback(horas[i], i); // ← la HOF decide: dos argumentos, en este orden
}
}
conCadaHora([12, 6, 14], (h, i) => console.log(`Tarea ${i + 1}: ${h} h`));
conCadaHora([12, 6, 14], (h) => console.log(h)); // puedes ignorar el segundoPor eso, antes de escribir un callback, hay que saber qué firma espera la función que lo recibirá. Es lo primero que se mira en la documentación de cualquier método.
miForEach: hacer algo con cada elemento
miForEach: hacer algo con cada elementoEl más sencillo: recorre el array y llama al callback con cada elemento. No devuelve nada; su valor está en el efecto que produce.
'use strict';
/**
* Llama a `accion` con cada elemento del array.
* @param {Array} lista array a recorrer
* @param {Function} accion callback (elemento, indice, lista)
*/
function miForEach(lista, accion) {
for (let i = 0; i < lista.length; i++) {
accion(lista[i], i, lista);
}
}
const titulos = [
'Rediseñar la sala polivalente',
'Cartelería del taller de serigrafía',
'Actualizar la web de reservas',
'Inventario de tintas de serigrafía',
'Guía de encuadernación para residentes',
'Presupuesto de la carpintería'
];
miForEach(titulos, (titulo, i) => {
console.log(`${i + 1}. ${titulo}`);
});
// 1. Rediseñar la sala polivalente
// 2. Cartelería del taller de serigrafía
// ...Los tres argumentos del callback (elemento, indice, lista) no son un capricho: es exactamente la firma que usa el forEach nativo, así que aprenderla ahora te ahorra trabajo en el Módulo 4.
Comparado con un for clásico, lo que aporta es que desaparece la maquinaria del bucle: no hay i = 0, ni i < lista.length, ni i++, y por tanto no hay errores de índice fuera de rango.
Una diferencia importante con el for: desde un callback no se puede hacer break. Un return dentro del callback termina el callback, no el recorrido.
miForEach(titulos, (titulo) => {
if (titulo.startsWith('Inventario')) return; // salta ESTE, no corta el recorrido
console.log(titulo);
});Si necesitas cortar, usa un for con break o una función pensada para buscar (lo verás en 04-05 con find y some).
miMap: transformar cada elemento
miMap: transformar cada elementomap construye un array nuevo del mismo tamaño, donde cada elemento es el resultado de aplicar el callback al original.
/**
* Devuelve un array nuevo con el resultado de transformar cada elemento.
* @param {Array} lista
* @param {Function} transformar callback (elemento, indice, lista) → valor nuevo
* @returns {Array} array nuevo del mismo tamaño
*/
function miMap(lista, transformar) {
const resultado = [];
for (let i = 0; i < lista.length; i++) {
resultado.push(transformar(lista[i], i, lista));
}
return resultado;
}
const horas = [12, 6, 14, 3, 8, 5];
const enMinutos = miMap(horas, (h) => h * 60);
console.log(enMinutos); // [ 720, 360, 840, 180, 480, 300 ]
console.log(horas); // [ 12, 6, 14, 3, 8, 5 ] ← intacto
const numeradas = miMap(titulos, (t, i) => `${i + 1}. ${t}`);
console.log(numeradas[5]); // 6. Presupuesto de la carpinteríaTres propiedades que definen map y que conviene grabar:
| Propiedad | Consecuencia |
|---|---|
| Devuelve un array nuevo | El original no se toca: es una operación pura |
| Mismo tamaño que el original | Si entran 6, salen 6 (para filtrar está filter) |
| Uno a uno, en orden | El elemento i del resultado viene del i del original |
El error más común es usar map cuando en realidad quieres forEach:
// ✗ map cuyo resultado se tira: usa forEach
miMap(titulos, (t) => console.log(t)); // devuelve [undefined × 6]
// ✓
miForEach(titulos, (t) => console.log(t));Regla: si vas a usar el array resultante, map; si solo quieres el efecto, forEach.
miFilter: quedarse con algunos
miFilter: quedarse con algunosfilter construye un array nuevo con los elementos para los que el callback (un predicado) devuelve un valor truthy.
/**
* Devuelve un array nuevo con los elementos que cumplen el predicado.
* @param {Array} lista
* @param {Function} predicado callback (elemento, indice, lista) → booleano
* @returns {Array} array nuevo, de 0 a lista.length elementos
*/
function miFilter(lista, predicado) {
const resultado = [];
for (let i = 0; i < lista.length; i++) {
if (predicado(lista[i], i, lista)) {
resultado.push(lista[i]);
}
}
return resultado;
}
const largas = miFilter(horas, (h) => h > 8);
console.log(largas); // [ 12, 14 ]
const deSerigrafia = miFilter(titulos, (t) => t.toLowerCase().includes('serigrafía'));
console.log(deSerigrafia);
// [ 'Cartelería del taller de serigrafía', 'Inventario de tintas de serigrafía' ]Con arrays paralelos, el truco es filtrar índices en lugar de valores, para poder consultar después el resto de los arrays:
const responsables = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const estados = ['en-curso', 'pendiente', 'pendiente', 'hecha', 'en-curso', 'pendiente'];
const indices = [0, 1, 2, 3, 4, 5];
const abiertasDeIvan = miFilter(indices, (i) => responsables[i] === 'Iván' && estados[i] !== 'hecha');
console.log(abiertasDeIvan); // [ 0, 4, 5 ]
miForEach(abiertasDeIvan, (i) => console.log(`${titulos[i]} — ${horas[i]} h`));
// Rediseñar la sala polivalente — 12 h
// Guía de encuadernación para residentes — 8 h
// Presupuesto de la carpintería — 5 hEste patrón "filtrar índices" es un apaño necesario mientras los datos vivan en arrays paralelos. En el Módulo 4, con un array de objetos, se escribirá directamente sobre las tareas.
miMap |
miFilter |
|
|---|---|---|
| Tamaño del resultado | Igual al original | Entre 0 y el original |
| El callback devuelve | Un valor nuevo | Un booleano |
| Los elementos del resultado | Transformados | Los originales, sin tocar |
| Nombre del callback | Transformador | Predicado |
miReduce: reducirlo todo a un valor
miReduce: reducirlo todo a un valorreduce es la más potente y la que más cuesta. Recorre el array manteniendo un acumulador que se va actualizando con cada elemento, y al final devuelve ese acumulador. El resultado puede ser un número, un texto, un array o un objeto: cualquier cosa.
/**
* Reduce el array a un único valor.
* @param {Array} lista
* @param {Function} reductor callback (acumulador, elemento, indice, lista) → nuevo acumulador
* @param {*} valorInicial valor de partida del acumulador
*/
function miReduce(lista, reductor, valorInicial) {
let acumulador = valorInicial;
for (let i = 0; i < lista.length; i++) {
acumulador = reductor(acumulador, lista[i], i, lista);
}
return acumulador;
}Todo el mecanismo cabe en cuatro líneas, pero conviene verlo paso a paso con la suma de horas del backlog:
| Vuelta | acc al entrar |
h |
acc + h |
|---|---|---|---|
| 0 | 0 (inicial) | 12 | 12 |
| 1 | 12 | 6 | 18 |
| 2 | 18 | 14 | 32 |
| 3 | 32 | 3 | 35 |
| 4 | 35 | 8 | 43 |
| 5 | 43 | 5 | 48 |
Y ahora los casos que demuestran que reduce no es solo "sumar":
// Máximo
const maxHoras = miReduce(horas, (acc, h) => (h > acc ? h : acc), horas[0]);
console.log(maxHoras); // 14
// Concatenar texto
const listado = miReduce(titulos, (acc, t) => `${acc}\n· ${t}`, 'Backlog:');
console.log(listado);
// Backlog:
// · Rediseñar la sala polivalente
// · ...
// Contar por categoría → devuelve un OBJETO
const porResponsable = miReduce(responsables, (acc, nombre) => {
acc[nombre] = (acc[nombre] ?? 0) + 1;
return acc;
}, {});
console.log(porResponsable); // { 'Iván': 3, Marta: 2, 'Lucía': 1 }
// Esfuerzo ponderado (la regla que arrastras desde el Módulo 2)
const PESOS = { alta: 3, media: 2, baja: 1 };
const prioridades = ['alta', 'media', 'alta', 'baja', 'media', 'alta'];
const indices2 = [0, 1, 2, 3, 4, 5];
const esfuerzo = miReduce(indices2, (acc, i) => acc + (PESOS[prioridades[i]] ?? 0) * horas[i], 0);
console.log(esfuerzo); // 124Dos advertencias sobre reduce:
- El reductor debe devolver siempre el acumulador. Olvidar el
returnes el error número uno; el acumulador pasa a serundefinedy todo revienta en la vuelta siguiente. - Da siempre un valor inicial. Sin él, el nativo usa el primer elemento como acumulador y falla con arrays vacíos. Nuestra
miReducesimplemente devolveríaundefined.
flowchart LR
A["inicial: 0"] --> B["+12 → 12"] --> C["+6 → 18"] --> D["+14 → 32"]
D --> E["+3 → 35"] --> F["+8 → 43"] --> G["+5 → 48"]
- Encadenar las cuatro sobre el backlog
Con las cuatro piezas ya se pueden expresar preguntas de negocio completas de forma casi declarativa. Pregunta de Marta: ¿cuántas horas abiertas de prioridad alta hay en el backlog, y cuáles son?
'use strict';
const HOY = '2026-09-20';
const indices = [0, 1, 2, 3, 4, 5];
const titulos = [
'Rediseñar la sala polivalente',
'Cartelería del taller de serigrafía',
'Actualizar la web de reservas',
'Inventario de tintas de serigrafía',
'Guía de encuadernación para residentes',
'Presupuesto de la carpintería'
];
const responsables = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const prioridades = ['alta', 'media', 'alta', 'baja', 'media', 'alta'];
const estados = ['en-curso', 'pendiente', 'pendiente', 'hecha', 'en-curso', 'pendiente'];
const horas = [12, 6, 14, 3, 8, 5];
const fechasLimite = ['2026-09-30', '2026-10-15', '2026-10-02',
'2026-09-12', '2026-11-05', '2026-09-05'];
// 1. Filtrar: alta prioridad y no terminadas
const altasAbiertas = miFilter(indices, (i) => prioridades[i] === 'alta' && estados[i] !== 'hecha');
console.log(altasAbiertas); // [ 0, 2, 5 ]
// 2. Transformar: una línea legible por tarea
const lineas = miMap(altasAbiertas, (i) => {
const aviso = fechasLimite[i] < HOY ? ' ⚠ VENCIDA' : '';
return `${titulos[i]} · ${responsables[i]} · ${horas[i]} h${aviso}`;
});
// 3. Reducir: total de horas
const horasAltas = miReduce(altasAbiertas, (acc, i) => acc + horas[i], 0);
// 4. Efecto: imprimir
miForEach(lineas, (linea) => console.log(` ${linea}`));
console.log(`Total de horas abiertas en prioridad alta: ${horasAltas} h`);Salida:
Rediseñar la sala polivalente · Iván · 12 h Actualizar la web de reservas · Lucía · 14 h Presupuesto de la carpintería · Iván · 5 h ⚠ VENCIDA Total de horas abiertas en prioridad alta: 31 h
Observa la estructura: filtrar → transformar → agregar → efecto. Cada paso es una operación con nombre, se puede leer por separado y se puede cambiar sin tocar los demás. Comparado con el bucle único del Módulo 2 —que hacía las cuatro cosas mezcladas dentro de un if— la diferencia de legibilidad es enorme.
- Funciones que devuelven funciones: fábricas y currying
Ya conociste las fábricas en 03-04. Aquí las llevamos un paso más allá.
// Fábrica de predicados por campo y valor
function crearPredicado(arrayCampo, valorEsperado) {
return (indice) => arrayCampo[indice] === valorEsperado;
}
const esDeIvan = crearPredicado(responsables, 'Iván');
const estaPendiente = crearPredicado(estados, 'pendiente');
const esAlta = crearPredicado(prioridades, 'alta');
console.log(miFilter(indices, esDeIvan)); // [ 0, 4, 5 ]
console.log(miFilter(indices, estaPendiente)); // [ 1, 2, 5 ]Y combinadores que producen predicados nuevos a partir de otros:
const y = (...predicados) => (i) => {
for (const p of predicados) {
if (!p(i)) return false;
}
return true;
};
const o = (...predicados) => (i) => {
for (const p of predicados) {
if (p(i)) return true;
}
return false;
};
const no = (predicado) => (i) => !predicado(i);
console.log(miFilter(indices, y(esDeIvan, esAlta))); // [ 0, 5 ]
console.log(miFilter(indices, o(esAlta, estaPendiente))); // [ 0, 1, 2, 5 ]
console.log(miFilter(indices, no(crearPredicado(estados, 'hecha')))); // [ 0, 1, 2, 4, 5 ]Con tres funciones de cinco líneas has construido un pequeño lenguaje de consultas sobre el backlog. Eso es poder expresivo.
Currying
Currying es transformar una función de varios parámetros en una cadena de funciones de un parámetro cada una:
// Versión normal
function esfuerzo(peso, horas) {
return peso * horas;
}
// Versión "currificada"
const esfuerzoCurry = (peso) => (horas) => peso * horas;
console.log(esfuerzo(3, 12)); // 36
console.log(esfuerzoCurry(3)(12)); // 36
// La gracia está en fijar el primer argumento
const esfuerzoAlta = esfuerzoCurry(3);
console.log(esfuerzoAlta(12)); // 36
console.log(esfuerzoAlta(14)); // 42
console.log(esfuerzoAlta(5)); // 15La técnica emparentada, la aplicación parcial, fija algunos argumentos y deja el resto:
function conMargen(limite, horasAbiertas) {
return limite - horasAbiertas;
}
const margenSemanal = (horasAbiertas) => conMargen(40, horasAbiertas); // R7 fijada
console.log(margenSemanal(25)); // 15
console.log(margenSemanal(45)); // -5¿Cuándo merece la pena? Cuando vas a llamar muchas veces con el mismo primer argumento, o cuando necesitas una función de un solo parámetro para pasarla a miMap o miFilter. Fuera de esos casos, currificar por currificar hace el código más difícil de leer.
- Composición de funciones:
componer y pipe
componer y pipeSi tienes varias transformaciones pequeñas, encadenarlas a mano es incómodo:
const quitarEspacios = (t) => t.trim();
const aMinusculas = (t) => t.toLowerCase();
const sinAcentos = (t) => t.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
const aGuiones = (t) => t.replace(/\s+/g, '-');
// Anidado: se lee de dentro afuera, al revés de como ocurre
const slug = aGuiones(sinAcentos(aMinusculas(quitarEspacios(' Presupuesto de la Carpintería '))));
console.log(slug); // presupuesto-de-la-carpinteriaComponer es crear una función nueva que aplica varias en cadena:
/** componer(f, g)(x) === f(g(x)) — se aplica de derecha a izquierda */
function componer(f, g) {
return (x) => f(g(x));
}
const limpiar = componer(aMinusculas, quitarEspacios);
console.log(limpiar(' ETIQUETA DE SERIGRAFÍA ')); // etiqueta de serigrafíaY pipe hace lo mismo pero en el orden en que se lee, de izquierda a derecha, que es mucho más natural:
/** pipe(f, g, h)(x) === h(g(f(x))) — se aplica de izquierda a derecha */
function pipe(...funciones) {
return function (valorInicial) {
let valor = valorInicial;
for (const fn of funciones) {
valor = fn(valor);
}
return valor;
};
}
const aEtiqueta = pipe(quitarEspacios, aMinusculas, sinAcentos, aGuiones);
console.log(aEtiqueta(' Presupuesto de la Carpintería ')); // presupuesto-de-la-carpinteria
console.log(aEtiqueta(' Serigrafía ')); // serigrafia
console.log(aEtiqueta('Encuadernación Manual')); // encuadernacion-manualFíjate en que pipe es literalmente miReduce aplicado a funciones en lugar de a datos: parte de un valor inicial y lo va transformando. La aplicación directa al proyecto es la R9 (etiquetas en minúsculas, sin duplicados):
const etiquetasCrudas = [' Carpintería ', 'CARPINTERÍA', 'Serigrafía', 'serigrafía ', 'Diseño'];
const normalizadas = miMap(etiquetasCrudas, pipe(quitarEspacios, aMinusculas, sinAcentos));
console.log(normalizadas);
// [ 'carpinteria', 'carpinteria', 'serigrafia', 'serigrafia', 'diseno' ]
const sinDuplicados = miReduce(normalizadas, (acc, e) => {
if (!acc.includes(e)) acc.push(e);
return acc;
}, []);
console.log(sinDuplicados); // [ 'carpinteria', 'serigrafia', 'diseno' ]flowchart LR
A["' Presupuesto de la Carpintería '"] --> B["quitarEspacios"] --> C["aMinusculas"]
C --> D["sinAcentos"] --> E["aGuiones"] --> F["'presupuesto-de-la-carpinteria'"]
- Funciones como configuración
El tercer gran uso de las HOF es parametrizar el comportamiento de un componente. En lugar de escribir una función distinta para cada variante, escribes una y le pasas la pieza que cambia.
// Un único "ordenador" que acepta cualquier comparador
function ordenarIndices(indices, comparar) {
const copia = [];
for (const i of indices) copia.push(i);
// Ordenación por inserción, para no depender todavía de sort() (llega en 04-05)
for (let i = 1; i < copia.length; i++) {
const actual = copia[i];
let j = i - 1;
while (j >= 0 && comparar(copia[j], actual) > 0) {
copia[j + 1] = copia[j];
j--;
}
copia[j + 1] = actual;
}
return copia;
}
const porFecha = (a, b) => (fechasLimite[a] < fechasLimite[b] ? -1 : fechasLimite[a] > fechasLimite[b] ? 1 : 0);
const porHorasDesc = (a, b) => horas[b] - horas[a];
const porPrioridad = (a, b) => (PESOS[prioridades[b]] ?? 0) - (PESOS[prioridades[a]] ?? 0);
console.log(miMap(ordenarIndices(indices, porFecha), (i) => `${fechasLimite[i]} ${titulos[i]}`));
// [ '2026-09-05 Presupuesto de la carpintería',
// '2026-09-12 Inventario de tintas de serigrafía',
// '2026-09-30 Rediseñar la sala polivalente', ... ]
console.log(miMap(ordenarIndices(indices, porHorasDesc), (i) => `${horas[i]} h`));
// [ '14 h', '12 h', '8 h', '6 h', '5 h', '3 h' ]El mismo principio se aplica a validadores y formateadores intercambiables:
function validarCon(reglas, indice) {
const errores = [];
miForEach(reglas, (regla) => {
if (!regla.comprobar(indice)) errores.push(`${regla.codigo}: ${regla.mensaje}`);
});
return errores;
}
const reglasDeAlta = [
{ codigo: 'R2', mensaje: 'El título es obligatorio.', comprobar: (i) => titulos[i].trim().length > 0 },
{ codigo: 'R3', mensaje: 'Las horas deben estar en 0-40.', comprobar: (i) => horas[i] > 0 && horas[i] <= 40 },
{ codigo: 'R10', mensaje: 'La tarea está vencida.', comprobar: (i) => !(fechasLimite[i] < HOY && estados[i] !== 'hecha') }
];
console.log(validarCon(reglasDeAlta, 5));
// [ 'R10: La tarea está vencida.' ]
console.log(validarCon(reglasDeAlta, 0));
// []Nota el patrón, porque es el mismo que estructura casi todo el software profesional: un motor genérico + un conjunto de funciones enchufables. Añadir una regla, un orden o un formato ya no significa tocar el motor.
- Ejemplo integrador: el motor de informes de Taller Nómada
Juntamos todo en una sola pieza. generarInforme no sabe nada de tareas: sabe filtrar, transformar, agregar y presentar, y recibe cada una de esas cuatro cosas como función.
'use strict';
/**
* Motor de informes genérico.
* @param {Array} fuente elementos de partida (aquí, índices del backlog)
* @param {Object} configuracion
* @param {string} configuracion.titulo
* @param {Function} configuracion.filtro (elemento) → booleano
* @param {Function} configuracion.transformar (elemento) → línea de texto
* @param {Function} configuracion.agregar (elementos) → resumen
* @param {Function} [configuracion.ordenar] comparador opcional
*/
function generarInforme(fuente, configuracion) {
const filtro = configuracion.filtro ?? (() => true);
const transformar = configuracion.transformar ?? ((x) => String(x));
const agregar = configuracion.agregar ?? (() => '');
const ordenar = configuracion.ordenar ?? null;
let seleccionados = miFilter(fuente, filtro);
if (ordenar !== null) seleccionados = ordenarIndices(seleccionados, ordenar);
const lineas = miMap(seleccionados, transformar);
const salida = [`── ${configuracion.titulo} ──`];
miForEach(lineas, (linea) => salida.push(` ${linea}`));
salida.push(` ${agregar(seleccionados)}`);
return miReduce(salida, (acc, l) => `${acc}\n${l}`, '').trim();
}
// ─── Piezas reutilizables ──────────────────────────────────────────
const noHecha = (i) => estados[i] !== 'hecha';
const vencida = (i) => fechasLimite[i] < HOY && estados[i] !== 'hecha';
const deResponsable = (nombre) => (i) => responsables[i] === nombre;
const lineaDetallada = (i) =>
`${titulos[i]} · ${responsables[i]} · ${prioridades[i]} · ${horas[i]} h · vence ${fechasLimite[i]}`;
const lineaCompacta = (i) => `${titulos[i]} (${horas[i]} h)`;
const sumaHoras = (seleccion) => `Total: ${miReduce(seleccion, (acc, i) => acc + horas[i], 0)} h`;
const cuentaYPeso = (seleccion) => {
const n = seleccion.length;
const peso = miReduce(seleccion, (acc, i) => acc + (PESOS[prioridades[i]] ?? 0) * horas[i], 0);
return `${n} tarea(s) · esfuerzo ponderado ${peso}`;
};
// ─── Informe 1: todo lo abierto, por fecha ─────────────────────────
console.log(generarInforme(indices, {
titulo: 'Trabajo abierto por fecha límite',
filtro: noHecha,
ordenar: porFecha,
transformar: lineaDetallada,
agregar: sumaHoras
}));── Trabajo abierto por fecha límite ── Presupuesto de la carpintería · Iván · alta · 5 h · vence 2026-09-05 Rediseñar la sala polivalente · Iván · alta · 12 h · vence 2026-09-30 Actualizar la web de reservas · Lucía · alta · 14 h · vence 2026-10-02 Cartelería del taller de serigrafía · Marta · media · 6 h · vence 2026-10-15 Guía de encuadernación para residentes · Iván · media · 8 h · vence 2026-11-05 Total: 45 h
// ─── Informe 2: la carga de Iván, compacta ─────────────────────────
console.log(generarInforme(indices, {
titulo: 'Carga abierta de Iván',
filtro: y(deResponsable('Iván'), noHecha),
ordenar: porHorasDesc,
transformar: lineaCompacta,
agregar: cuentaYPeso
}));── Carga abierta de Iván ── Rediseñar la sala polivalente (12 h) Guía de encuadernación para residentes (8 h) Presupuesto de la carpintería (5 h) 3 tarea(s) · esfuerzo ponderado 67
// ─── Informe 3: alertas de vencimiento ─────────────────────────────
console.log(generarInforme(indices, {
titulo: '⚠ Tareas vencidas',
filtro: vencida,
transformar: (i) => `${titulos[i]} — responsable: ${responsables[i]} — venció el ${fechasLimite[i]}`,
agregar: (seleccion) => `${seleccion.length} tarea(s) requieren atención inmediata`
}));── ⚠ Tareas vencidas ── Presupuesto de la carpintería — responsable: Iván — venció el 2026-09-05 1 tarea(s) requieren atención inmediata
Tres informes completamente distintos, cero cambios en el motor. Eso es lo que compran las funciones de orden superior: cuando llegue el Módulo 6 y Marta quiera filtrar el backlog desde un desplegable de la interfaz, lo único que cambiará es qué función se pasa como filtro.
- Una nota sobre legibilidad
Todo lo anterior es potente, y precisamente por eso conviene decir cuándo no usarlo. El estilo funcional tiene un punto de rendimientos decrecientes.
// ✗ Ilegible: currying + composición + reduce anidado para algo trivial
const total = pipe(
(xs) => miFilter(xs, (i) => estados[i] !== 'hecha'),
(xs) => miReduce(xs, (a, i) => a + horas[i], 0)
)(indices);
// ✓ Igual de correcto y mucho más claro
let total = 0;
for (let i = 0; i < horas.length; i++) {
if (estados[i] !== 'hecha') total += horas[i];
}Criterios para decidir:
| Usa HOF cuando… | Usa un bucle normal cuando… |
|---|---|
| La operación se repite con variantes | Es un cálculo puntual y simple |
| El "qué hacer" debe poder cambiar | El comportamiento es fijo |
| Quieres nombrar cada paso (filtrar, transformar) | El bucle cabe en cuatro líneas legibles |
| Vas a reutilizar las piezas | Solo se usa una vez |
| Cortas la lógica en trozos probables | Necesitas break, continue o índices retorcidos |
Y una regla de oro: el código se escribe una vez y se lee cincuenta. Si tu compañera tiene que dibujar un diagrama para entender una línea, esa línea está mal, por elegante que sea.
Errores Comunes y Consejos
1. Pasar el resultado en lugar de la función.
Síntoma: TypeError: predicado is not a function.
2. Olvidar el return en el reductor.
miReduce(responsables, (acc, n) => { acc[n] = (acc[n] ?? 0) + 1; }, {});
// ✗ TypeError: Cannot set properties of undefinedCon cuerpo de bloque, return acc; es obligatorio.
3. Usar map por su efecto secundario. Si no usas el array devuelto, querías forEach.
4. Intentar cortar un forEach con break. No es posible. Usa un for clásico.
5. Mutar el array original dentro de un callback. Modificar la lista mientras se recorre produce saltos y elementos repetidos. Trabaja siempre sobre el resultado.
6. Callbacks con efectos secundarios ocultos. Un predicado que además imprime o modifica algo hace imposible razonar sobre el filtro. Los predicados deben ser puros (03-03).
7. Consejo: nombra los callbacks que se repiten. miFilter(indices, noHecha) se lee infinitamente mejor que miFilter(indices, (i) => estados[i] !== 'hecha') repetido siete veces.
8. Consejo: documenta la firma esperada. Cuando escribas una HOF, deja claro en un comentario qué recibirá el callback y qué debe devolver. Es lo primero que necesita quien la use.
Ejercicios
Ejercicio 1 — miSome, miEvery y miFind
Implementa tres funciones de orden superior más, con la misma firma de callback (elemento, indice, lista):
miSome(lista, predicado):truesi al menos uno cumple; corta en cuanto encuentra uno.miEvery(lista, predicado):truesi todos cumplen; corta en cuanto falla uno.miFind(lista, predicado): devuelve el primer elemento que cumple, oundefined.
Úsalas para responder: ¿hay alguna tarea vencida? ¿están todas asignadas? ¿cuál es la primera tarea de más de 10 h?
Ejercicio 2 — agruparPor
Escribe agruparPor(lista, obtenerClave), una función de orden superior que devuelva un objeto donde cada clave es el resultado de obtenerClave(elemento) y cada valor es un array con los elementos de ese grupo. Úsala para agrupar los índices del backlog por responsable y por estado, y después calcula las horas abiertas de cada grupo.
Ejercicio 3 — Motor de informes ampliado
Amplía generarInforme para que acepte dos opciones más:
limite: número máximo de líneas a mostrar (si se recorta, añade una línea… y N más).formatoCabecera: una función(titulo, total) => stringque construya la cabecera.
Genera con él un informe "Top 3 por horas" del trabajo abierto.
Soluciones
Ejercicio 1
function miSome(lista, predicado) {
for (let i = 0; i < lista.length; i++) {
if (predicado(lista[i], i, lista)) return true; // corta aquí
}
return false;
}
function miEvery(lista, predicado) {
for (let i = 0; i < lista.length; i++) {
if (!predicado(lista[i], i, lista)) return false; // corta aquí
}
return true;
}
function miFind(lista, predicado) {
for (let i = 0; i < lista.length; i++) {
if (predicado(lista[i], i, lista)) return lista[i];
}
return undefined;
}
console.log(miSome(indices, vencida)); // true
console.log(miEvery(indices, (i) => responsables[i] !== null)); // true
console.log(miFind(indices, (i) => horas[i] > 10)); // 0
const encontrada = miFind(indices, (i) => horas[i] > 10);
console.log(titulos[encontrada]); // Rediseñar la sala polivalenteComentario: fíjate en que miSome y miEvery sí pueden cortar, porque el return está en el bucle de la función de orden superior, no en el callback. Esa es la diferencia con miForEach. miFind devuelve undefined cuando no encuentra nada, no -1 ni null: es coherente con lo que hace el método nativo que verás en 04-05.
Ejercicio 2
function agruparPor(lista, obtenerClave) {
return miReduce(lista, (grupos, elemento, indice) => {
const clave = obtenerClave(elemento, indice, lista);
if (!(clave in grupos)) grupos[clave] = [];
grupos[clave].push(elemento);
return grupos; // ← imprescindible
}, {});
}
const porResponsable = agruparPor(indices, (i) => responsables[i]);
console.log(porResponsable);
// { 'Iván': [ 0, 4, 5 ], Marta: [ 1, 3 ], 'Lucía': [ 2 ] }
const porEstado = agruparPor(indices, (i) => estados[i]);
console.log(porEstado);
// { 'en-curso': [ 0, 4 ], pendiente: [ 1, 2, 5 ], hecha: [ 3 ] }
// Horas abiertas por responsable
const nombres = ['Iván', 'Marta', 'Lucía'];
miForEach(nombres, (nombre) => {
const grupo = porResponsable[nombre] ?? [];
const abiertas = miReduce(miFilter(grupo, noHecha), (acc, i) => acc + horas[i], 0);
console.log(`${nombre}: ${abiertas} h abiertas · margen ${40 - abiertas} h`);
});
// Iván: 25 h abiertas · margen 15 h
// Marta: 6 h abiertas · margen 34 h
// Lucía: 14 h abiertas · margen 26 hComentario: agruparPor es una HOF que recibe un extractor de clave y devuelve un objeto de grupos. Es una de las funciones auxiliares más útiles que existen y aparece en todas las librerías de utilidades. El return grupos del reductor es el punto donde más gente se equivoca.
Ejercicio 3
function generarInforme(fuente, configuracion) {
const filtro = configuracion.filtro ?? (() => true);
const transformar = configuracion.transformar ?? ((x) => String(x));
const agregar = configuracion.agregar ?? (() => '');
const ordenar = configuracion.ordenar ?? null;
const limite = configuracion.limite ?? Infinity;
const formatoCabecera = configuracion.formatoCabecera ?? ((t, n) => `── ${t} (${n}) ──`);
let seleccionados = miFilter(fuente, filtro);
if (ordenar !== null) seleccionados = ordenarIndices(seleccionados, ordenar);
const total = seleccionados.length;
const visibles = miFilter(seleccionados, (_, indice) => indice < limite);
const ocultas = total - visibles.length;
const salida = [formatoCabecera(configuracion.titulo, total)];
miForEach(miMap(visibles, transformar), (linea) => salida.push(` ${linea}`));
if (ocultas > 0) salida.push(` … y ${ocultas} más`);
salida.push(` ${agregar(seleccionados)}`);
return miReduce(salida, (acc, l) => `${acc}\n${l}`, '').trim();
}
console.log(generarInforme(indices, {
titulo: 'Top 3 por horas',
filtro: noHecha,
ordenar: porHorasDesc,
transformar: (i) => `${horas[i]} h — ${titulos[i]} (${responsables[i]})`,
agregar: sumaHoras,
limite: 3,
formatoCabecera: (t, n) => `▓▓ ${t.toUpperCase()} · ${n} tareas abiertas ▓▓`
}));▓▓ TOP 3 POR HORAS · 5 tareas abiertas ▓▓ 14 h — Actualizar la web de reservas (Lucía) 12 h — Rediseñar la sala polivalente (Iván) 8 h — Guía de encuadernación para residentes (Iván) … y 2 más Total: 45 h
Comentario: el agregar sigue recibiendo todos los seleccionados, no solo los visibles, para que el total siga siendo el real (45 h) y no el de las tres líneas mostradas. Es una decisión de diseño que conviene hacer explícita; la contraria también sería defendible, pero hay que elegir una y documentarla. Fíjate también en el uso de _ como nombre del parámetro que no se usa en miFilter(seleccionados, (_, indice) => ...): es una convención muy extendida para decir "este argumento existe pero lo ignoro".
Conclusión
Ya tienes la herramienta que convierte código repetitivo en código configurable. Una función de orden superior recibe funciones, devuelve funciones o ambas cosas, y su valor está en separar el esqueleto fijo —recorrer, filtrar, ordenar, informar— del detalle variable que le pasas como callback. Has aprendido el vocabulario con precisión: callback, predicado, transformador, comparador, reductor, y el hecho fundamental de que es la función de orden superior, y no tú, quien decide cuándo se invoca el callback y con qué argumentos.
Has implementado a mano las cuatro operaciones fundamentales sobre listas: miForEach (efecto sobre cada elemento, sin retorno), miMap (array nuevo del mismo tamaño con cada elemento transformado), miFilter (array nuevo con los que cumplen un predicado) y miReduce (todo reducido a un valor mediante un acumulador). Saber que caben en cuatro o cinco líneas cada una desmitifica por completo los métodos nativos que estudiarás en Iteración sobre Arrays y Buscar, Ordenar y Agregar Datos: cuando los uses, sabrás exactamente qué está pasando por dentro.
Y has visto los tres patrones que se construyen encima: fábricas y currying, que fijan argumentos y producen funciones especializadas (crearPredicado, y, o, no, esfuerzoCurry); composición con componer y pipe, que encadena transformaciones pequeñas en una tubería legible —la que normaliza etiquetas según la R9—; y funciones como configuración, que dieron lugar al motor de informes de Taller Nómada: tres informes radicalmente distintos sin tocar una sola línea del motor. Junto con la advertencia final, que importa tanto como lo demás: si un bucle de cuatro líneas es más claro, escribe el bucle.
Queda una última forma de resolver problemas con funciones, y es la más desconcertante de todas: una función que se llama a sí misma. Ya la has rozado dos veces —la expresión de función con nombre de 03-02 servía para eso, y el RangeError de 03-05 apareció por una llamada que nunca paraba—. En Recursividad le pondrás método: caso base, caso recursivo, la pila de llamadas dibujada paso a paso, y el problema real que la justifica en Nómada Tareas: recorrer las subtareas anidadas de una tarea para sumar sus horas.
Curso de JavaScript: De Principiante a Avanzado
Módulo 1: Introducción a JavaScript
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
