Funciones flecha
En resumen: Una función flecha es una forma corta de escribir una función que pasas como valor, por ejemplo para filtrar u ordenar.
Problema
Una función entera para una comparación
Hay que ordenar los pedidos por total.
class Order { constructor(customer, total) { this.customer = customer; this.total = total; }}const orders = [ new Order("María", 75.5), new Order("Jorge", 12.9), new Order("Elena", 40),];
function byTotal(a, b) { return a.total - b.total;}
orders.sort(byTotal);
for (const o of orders) { console.log(`${o.customer}: ${o.total.toFixed(2)} €`);}Jorge: 12.90 €Elena: 40.00 €María: 75.50 €sort recibe una función de comparación. Recibe dos pedidos y devuelve un número negativo si el primero debe ir antes del segundo, un número positivo si va después, y 0 si son iguales. a.total - b.total hace exactamente eso.
Funciona. Pero para una comparación escribes una función entera con nombre, lejos del lugar donde la usas. Y solo se llama aquí. Tú quieres pasar solo la regla.
La idea
Una función flecha: solo la regla
El mismo orden, en una línea:
orders.sort((a, b) => a.total - b.total);(a, b) => a.total - b.total se llama función flecha. En otros lenguajes lo mismo se llama lambda. A la izquierda de => están los parámetros, a la derecha está el cuerpo. Es la función byTotal sin nombre, sin function y sin return.
Las formas de escribirla:
- Un solo parámetro puede ir sin paréntesis:
o => o.total > 50. Normalmente se escriben los paréntesis:(o) => .... Sin parámetros es() => .... - Un cuerpo de varias líneas va entre
{ }, y entoncesreturnes obligatorio.
const byTotal = (a, b) => a.total - b.total;const byTotalLong = (a, b) => { const diff = a.total - b.total; return diff < 0 ? -1 : diff > 0 ? 1 : 0;};const guest = () => new Order("Invitado", 0);
orders.sort(byTotalLong);for (const o of orders) { console.log(`${o.customer}: ${o.total.toFixed(2)} €`);}console.log(guest().customer);Jorge: 12.90 €Elena: 40.00 €María: 75.50 €InvitadoUna función flecha es un valor: puedes guardarla en una variable y pasarla más tarde.
Más precisamente, para curiosos
Si el cuerpo es un objeto, va entre paréntesis. Sin ellos, { se lee como el comienzo de un cuerpo, y la función devuelve undefined:
const guest = () => ({ customer: "Invitado" });const broken = () => { customer: "Invitado" };console.log(guest().customer);console.log(broken());InvitadoundefinedLa idea
Una función es un valor
¿Cómo sabe sort que (a, b) => ... es la función de comparación? No necesita saber nada más. En JavaScript una función es un valor, como un número o una cadena: puedes guardarla en una variable, pasarla a otra función y llamarla con paréntesis.
const student = (price) => price * 0.8;const minusFive = (price) => price - 5;
console.log(student(50));console.log(minusFive(50));4045student es un objeto de tipo function: typeof student es "function". Lo llamas como cualquier función: student(50). Dos reglas de precio, y ni una sola clase.
Aquí Java necesita una interfaz funcional: una interfaz con un método que dice qué tipo de regla es. JavaScript no escribe tipos y no tiene interfaces, así que no necesita nada parecido. Cualquier función se puede pasar y llamar.
const student = (price) => price * 0.8;const minusFive = (price) => price - 5;
console.log(student(50));console.log(minusFive(50));Paso a paso, en palabras
- La función flecha es un objeto de tipo function, y student apunta a él. Su cuerpo todavía no se ejecuta.
- Una segunda regla, un segundo objeto. No se escribe ni una sola clase.
- student(50) pasa 50 a la función: price es 50, y price * 0.8 da 40.
- minusFive(50): el mismo número, otra regla, 45.
Más precisamente, para curiosos
Una función flecha toma this del lugar donde está escrita, como una lambda en Java. Una función escrita con function tiene su propio this, y cuando forEach la llama, este es undefined:
class Cart { constructor(owner) { this.owner = owner; this.items = ["café", "miel"]; }
print() { this.items.forEach((item) => console.log(`${this.owner}: ${item}`)); }
printOld() { this.items.forEach(function (item) { console.log(`${this.owner}: ${item}`); }); }}
const cart = new Cart("María");cart.print();cart.printOld();María: caféMaría: mielTypeError: Cannot read properties of undefined (reading 'owner')Así que escribe una regla dentro de un método de clase como función flecha.
La idea
Los cuatro tipos de reglas
El tipo de toda regla es function, pero hay cuatro tipos de reglas. Conviene reconocerlos, porque las funciones que reciben reglas quieren un tipo concreto:
- condición: recibe un valor, devuelve
trueofalse. «¿El pedido es grande?» - transformación: recibe una cosa, devuelve otra. Un pedido en una etiqueta.
- acción: recibe un valor y hace algo con él: lo imprime, lo guarda. Su resultado no se usa.
- proveedor: no recibe nada, devuelve un objeto nuevo cuando se lo pides.
const big = (o) => o.total >= 50;const label = (o) => o.customer.toUpperCase();const show = (text) => console.log("-> " + text);const guest = () => new Order("Invitado", 0);
const order = new Order("María", 75.5);console.log(big(order));show(label(order));console.log(guest().customer);true-> MARÍAInvitadoEn Java las reglas son objetos con métodos que las combinan: las condiciones se unen con and, or y negate («no»), y las funciones se encadenan una tras otra con andThen. JavaScript no tiene esos métodos. La nueva regla es una nueva función que llama a las anteriores: (o) => big(o) && maria(o). Dos funciones una tras otra también se combinan con una función:
const withVat = (price) => price * 1.2;const rounded = (price) => Math.round(price * 100) / 100;const andThen = (f, g) => (x) => g(f(x));
const finalPrice = andThen(withVat, rounded);console.log(finalPrice(10.555));12.67andThen recibe dos funciones y devuelve una tercera: llama a la primera y pasa su resultado a la segunda.
Más precisamente, para curiosos
Una acción igualmente devuelve algo: undefined, porque ese es el resultado de console.log. console.log(show("café")) imprime «-> café» y luego undefined.
Pruébalo
Condiciones combinadas
¿Qué imprimirá el programa?
const big = (o) => o.total >= 50;const maria = (o) => o.customer === "María";const bigAndMaria = (o) => big(o) && maria(o);const bigOrMaria = (o) => big(o) || maria(o);const notBig = (o) => !big(o);
const small = new Order("María", 30);const large = new Order("Jorge", 80);
console.log(bigAndMaria(small));console.log(bigOrMaria(small));console.log(notBig(large));bigAndMaria da false, y bigOrMaria da true, porque es de María. El pedido de Jorge es de 80 €, es grande, y ! invierte la respuesta: false.Míralo
Los arrays reciben funciones
let cart = ["pan", "", "miel", " ", "azúcar"];cart = cart.filter((item) => item.trim() !== "");cart.forEach((item) => console.log(item));
const customers = [ { name: "María", city: "Granada" }, { name: "Jorge", city: "Madrid" }, { name: "Elena", city: "Granada" },];const byCity = Map.groupBy(customers, (c) => c.city);byCity.forEach((list, city) => console.log(`${city}: ${list.map((c) => c.name).join(", ")}`));panmielazúcarGranada: María, ElenaMadrid: Jorgefilter(condición)crea un array nuevo solo con los elementos para los que la condición es verdadera. El anterior no cambia: aquícarteslety simplemente apunta al nuevo. No hay ningún método que elimine elementos del propio array según una condición.forEach(acción)hace la acción con cada elemento.Map.groupBy(array, función)agrupa: la función dice la clave de cada elemento, y el resultado es unMapcon un array de los elementos para cada clave.- El
forEachde unMaprecibe una función con dos parámetros, pero en orden inverso: primero el valor, luego la clave.
{ name: "María", city: "Granada" } es un objeto sin clase: solo campos. Un Map mantiene las claves en el orden en que se añadieron, por eso Granada va antes que Madrid.
La idea
Una función por su nombre, sin paréntesis
(s) => Number(s). Entonces puedes pasar la propia función, por su nombre y sin paréntesis:const names = ["maría", "jorge"];names.forEach((name) => console.log(name));
const upper = (s) => s.toUpperCase();const parse = Number;console.log(upper("elena"));console.log(parse("42") + 1);
console.log(["1", "2", "3"].map(Number));console.log(["1", "2", "3"].map(parseInt));maríajorgeELENA43[ 1, 2, 3 ][ 1, NaN, NaN ]Java tiene una notación aparte para esto, la referencia a método con ::. En JavaScript basta con el nombre, con una trampa: la función recibe todo lo que le pasa quien la llama. map y forEach pasan el elemento, su índice y el array entero. Number solo mira el primero y funciona. parseInt recibe un segundo parámetro, la base (radix) del sistema de numeración, y le llega el índice del elemento: parseInt("2", 1) es NaN. Lo mismo pasa con console.log, que imprime todo lo que recibe:
["maría", "jorge"].forEach(console.log);maría 0 [ 'maría', 'jorge' ]jorge 1 [ 'maría', 'jorge' ]Un método del parámetro, como toUpperCase, no tiene forma corta. String.prototype.toUpperCase no se puede pasar directamente: el programa se detiene con un TypeError. Escribes (s) => s.toUpperCase().
Así que pasa por nombre solo funciones cuyos parámetros conoces. Si no, escribe una función flecha.
Míralo
Ordenar por varios campos
La función de comparación puede comprobar varios campos uno tras otro. Si la primera comparación da 0, || pasa a la siguiente: 0 es «falsy» en JavaScript.
Los estudiantes se ordenan por nota, de la más alta a la más baja, y por nombre cuando las notas son iguales:
const students = [ new Student("Juan", 5.5), new Student("Ana", 6), new Student("Pedro", 5.5), new Student("Bruno", 4.75),];students.sort((a, b) => b.grade - a.grade || a.name.localeCompare(b.name));Ana 6Juan 5.5Pedro 5.5Bruno 4.75b.grade - a.grade pone primero la nota más alta. Cuando las notas son iguales, a.name.localeCompare(b.name) compara los nombres alfabéticamente. Se lee de izquierda a derecha: «por nota al revés, y por nombre cuando son iguales».
Más precisamente, para curiosos
- Sin función,
sortconvierte los elementos en cadenas y compara las cadenas. Con números esto da un orden incorrecto:
console.log([10, 9, 1].sort());console.log([10, 9, 1].sort((a, b) => a - b));
const prices = [40, 12, 75];const sorted = prices.toSorted((a, b) => a - b);console.log(sorted, prices);[ 1, 10, 9 ][ 1, 9, 10 ][ 12, 40, 75 ] [ 40, 12, 75 ]sortordena el propio array.toSorteddevuelve un array nuevo ordenado, y el anterior se queda como estaba.
Pruébalo
¿Qué invierte reversed?
reversed recibe una función de comparación y devuelve una nueva que intercambia a y b.¿Qué imprimirá el programa?
const students = [ new Student("Juan", 5.5), new Student("Ana", 6), new Student("Pedro", 5.5), new Student("Bruno", 4.75),];
const byGradeThenName = (a, b) => a.grade - b.grade || a.name.localeCompare(b.name);const reversed = (compare) => (a, b) => compare(b, a);
students.sort(reversed(byGradeThenName));students.forEach((s) => console.log(`${s.name} ${s.grade}`));reversed invierte toda la función que recibe, y esa función es «por nota, luego por nombre». Así que se invierten las dos: las notas van de la más alta a la más baja, y Pedro va antes que Juan. Si quieres invertir solo las notas, intercambias a y b solo en la comparación de las notas, como en la tarjeta anterior.Pruébalo
Encuentra el error
Elige la línea con el error.
total es const, y la función flecha intenta cambiarlo:
TypeError: Assignment to constant variable.El error aparece en la línea 3, pero la causa está en la línea 2: el valor de un const no cambia. Con let total = 0; el programa imprime 127.
Aquí JavaScript es distinto de Java. Allí la lambda recibe una copia del valor y no puede cambiar la variable local en absoluto. En JavaScript la función flecha ve la propia variable: puede leerla y, si es let, cambiarla. Para una suma, eso sí, hay una forma más clara, reduce, en la próxima lección.
Más precisamente, para curiosos
Una función flecha ve la propia variable, no una copia. Si la variable cambia después de la función flecha, la siguiente llamada ve el valor nuevo:
let rate = 0.2;const discount = (price) => price * (1 - rate);console.log(discount(100));
rate = 0.5;console.log(discount(100));8050En Java esto está prohibido precisamente por sorpresas como esta. Si la regla tiene que recordar un valor, pásalo como parámetro.
Ejercicio
Reglas como valores
1. Ordenar por varios campos. Una clase Product con nombre, categoría y precio. Ordena los productos por categoría, dentro de una categoría del más caro al más barato, y por nombre cuando el precio es igual, con un solo sort y una función de comparación con ||. Los productos son: ratón, accesorios, 25 €; portátil, computadoras, 1299 €; teclado, accesorios, 49,90 €; tableta, computadoras, 399 €; auriculares, accesorios, 49,90 €.
Accesorios | Auriculares | 49.90 €Accesorios | Teclado | 49.90 €Accesorios | Ratón | 25.00 €Computadoras | Portátil | 1299.00 €Computadoras | Tableta | 399.00 €2. Filtrar pedidos. Añade una ciudad a Order. Escribe una función select(orders, rule) que devuelva una lista nueva solo con los pedidos para los que la regla es verdadera. Haz dos reglas, madrid y big (al menos 50 €), e imprime los pedidos para los que se cumplen las dos, los de fuera de Madrid y los «de Madrid o de al menos 100 €». Cada una de las tres reglas es una función flecha que llama a madrid y big. Los pedidos: María, Madrid, 75,50 €; Jorge, Valencia, 12,90 €; Elena, Madrid, 40 €; Diego, Granada, 120 €.
Madrid, desde 50 €: María, Madrid: 75.50 €Fuera de Madrid: Jorge, Valencia: 12.90 € Diego, Granada: 120.00 €Envío gratis (Madrid o desde 100 €): María, Madrid: 75.50 € Elena, Madrid: 40.00 € Diego, Granada: 120.00 €andThen(f, g), que devuelve una función: primero f, luego g. Úsala para encadenar los descuentos en dos órdenes: primero el de estudiante y primero el del viernes. Pruébalo con 120 € y con 15 €. ¿Importa el orden?120.00 € -> estudiante y luego viernes: 88.00 €; viernes y luego estudiante: 90.00 €15.00 € -> estudiante y luego viernes: 0.00 €; viernes y luego estudiante: 0.00 €Más tareas
- Limpiar una lista. De un array de correos, elimina los vacíos y los que no tienen
@con un solofilter, y luego imprime el resto conforEach. - Estudiantes por clase. Agrupa estudiantes por clase (
"10A","10B") conMap.groupBye imprime cada clase con los nombres que tiene. - Validadores. Haz un objeto con tres validadores como funciones flecha: texto no vacío, solo dígitos (
/^\d+$/.test(value)), longitud de hasta 10. Prueba cada uno con algunas cadenas.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Interfaces sin interface · Arrays: push, splice y sort · Map y Set · Clases anidadas y datos
- Continúa en: Métodos de arrays · Datos, switch y desestructuración · Patrones de diseño · Tests con node:test
- Ejercicios sobre el tema: JavaScript moderno