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, filter y reduce de 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

  1. Qué es una función de orden superior
  2. Callbacks: el vocabulario
  3. miForEach: hacer algo con cada elemento
  4. miMap: transformar cada elemento
  5. miFilter: quedarse con algunos
  6. miReduce: reducirlo todo a un valor
  7. Encadenar las cuatro sobre el backlog
  8. Funciones que devuelven funciones: fábricas y currying
  9. Composición de funciones: componer y pipe
  10. Funciones como configuración
  11. Ejemplo integrador: el motor de informes de Taller Nómada
  12. Una nota sobre legibilidad
  13. Errores Comunes y Consejos
  14. Ejercicios
  15. Conclusión

  1. 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"]

  1. 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 segundo

Por 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.

  1. miForEach: hacer algo con cada elemento

El 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).

  1. miMap: transformar cada elemento

map 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ía

Tres 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.

  1. miFilter: quedarse con algunos

filter 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 h

Este 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

  1. miReduce: reducirlo todo a un valor

reduce 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:

const total = miReduce(horas, (acc, h) => acc + h, 0);
console.log(total);   // 48
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);   // 124

Dos advertencias sobre reduce:

  1. El reductor debe devolver siempre el acumulador. Olvidar el return es el error número uno; el acumulador pasa a ser undefined y todo revienta en la vuelta siguiente.
  2. Da siempre un valor inicial. Sin él, el nativo usa el primer elemento como acumulador y falla con arrays vacíos. Nuestra miReduce simplemente devolvería undefined.
flowchart LR
    A["inicial: 0"] --> B["+12 → 12"] --> C["+6 → 18"] --> D["+14 → 32"]
    D --> E["+3 → 35"] --> F["+8 → 43"] --> G["+5 → 48"]

  1. 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.

  1. 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));    // 15

La 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.

  1. Composición de funciones: componer y pipe

Si 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-carpinteria

Componer 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ía

Y 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-manual

Fí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'"]

  1. 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.

  1. 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.

  1. 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.

miFilter(indices, esDeIvan(0));   // ✗ pasa `true`, no una función
miFilter(indices, esDeIvan);      // ✓

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 undefined

Con 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): true si al menos uno cumple; corta en cuanto encuentra uno.
  • miEvery(lista, predicado): true si todos cumplen; corta en cuanto falla uno.
  • miFind(lista, predicado): devuelve el primer elemento que cumple, o undefined.

Ú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) => string que 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 polivalente

Comentario: fíjate en que miSome y miEvery 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 h

Comentario: 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

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados