Aprende leyendo en orden

Funciones de orden superior y callbacks — pasar y devolver funciones

Funciones que reciben funciones como argumentos y los callbacks que se les pasan. Incluye forEach, un filter hecho a mano y funciones que devuelven funciones.

A veces quieres hacer cosas un poco distintas con la misma lista: solo mostrar cada elemento, filtrarla con una condición o sumar los importes. Lo que tienen en común es la estructura del bucle; lo único que cambia es lo que se hace con cada elemento.

Este artículo trata las funciones de orden superior, que reciben funciones como argumentos, y los callbacks, las funciones que se pasan y se llaman.

Pasar la lógica como argumento — callbacks y forEach

Supón que quieres mostrar una lista de pedidos uno por uno. Con for...of funciona, pero tienes que escribir el mismo bucle cada vez que muestras una lista, aunque lo único que cambia es cómo se muestra cada elemento.

Una función de orden superior (una función que recibe una función como argumento o que devuelve una función) te permite separar la estructura del bucle de lo que se hace con cada elemento. La función que se pasa como argumento y que llama la función que la recibió se llama callback. El método de arrays forEach llama al callback que le pasas una vez por cada elemento.

const orders = [
  { id: "A-1101", item: "Cuaderno", amount: 480 },
  { id: "A-1102", item: "Bolígrafo", amount: 150 },
  { id: "A-1103", item: "Notas adhesivas", amount: 320 },
];

// La función se encarga de mostrar un solo elemento
const printOrder = (order) => {
  console.log(`${order.id} ${order.item} ${order.amount} yenes`);
};

// Pasa solo el nombre de la función. Quien la llama es forEach
orders.forEach(printOrder);
// A-1101 Cuaderno 480 yenes
// A-1102 Bolígrafo 150 yenes
// A-1103 Notas adhesivas 320 yenes
El mismo printOrder, llamado tres veces
1.er elemento:A-1101order recibeA-1101Muestra A-1101Cuaderno 480 yenes2.º elemento:A-1102order recibeA-1102Muestra A-1102Bolígrafo150 yenes3.er elemento:A-1103order recibeA-1103Muestra A-1103Notas adhesivas320 yenes
Pasas una sola función, printOrder, y quien la llama es forEach. Entre una llamada y otra, lo único que cambia es el elemento que recibe order.

La línea orders.forEach(printOrder) no tiene ni condición de bucle ni índice. Para cambiar cómo se muestran los elementos, solo editas printOrder, y puedes pasar esa misma función a otras listas sin cambiarla.

forEach no se puede detener a la mitad

Dentro de forEach no puedes escribir break, y un return solo termina el callback de ese elemento: los demás elementos se siguen procesando. Si quieres cortar el bucle en cuanto encuentres el elemento que buscas, usa for...of con break.

Muestra una lista de consultas de soporte una por una. tickets ya está declarado.

① Escribe una función printTicket que muestre una consulta con el formato "[T-01] ¿Dónde está mi pedido? (resuelta)". Si todavía no está resuelta, muestra (pendiente).

② Pasa printTicket a forEach para mostrar todas las consultas.

③ Cuenta las consultas resueltas y muestra el resultado con el formato "Resueltas: N".

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Recibir una condición como función — un filter hecho a mano y los booleanos

Supón que quieres filtrar una lista con distintas condiciones: primero solo los productos con stock, después solo los de 3000 yenes o más. Si escribes un bucle aparte para cada condición, terminas con copias casi idénticas, hasta en el array que junta los resultados y la línea del push.

Lo único que cambia es la línea de la condición, así que recíbela como una función de condición y junta solo los elementos para los que devuelve true. Qué se le pasa a un callback, y en qué orden, lo decide la función que lo recibe y lo llama. El filterItems de abajo le pasa a la función de condición un solo elemento.

const products = [
  { name: "Tapete de escritorio", stock: 4 },
  { name: "Soporte para monitor", stock: 0 },
  { name: "Organizador de cables", stock: 9 },
];

// Recibe una función de condición y junta solo los elementos para los que devuelve true
function filterItems(items, judge) {
  const kept = [];
  items.forEach((item) => {
    if (judge(item)) {
      kept.push(item);
    }
  });
  return kept;
}

const inStock = filterItems(products, (product) => product.stock > 0);
console.log(inStock.length);          // 2
console.log(inStock[0].name);         // Tapete de escritorio
El valor devuelto decide a dónde va cada elemento
Tapete deescritorio con4 en stockCompruebastock > 04 > 0 estrueEntra enkeptSoporte paramonitor con0 en stockCompruebastock > 00 > 0 esfalseNo entraen kept
La condición devuelve true para el producto con 4 en stock y false para el que tiene 0, así que solo el primero entra en kept. Que un elemento se quede o no depende del valor que devuelve la función que pasaste.

La palabra "stock" no aparece en ninguna parte dentro de filterItems. Si más adelante quieres solo los productos con mucho stock, basta con cambiar la condición de la línea que llama a product.stock >= 5, sin tocar filterItems para nada.

Cuenta los envíos de un calendario de entregas que cumplen una condición. shipments ya está declarado.

① Escribe countIf, que recibe una función de condición y devuelve cuántos elementos la cumplen.

② Muestra cuántos envíos salen el mismo día.

③ Muestra cuántos envíos tienen un costo de 4000 yenes o más.

④ Guarda en una variable isNextDay una función que compruebe los envíos que no salen el mismo día, pásala a countIf y muestra el resultado.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Pasar una condición que construiste — funciones de orden superior que devuelven funciones

A veces le pasas a filterItems condiciones que solo se diferencian en la categoría con la que comparan: "solo papelería", "solo vajilla", etc. Si escribes la expresión de la condición en cada llamada, cambiar el nombre de la propiedad que comparas te obliga a editar todas esas llamadas.

La otra forma de función de orden superior es una función que devuelve una función. Tiene la misma forma que las fábricas de funciones del artículo anterior: construyes byCategory, que recibe una categoría y devuelve una función de condición. La condición devuelta recuerda category como closure, así que puedes pasarla directamente a filterItems.

const items = [
  { name: "Cuaderno", category: "Papelería" },
  { name: "Taza", category: "Vajilla" },
  { name: "Bolígrafo", category: "Papelería" },
];

// Recibe una categoría y devuelve una función que comprueba si un elemento pertenece a ella
function byCategory(category) {
  return (item) => item.category === category;
}

// Todavía no se compara nada; lo que devuelve es la función de condición
console.log(typeof byCategory("Papelería"));   // function

// Pasa la condición devuelta directamente al filterItems de la sección anterior
console.log(filterItems(items, byCategory("Papelería")).length);   // 2
console.log(filterItems(items, byCategory("Vajilla")).length);     // 1
Dónde se define cada valor de la condición
Nivel más externo (fuera de cualquier llave)
  • filterItems(items, byCategory("Papelería")) — construye la condición y la pasa como 2.º argumento
Dentro de byCategory
  • category — recibe "Papelería" en la línea que la llama
  • return (item) => ... — devuelve la función de condición sin comparar nada
Dentro de la función de condición devuelta
  • item — recibe un elemento que le pasa filterItems
  • item.category === category — compara con la category recordada
Dentro de filterItems
  • judge — guarda la función de condición que devolvió byCategory
  • judge(item) — pasa los elementos uno por uno y llama a la condición
category se define en la línea que llama a byCategory, e item se define dentro de filterItems. El lugar donde se construye la condición y el lugar donde se llama son distintos.

La línea que ejecuta byCategory("Papelería") todavía no compara nada; la comparación ocurre recién cuando se llama a judge(item) dentro de filterItems. Con 3 productos, la misma condición se llama 3 veces, y category sigue siendo "Papelería" mientras solo cambia item.

Sin paréntesis, se quedan todos los elementos

Si escribes filterItems(items, byCategory), cada producto se pasa a byCategory como category, y lo que vuelve como resultado de la condición es una función. Una función es truthy (cualquier valor que no sea falsy), así que se quedan todos los productos. Lo que tienes que pasar es la condición que devuelve byCategory("Papelería").

Filtra los pedidos que califican para el envío gratis exclusivo para miembros. orders y filterItems ya están declarados.

① Escribe both, que recibe dos condiciones y devuelve una condición que devuelve true solo cuando se cumplen las dos.

② Guarda dos condiciones en variables: una para los pedidos de 5000 yenes o más y otra para los miembros.

③ Combina las condiciones de ② con both, pasa el resultado a filterItems y muestra el ID de cada pedido que quede.

④ Aplica la condición de ③ al segundo pedido y muestra el resultado.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Una sola estructura para todo — una función general con lógica intercambiable

Supón que escribiste dos funciones que suman los costos de envío, una para el envío estándar y otra para el exprés. El bucle y la variable que guarda el total son iguales; lo único que cambia es la expresión que calcula el costo de cada pedido. Así es fácil corregir una y olvidarte de corregir la otra.

Convierte la parte que cambia en un parámetro de tipo función y deja una sola copia de la estructura. sumShipping recibe un array de pedidos y una función que calcula el costo, le pasa cada pedido y va sumando los valores que devuelve. Quien llama escribe solo la expresión del costo, y la función que recibe se queda solo con el bucle y el total.

const shippingOrders = [{ id: "A-1101", total: 6200 }, { id: "A-1102", total: 3400 }];

// Una sola estructura. Solo el cálculo del costo se recibe como función
function sumShipping(orders, calcFee) {
  let total = 0;
  for (const order of orders) {
    total = total + calcFee(order);
  }
  return total;
}

const normalFee = (order) => (order.total >= 5000 ? 0 : 500);
const expressFee = () => 800;   // No usa order, así que el parámetro se puede omitir
const flatFee = () => 300;

console.log(sumShipping(shippingOrders, normalFee));    // 500
console.log(sumShipping(shippingOrders, expressFee));   // 1600
console.log(sumShipping(shippingOrders, flatFee));      // 600
Tres funciones de costo, una sola estructura
sumShipping(orders,calcFee): una solacalcFee recibenormalFeecalcFee recibeexpressFeecalcFee recibeflatFee0 desde 5000yenes; si no, 500800 sin importarel pedido300 sin importarel pedidoEl total es 500El total es 1600El total es 600
Según la función que se pase como calcFee, los mismos dos pedidos suman 500, 1600 o 600. El código dentro de sumShipping es idéntico las tres veces.

Incluso un cambio como poner un tope al total se resuelve con una sola edición en sumShipping, y se aplica a los tres tipos de costo. La tabla de abajo muestra cómo cada una de las tres funciones que reciben callbacks en este artículo llama a su callback y usa el valor que devuelve.

Función que recibeCómo llama al callbackRetorno del callback
forEachCon cada elemento, uno a unoNo se usa
filterItems hecho a manoLo llama como judge(item)Junta los que dan true
sumShipping hecho a manoLo llama como calcFee(order)Se suma como envío del pedido

A partir de los turnos de un empleado de medio tiempo, muestra el pago de cada turno y calcula el total. shifts ya está declarado.

① Escribe payReport, que recibe los turnos y una función que calcula el pago, muestra cada uno con el formato "N horas: N yenes" y después devuelve el total.

② Pasa un cálculo de 1100 yenes por hora y muestra el total devuelto con el formato "Total: N yenes".

③ Pasa un cálculo en el que solo los turnos nocturnos se pagan a 1.25 veces la tarifa, y muestra el total con el mismo formato.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Cuando escribes orders.forEach(printOrder), ¿quién llama a printOrder?

Pregunta 2En el filterItems hecho a mano, ¿qué pasa con un elemento cuando la función de condición que pasaste devuelve false?

Pregunta 3Si pasas byCategory sin paréntesis, como en filterItems(items, byCategory), ¿qué contiene el array que se devuelve?