Métodos de arrays
En resumen: Los métodos de arrays procesan los datos como una serie de pasos: filter criba, map transforma, toSorted ordena y reduce construye un único resultado.
Problema
Un informe con un bucle
Ahora los pedidos también tienen una ciudad:
class Order { constructor(customer, city, total) { this.customer = customer; this.city = city; this.total = total; }}const orders = [ new Order("María", "Madrid", 75.5), new Order("Jorge", "Valencia", 12.9), new Order("Elena", "Madrid", 40), new Order("Diego", "Granada", 120), new Order("Ana", "Valencia", 64),];Quieres los clientes con pedidos de al menos 50 €, en orden alfabético:
const result = [];for (const o of orders) { if (o.total >= 50) { result.push(o.customer); }}result.sort();console.log(result);[ 'Ana', 'Diego', 'María' ]Ocho líneas, y la pregunta es una sola. El código dice cómo: crea una lista vacía, recorre, comprueba, añade, ordena. Para entender qué hace, tienes que ejecutarlo en tu cabeza. Añade «solo los tres primeros» o «sin repetidos», y el bucle crecerá.
La idea
Una cadena de pasos
El mismo informe, escrito como pasos:
const result = orders .filter((o) => o.total >= 50) .map((o) => o.customer) .toSorted();console.log(result);[ 'Ana', 'Diego', 'María' ]Se lee de arriba abajo: toma los pedidos, quédate con los de al menos 50 €, toma el cliente, ordena. Cada paso es un método de array y recibe una función flecha de la lección anterior:
filter(condición)se queda solo con los elementos para los que la condición es verdadera;map(función)convierte cada elemento en otro: crea una cadena a partir de unOrder;toSorted()ordena alfabéticamente, ytoSorted(función de comparación)según tu regla.
Cada método devuelve un array nuevo, y sobre él se llama al siguiente. Por eso los pasos se alinean en una cadena. El array orders no cambia.
Más precisamente, para curiosos
- El antiguo
sortordena el propio array y lo devuelve.toSortedes el más nuevo y devuelve un array nuevo. Verás por qué importa la diferencia en la tarjeta «Encuentra el error». - Cada paso recorre el array entero antes de que empiece el siguiente. Una cinta transportadora por la que los elementos avanzan de uno en uno es lo que te da un iterador. De él trata la siguiente tarjeta.
Míralo
Qué pasa el filtro
values(). El iterador tiene los mismos filter y map, pero es una cinta transportadora: da los valores de uno en uno, cuando alguien los pide. toArray() los reúne en un array.const prices = [12, 75, 40, 130, 58];const labels = prices .values() .filter((p) => p >= 50) .map((p) => `${p} €`) .toArray();console.log(labels);Paso a paso, en palabras
- Cinco precios. values() da un iterador: un camino por el que todavía no ha pasado nada.
- 12 no pasa p >= 50 y se queda fuera. Nunca llega a map.
- 75 pasa filter, map lo convierte en el texto «75 €», toArray() lo reúne.
- 40 también se queda fuera. Cada elemento recorre todo el camino antes de que salga el siguiente.
- 130 y 58 pasan. El array prices no cambia.
- El resultado es un array nuevo con tres cadenas.
Los cinco precios avanzan de uno en uno. El 12 llega a filter y se queda fuera. El 75 pasa, map lo convierte en el texto «75 €» y toArray() lo recoge. Después el 40 se queda fuera, y el 130 y el 58 pasan.
Cada elemento hace todo el recorrido antes de que empiece el siguiente. Y nada empieza hasta que toArray() pide los valores.
Más precisamente, para curiosos
Esto se llama evaluación perezosa. Los métodos del iterador solo anotan lo que hay que hacer. Míralo con un print en cada paso:
const check = (p) => { console.log("filter " + p); return p >= 50;};const double = (p) => { console.log("map " + p); return p * 2;};
console.log([12, 75, 40].filter(check).map(double));
const stream = [12, 75, 40].values().filter(check).map(double);console.log("El iterador está listo, todavía no pasó nada por él.");console.log(stream.toArray());filter 12filter 75filter 40map 75[ 150 ]El iterador está listo, todavía no pasó nada por él.filter 12filter 75map 75filter 40[ 150 ]- El array (el primer
console.log) lo hace todo enseguida y paso a paso:filterrecorre los tres precios, y solo despuésmaprecorre el que queda. - El iterador está listo antes de que un solo elemento haya pasado por él. Después
map 75llega justo después defilter 75: los elementos no esperan a que todos pasen porfilter. - Por eso puedes tomar solo los primeros valores del iterador con
take, sin comprobar el resto:
const check = (p) => { console.log("filter " + p); return p >= 50;};
console.log([75, 130, 58, 12, 99].values().filter(check).take(2).toArray());filter 75filter 130[ 75, 130 ]Para las tareas de todos los días basta con el array, y es más fácil de leer. El iterador es para muchos datos, o cuando solo necesitas los primeros.
La idea
Suma, cantidad, media
El resultado puede ser un número en lugar de una lista. La suma de los números pares:
const numbers = [1, 2, 3, 4, 5];const sum = numbers.filter((n) => n % 2 === 0).reduce((total, n) => total + n, 0);console.log(`Suma de los números pares: ${sum}`);Suma de los números pares: 6reduce(función, inicio) reduce el array a un único valor. Empieza desde 0, y la función recibe la suma acumulada y el siguiente elemento y devuelve la nueva suma.Lo mismo para los pedidos:
const big = orders.filter((o) => o.total >= 50).length;const revenue = orders.reduce((sum, o) => sum + o.total, 0);const average = orders.length > 0 ? revenue / orders.length : 0;const maxTotal = Math.max(...orders.map((o) => o.total));
console.log(`Pedidos de desde 50 €: ${big}`);console.log(`Ingresos: ${revenue.toFixed(2)} €`);console.log(`Media: ${average.toFixed(2)} €`);console.log(`Mayor: ${maxTotal.toFixed(2)} €`);Pedidos de desde 50 €: 3Ingresos: 312.40 €Media: 62.48 €Mayor: 120.00 €- La
lengthdel array filtrado es la cantidad. - La media es la suma dividida entre la cantidad. Si no hay pedidos,
0 / 0esNaN(«no es un número»), de ahí la comprobaciónorders.length > 0. Math.max(...array)da el número más grande....despliega el array en parámetros separados:Math.max(75.5, 12.9, ...). Si el array está vacío, el resultado es-Infinity. Qué hacer con un resultado que puede faltar es la siguiente idea.
Más precisamente, para curiosos
Con un array vacío:
const prices = [];console.log(prices.reduce((sum, p) => sum + p, 0));console.log(Math.max(...prices));console.log(0 / prices.length);0-InfinityNaNSin valor inicial, reduce sobre un array vacío se detiene con TypeError: Reduce of empty array with no initial value. Así que escribe siempre el valor inicial.
La idea
Ordenar, recortar, sin repetidos
const ratings = [5, 3, 5, 4, 2, 4, 5];console.log([...new Set(ratings)].toSorted((a, b) => a - b));
const top = orders .toSorted((a, b) => b.total - a.total) .slice(0, 3) .map((o) => o.customer);console.log(top);
Array.from({ length: 3 }, (_, i) => i + 1).forEach((i) => console.log(`Mesa ${i}`));[ 2, 3, 4, 5 ][ 'Diego', 'María', 'Ana' ]Mesa 1Mesa 2Mesa 3new Set(ratings)quita los repetidos: en un conjunto cada valor está una sola vez.[...set]lo vuelve a convertir en un array.toSorted((a, b) => b.total - a.total)ordena empezando por el pedido más grande, con una función de comparación de la lección anterior.slice(0, 3)toma los elementos desde el índice 0 hasta el índice 3, sin incluirlo: los tres primeros. Junto con la ordenación, eso es un «top 3».- JavaScript no tiene
range.Array.from({ length: 3 }, (_, i) => i + 1)crea un array con 3 elementos llamando a la función para cada índicei: 1, 2, 3._es el nombre de un parámetro que no necesitamos.
Más precisamente, para curiosos
Un Set mantiene los valores en el orden en que se añadieron, así que sin ordenar obtienes el orden de primera aparición:
const ratings = [5, 3, 5, 4, 2, 4, 5];console.log([...new Set(ratings)]);[ 5, 3, 4, 2 ]Pruébalo
¿Qué llega al final?
¿Qué imprimirá el programa?
const drinks = ["café", "té", "café", "vino", "té", "jugo"];const result = [...new Set(drinks)] .toSorted() .slice(0, 3) .map((d) => d.toUpperCase());console.log(result);new Set deja café, té, vino, jugo. toSorted() los ordena alfabéticamente: café, jugo, té, vino. slice(0, 3) se queda con los tres primeros, y map los pasa a mayúsculas.La idea
undefined: un resultado que puede faltar
¿Cuál es el pedido más grande? Si el array está vacío, no hay respuesta. Entonces el resultado es undefined: «ningún valor». Pero quien llama tiene que acordarse de comprobarlo. Si se le olvida, el programa se detiene con un TypeError en algún sitio más adelante.
Java tiene para esto Optional: una caja que o bien contiene un valor, o bien está vacía. JavaScript no tiene una caja así, pero tiene dos operadores que hacen corta la comprobación:
const biggest = orders.toSorted((a, b) => b.total - a.total)[0];if (biggest !== undefined) { console.log(`Mayor: ${biggest.customer}`);}
const huge = orders.find((o) => o.total > 500);console.log(huge !== undefined);console.log(huge?.customer ?? "no existe ese pedido");Mayor: Diegofalseno existe ese pedido[0]de un array vacío esundefined.find(condición)da el primer elemento para el que la condición es verdadera, oundefined.huge?.customer: sihugeesundefinedonull, toda la expresión esundefined, en lugar de que el programa se detenga. Esto se llama encadenamiento opcional.x ?? ydaysixesundefinedonull, yxen caso contrario.
Tus propias funciones normalmente devuelven null cuando no tienen respuesta: «falta a propósito». ?? también funciona con él:
function nickname(user) { // Devuelve el apodo, o null si la persona no tiene. return user === "maria" ? "Mimi" : null;}const maria = nickname("maria");if (maria !== null) { console.log(`Apodo: ${maria}`);}
const jorge = nickname("jorge");console.log(jorge ?? "sin apodo");Apodo: Mimisin apodoMás precisamente, para curiosos
- Sin
?.:
const huge = orders.find((o) => o.total > 500);console.log(huge.customer);TypeError: Cannot read properties of undefined (reading 'customer')x || ytambién da un valor de reserva, pero trata una cadena vacía y el0como si faltaran.??no (la tercera línea es una cadena vacía):
console.log(null ?? "sin apodo");console.log("" || "sin apodo");console.log("" ?? "sin apodo");console.log(0 ?? 5);sin apodosin apodo
0someyeveryresponden contrueofalsesi alguno o todos los elementos cumplen la condición:
console.log(orders.some((o) => o.total > 100));console.log(orders.every((o) => o.total >= 10));truetruePruébalo
Encuentra el error
Elige la línea con el error.
sort no crea un array nuevo. Ordena el propio prices y lo devuelve, así que sorted y prices son el mismo array:
Más caro: 130Primero de la lista: 130La solución es toSorted en la línea 2. Devuelve un array nuevo ordenado, y prices sigue siendo 12, 75, 40, 130.
El iterador de values(), en cambio, se recorre una sola vez, como un stream en Java. El segundo toArray() da un array vacío:
const prices = [12, 75, 40, 130];const expensive = prices.values().filter((p) => p >= 50);console.log(expensive.toArray());console.log(expensive.toArray());[ 75, 130 ][]La idea
Agrupar y unir
¿Cuántos pedidos hay de cada ciudad, y cuáles son los ingresos?
const byCity = Map.groupBy(orders, (o) => o.city);const cities = [...byCity.keys()].toSorted();
const countByCity = Object.fromEntries(cities.map((city) => [city, byCity.get(city).length]));console.log(countByCity);
for (const city of cities) { const sum = byCity.get(city).reduce((total, o) => total + o.total, 0); console.log(`${city}: ${sum.toFixed(2)} €`);}
const customers = orders .map((o) => o.customer) .toSorted() .join(", ");console.log(customers);{ Granada: 1, Madrid: 2, Valencia: 2 }Granada: 120.00 €Madrid: 115.50 €Valencia: 76.90 €Ana, Diego, Elena, Jorge, MaríaMap.groupBy(array, función)da unMap: para cada ciudad, un array de sus pedidos.[...byCity.keys()]crea un array con las claves, ytoSorted()las ordena alfabéticamente.Object.fromEntries(pares)crea un objeto a partir de pares[clave, valor]. Aquí la clave es la ciudad, y el valor es la cantidad de sus pedidos (length).join(", ")une las cadenas en una sola, con una coma entre ellas.
Un Map mantiene las claves en el orden en que se añadieron: Madrid, Valencia, Granada. Por eso las ciudades se ordenan explícitamente.
Más precisamente, para curiosos
- Para cada ciudad, un array de los nombres. Aquí las ciudades van en el orden en que se añadieron:
const namesByCity = new Map();for (const [city, list] of Map.groupBy(orders, (o) => o.city)) { namesByCity.set(city, list.map((o) => o.customer));}console.log(namesByCity);Map(3) { 'Madrid' => [ 'María', 'Elena' ], 'Valencia' => [ 'Jorge', 'Ana' ], 'Granada' => [ 'Diego' ]}- Si una clave se repite en los pares para
Object.fromEntries, gana el último valor, sin ningún error (en JavatoMaplanza una excepción):
const lastByCity = Object.fromEntries(orders.map((o) => [o.city, o.customer]));console.log(lastByCity);{ Madrid: 'Elena', Valencia: 'Ana', Granada: 'Diego' }Ejercicio
Informes con métodos de arrays
ask, como antes.1. Ventas por categoría. Una clase Sale con un producto, una categoría, una cantidad y un precio unitario, y un método revenue() (cantidad por precio). Agrupa por categoría con Map.groupBy, suma cada grupo con reduce, imprime las categorías en orden alfabético y los ingresos totales. Las ventas: café en grano, bebidas, 12 a 18,50 €; té, bebidas, 30 a 4,20 €; chocolate, dulces, 40 a 2,80 €; galletas, dulces, 25 a 3,10 €; miel, comida, 8 a 12 €; otra vez café en grano, 5 a 18,50 €.
Bebidas: 440.50 €Comida: 96.00 €Dulces: 189.50 €Total: 726.00 €2. Los 3 mejores productos. Con las mismas ventas, encuentra los tres productos con más ingresos. El café está en dos ventas, así que primero agrupa por producto y suma, y luego ordena las entradas del mapa por valor. Pista: [...byProduct].toSorted((a, b) => b[1] - a[1]).slice(0, 3): cada elemento de un Map es un par [producto, ingresos].
Café en grano: 314.50 €Té: 126.00 €Chocolate: 112.00 €3. Nota media. Lee una línea de notas separadas por espacios. Imprime la media, la más alta y cuántas son excelentes (5,50 o más). Pista: (await ask("")).split(" ").map(Number).
4.50 5.75 6.00 3.00 5.50Media: 4.95Máxima: 6.00Excelentes: 34. Las palabras de un texto. Lee una línea de texto e imprime sus palabras sin repetidos, en orden alfabético, en minúsculas y separadas por comas. Divide por todo lo que no sea una letra: split(/[^\p{L}]+/u), y elimina las cadenas vacías.
Café con leche, café sin azúcar. ¡Té con miel!azúcar, café, con, leche, miel, sin, téMás tareas
- La palabra más larga. Con
reduce, encuentra la palabra más larga de una frase. ¿Qué pasará con una línea vacía sin valor inicial? - Media por grupo. Estudiantes con un nombre, un grupo y una nota: agrúpalos con
Map.groupBye imprime la nota media de cada grupo. - Pedidos grandes y pequeños. Con
some, comprueba si hay un pedido de más de 1000 €, y coneverysi todos son de al menos 10 €.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Arrays: push, splice y sort · Map y Set · Clases anidadas y datos · Funciones flecha
- Continúa en: Datos, switch y desestructuración · Tests con node:test · Bases de datos · Reflexión
- Ejercicios sobre el tema: JavaScript moderno