Aprende leyendo en orden

reduce — reducir un array a un solo valor

Aprende reduce, que combina un array en un solo valor. Verás el acumulador y el valor inicial, sumas, máximos, agrupación y el TypeError con arrays vacíos.

Si necesitas el total de las líneas de un pedido, map mantiene el número de elementos y filter solo quita algunos, así que en ambos casos lo que obtienes es un array. Ninguno de los dos sirve para combinar un array en un solo valor, como un total o un máximo.

En este artículo verás reduce, que combina un array en un solo valor, y el valor inicial desde el que empieza.

Combinar un array en un solo valor — reduce y el valor inicial

Supón que quieres mostrar en pantalla el total de un carrito de compras. Con forEach, la variable que guarda el total tiene que quedar fuera del bucle, donde cualquier línea posterior podría sobrescribirla. Y como su valor cambia con cada suma, tienes que declararla con let en lugar de const.

El método de array reduce (un método que pasa los elementos a un callback uno por uno y devuelve solo el último valor de retorno) recibe un callback cuyo primer parámetro es el acumulador (el parámetro que guarda el valor devuelto en la llamada anterior). A diferencia de map y filter, el elemento va en segundo lugar. En el segundo argumento del propio reduce escribes el valor inicial (el valor que tiene el acumulador en la primera llamada).

const cartItems = [
  { name: "Soporte para laptop", price: 3200 },
  { name: "Hub USB", price: 2480 },
  { name: "Cable HDMI", price: 1180 },
];

// El primer parámetro es el valor de retorno de la llamada anterior; el segundo, el elemento actual
const addPrice = (total, item) => total + item.price;

// El segundo argumento, 0, es el total en la primera llamada
const totalPrice = cartItems.reduce(addPrice, 0);
console.log(totalPrice);          // 6860

// Lo que se devuelve es un solo valor, no un array
console.log(typeof totalPrice);   // number
total se reemplaza en cada llamada
Llamada 1:total vale 0Devuelve0 + 3200Llamada 2:total vale 3200Devuelve3200 + 2480Llamada 3:total vale 5680Devuelve5680 + 1180totalPricevale 6860El último retornose guarda tal cual
El 3200 que devuelve la llamada 1 llega como total a la llamada 2. El 6860 que devuelve la última llamada es el resultado de reduce.

No necesitas una variable propia para el total parcial, porque reduce se encarga de pasar el valor de una llamada a la siguiente. Y lo que se pasa no tiene que ser un número: también puede ser un elemento o un objeto.

A partir de una lista de pedido de artículos de oficina, calcula la cantidad total y el costo total del pedido. orderLines ya está declarado, con un producto por línea.

① Muestra la cantidad total.

② Muestra el costo total, donde el costo de cada línea es precio unitario × cantidad.

③ Muestra el costo promedio por producto, que es el total de ② dividido entre el número de productos.

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

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Quedarse con el mayor — pasar un número o un elemento

Supón que quieres saber cuál de los videos que subiste tiene más reproducciones. Esta vez no sumas nada: comparas valores y solo pasas el mayor a la siguiente llamada. La pregunta es con qué valor debe empezar el acumulador.

El acumulador puede guardar un número o el propio elemento. Si pasas 0 como segundo argumento, lo que va de una llamada a otra es un número; si pasas videos[0], es el propio elemento. Usa como valor inicial el mismo tipo de valor que devuelve el callback.

const videos = [
  { title: "SQL básico #1", views: 8200 },
  { title: "Linux básico #3", views: 15400 },
  { title: "Fundamentos de Git", views: 9700 },
];

// El valor inicial es un número, así que compara y devuelve números
const maxViews = videos.reduce((max, video) => Math.max(max, video.views), 0);
console.log(maxViews);         // 15400

// El valor inicial es un elemento, así que compara y devuelve elementos
const topVideo = videos.reduce(
  (top, video) => (video.views > top.views ? video : top),
  videos[0],
);
console.log(topVideo.title);   // Linux básico #3
Lo que pones al principio se pasa hasta el final
Valor inicial 0(un número)Compara maxcon video.viewsDevuelve elnúmero mayormaxViewsvale 15400Valor inicialvideos[0]Compara top.viewscon video.viewsDevuelve elelemento mayorPuedes leertopVideo.title
La fila de arriba solo pasa un número; la de abajo pasa el elemento tal cual. Si también necesitas el título, usa un elemento como valor inicial.

Las dos llamadas a reduce comparan views; lo único que cambia es lo que se pasa a la siguiente llamada. Si a la segunda le dieras 0 como valor inicial, top.views sería undefined en la primera llamada, así que la comparación daría false cada vez y se devolvería 0 hasta el final.

Un valor inicial de 0 falla con números negativos

En un array que puede tener valores negativos, como un registro de temperaturas, si empiezas desde 0, el 0 sigue siendo el mayor aunque el máximo real sea -3, y el resultado es un valor que no está en el array. Si comparas los elementos entre sí y empiezas desde el primer elemento del array, el rango de los valores deja de importar.

Con las ventas y los costos de este mes de cada sucursal, busca las sucursales con la mayor y la menor ganancia. stores ya está declarado, y las cifras están en miles de yenes.

① Muestra el nombre de la sucursal con la mayor ganancia (ventas − costos).

② Muestra el nombre de la sucursal con la menor ganancia.

③ Muestra la ganancia de la sucursal de ②.

④ Muestra la diferencia de ganancia entre la sucursal de ① y la de ②.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Reducir conteos a un objeto — agrupación

Supón que quieres saber cuántas consultas de soporte hay en cada categoría. A diferencia de un total, el resultado no es un solo número: necesitas un valor por categoría. Podrías crear un objeto vacío fuera del bucle e ir sumando con forEach, pero entonces el objeto del recuento vuelve a quedar fuera del bucle, donde cualquier línea podría sobrescribirlo.

También puedes usar {} como valor inicial. En ese caso, el acumulador es el objeto donde se lleva el recuento, y el callback actualiza una clave y luego devuelve ese objeto. Leer una clave que todavía no existe da undefined, así que añade ?? 0 para empezar a contar desde 0.

const tickets = [
  { subject: "Quiero devolver un producto", category: "Devoluciones" },
  { subject: "Mi pedido no ha llegado", category: "Envíos" },
  { subject: "Necesito otra etiqueta de envío", category: "Envíos" },
];

// Suma 1 al conteo de la categoría de cada consulta
const countByCategory = tickets.reduce((counts, ticket) => {
  counts[ticket.category] = (counts[ticket.category] ?? 0) + 1;
  return counts;          // Pasa el counts actualizado a la siguiente llamada
}, {});                   // Este {} es counts en la primera llamada

console.log(JSON.stringify(countByCategory));  // {"Devoluciones":1,"Envíos":2}
console.log(countByCategory["Envíos"]);        // 2
Las 3 llamadas actualizan el mismo objeto counts
Llamada 1:counts es {}Pone Devolucionesen 1 y lo devuelveLlamada 2:Devoluciones 1Pone Envíos en 1y lo devuelveLlamada 3:Devoluciones 1y Envíos 1Pone Envíos en 2y lo devuelve
Cada llamada suma 1 a una clave del counts que recibe y devuelve ese mismo objeto. El único objeto en el que se escribe es el que se pasó como valor inicial.

El {} inicial es un objeto nuevo que solo guarda el recuento, así que no se escribe nada en los elementos de tickets. La tabla de abajo resume el valor inicial y lo que devuelve el callback en los 3 usos que has visto hasta ahora.

A qué quieres reducirValor inicialLo que devuelve el callback
Un total0El total anterior más el valor
El elemento mayorEl primer elemento del arrayEl elemento que resulte mayor
Conteos por categoríaUn objeto vacíoEl objeto con la clave actualizada

Calcula las ventas de una tienda en línea por método de pago. orders ya está declarado, y amount es el importe de un pedido.

① Crea un objeto salesByMethod con los métodos de pago como claves y el importe total de cada uno como valor.

② Convierte salesByMethod en una cadena JSON y muéstrala.

③ Muestra qué porcentaje de las ventas totales corresponde a la tarjeta de crédito (solo el número).

④ Muestra cuántos métodos de pago distintos hubo.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Cuando no hay elementos — reduce sin valor inicial

Un resultado filtrado puede quedar vacío. Si calculas el total de los envíos de un día en el que no hubo ninguno, reduce devuelve 0 o lanza un error, según cómo lo hayas escrito.

Puedes omitir el segundo argumento, el valor inicial. Si lo omites, el primer elemento del array pasa a ser el acumulador inicial, y la primera llamada al callback recibe el elemento del índice 1. Un array vacío no tiene primer elemento que tomar, así que se lanza un TypeError.

const yesterdayAmounts = [3200, 2480, 1180];
const todayAmounts = [];

// Sin valor inicial, el primer elemento, 3200, es el total inicial
console.log(yesterdayAmounts.reduce((total, amount) => total + amount));  // 6860

// Incluso con un array vacío, si hay valor inicial, se devuelve tal cual
console.log(todayAmounts.reduce((total, amount) => total + amount, 0));   // 0

// Sin valor inicial, no hay forma de decidir el total inicial
// todayAmounts.reduce((total, amount) => total + amount);
// TypeError: Reduce of empty array with no initial value
Un array vacío con y sin valor inicial
Array vacío,valor inicial 0No hay elementosque pasarEl callbackno se llamaSe devuelveel 0 inicialArray vacío,sin valor inicialNo hay primerelemento que tomarNo haytotal inicialLanza unTypeError
El mismo array vacío da resultados distintos según si pasas un valor inicial o no. El primer elemento solo hace falta cuando no hay valor inicial.

Este tipo de error es fácil de pasar por alto al escribir el código: funciona con los datos que tienes y solo falla el primer día en que ningún elemento cumple la condición. Cuando pases a reduce un resultado filtrado, pásale un valor inicial por si queda vacío.

Si omites {}, se escribe en el primer elemento

Si omites el {} final en el ejemplo de countByCategory, el propio objeto de consulta tickets[0] pasa a ser el counts inicial. No se lanza ningún error: a ese objeto se le agrega una clave Envíos, y la consulta de Devoluciones nunca se cuenta. Aunque el array no esté vacío, pasa siempre un valor inicial cuando reduzcas a un objeto.

Calcula el total de los reembolsos grandes en el registro de reembolsos de este mes. refunds ya está declarado, y las cantidades están en yenes.

① Muestra el total de todos los reembolsos, omitiendo el valor inicial.

② Crea un array solo con los reembolsos de 5000 yenes o más y muestra cuántos hay.

③ Muestra el total del array de ②, pasando un valor inicial.

④ Muestra el total del array de ②, omitiendo el valor inicial.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1¿Qué recibe como primer argumento el callback que le pasas a reduce?

Pregunta 2Si pasas un valor inicial de 0 a un reduce que compara los views de los elementos y devuelve el mayor, ¿qué se obtiene?

Pregunta 3¿Qué pasa si llamas a reduce sin valor inicial sobre un array vacío?