Ir al contenido
wannacode

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.

Order
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)} €`);
}
Salida
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 entonces return es 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);
Salida
Jorge: 12.90 €
Elena: 40.00 €
María: 75.50 €
Invitado

Una 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());
Salida
Invitado
undefined

La 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));
Salida
40
45

student 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
  1. La función flecha es un objeto de tipo function, y student apunta a él. Su cuerpo todavía no se ejecuta.
  2. Una segunda regla, un segundo objeto. No se escribe ni una sola clase.
  3. student(50) pasa 50 a la función: price es 50, y price * 0.8 da 40.
  4. 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();
Salida
María: café
María: miel
TypeError: 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 true o false. «¿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);
Salida
true
-> MARÍA
Invitado

En 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));
Salida
12.67

andThen 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));
Respuesta

Míralo

Los arrays reciben funciones

Los arrays tienen métodos que reciben una regla en lugar de un bucle:
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(", ")}`));
Salida
pan
miel
azúcar
Granada: María, Elena
Madrid: Jorge
  • filter(condición) crea un array nuevo solo con los elementos para los que la condición es verdadera. El anterior no cambia: aquí cart es let y 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 un Map con un array de los elementos para cada clave.
  • El forEach de un Map recibe 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

A menudo una función flecha solo pasa su parámetro a una función que ya existe: (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));
Salida
maría
jorge
ELENA
43
[ 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);
Salida
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));
Salida
Ana 6
Juan 5.5
Pedro 5.5
Bruno 4.75

b.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, sort convierte 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);
Salida
[ 1, 10, 9 ]
[ 1, 9, 10 ]
[ 12, 40, 75 ] [ 40, 12, 75 ]
  • sort ordena el propio array. toSorted devuelve un array nuevo ordenado, y el anterior se queda como estaba.

Pruébalo

¿Qué invierte reversed?

Aquí 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}`));
Respuesta

Pruébalo

Encuentra el error

El programa debería imprimir la suma de los precios. Se detiene con un error en el primer precio. ¿En qué línea está la causa?

Elige la línea con el error.

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));
Salida
80
50

En 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

Resuelve las tareas en tu editor.

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

Salida
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 €.

Salida
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 €
3. Descuentos uno tras otro. Haz tres descuentos como funciones flecha: un descuento de estudiante (−10 %), un descuento de Black Friday (−20 €) y «no menos de cero». Escribe 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?
Salida
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 solo filter, y luego imprime el resto con forEach.
  • Estudiantes por clase. Agrupa estudiantes por clase ("10A", "10B") con Map.groupBy e 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

1. ¿Cuál es una función flecha que devuelve la suma de dos precios?
2. ¿Cómo construyes la condición «grande y de María» con las funciones big y maria?
3. ¿Qué devuelve ["1", "2", "3"].map(parseInt)?
4. ¿Cómo ordena students.sort((a, b) => b.grade - a.grade || a.name.localeCompare(b.name))?
5. const total = 0; prices.forEach((p) => (total += p)); ¿Qué pasa?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·