Pregunta 1Cuando escribes orders.forEach(printOrder), ¿quién llama a printOrder?
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
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.
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
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.
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
filterItems(items, byCategory("Papelería"))— construye la condición y la pasa como 2.º argumento
category— recibe "Papelería" en la línea que la llamareturn (item) => ...— devuelve la función de condición sin comparar nada
item— recibe un elemento que le pasafilterItemsitem.category === category— compara con la category recordada
judge— guarda la función de condición que devolvióbyCategoryjudge(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").
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
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 recibe | Cómo llama al callback | Retorno del callback |
|---|---|---|
| forEach | Con cada elemento, uno a uno | No se usa |
| filterItems hecho a mano | Lo llama como judge(item) | Junta los que dan true |
| sumShipping hecho a mano | Lo llama como calcFee(order) | Se suma como envío del pedido |
Verificación de conocimientos
Responde cada pregunta una a una.
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?