Пошук уроків, статей та іншого контенту
Освоїте reduce для підсумовування, групування та побудови результату на основі всіх елементів.
reduceМетод reduce проходить по всіх елементах масиву та поступово формує одне підсумкове значення.
Цим значенням може бути:
число;
рядок;
масив;
об’єкт;
будь-яка інша структура даних.
Загальний синтаксис:
const result = array.reduce((accumulator, currentValue, index, array) => {
// оновлення accumulator
return accumulator;
}, initialValue);Основні параметри callback-функції:
accumulator — накопичене значення;
currentValue — поточний елемент масиву;
index — індекс поточного елемента;
array — початковий масив;
initialValue — початкове значення акумулятора.
У більшості випадків варто явно передавати initialValue. Це робить код зрозумілішим і захищає від помилок на порожніх масивах.
Найпростіший приклад — обчислення суми чисел:
const prices = [120, 80, 250, 50];
const total = prices.reduce((sum, price) => {
return sum + price;
}, 0);
console.log(total); // 500Послідовність обчислень:
sum = 0, price = 120 → 120;
sum = 120, price = 80 → 200;
sum = 200, price = 250 → 450;
sum = 450, price = 50 → 500.
Те саме можна записати коротше:
const prices = [120, 80, 250, 50];
const total = prices.reduce((sum, price) => sum + price, 0);
console.log(total); // 500Якщо масив містить об’єкти, у callback потрібно звертатися до потрібної властивості:
const products = [
{ name: "Клавіатура", price: 1500 },
{ name: "Миша", price: 800 },
{ name: "Навушники", price: 2200 }
];
const totalPrice = products.reduce((total, product) => {
return total + product.price;
}, 0);
console.log(totalPrice); // 4500Акумулятор не обов’язково має бути числом. Наприклад, за один прохід можна обчислити суму, кількість і максимальне значення:
const transactions = [120, -50, 300, -80, 200];
const statistics = transactions.reduce(
(result, amount) => {
result.total += amount;
result.count += 1;
result.max = Math.max(result.max, amount);
return result;
},
{
total: 0,
count: 0,
max: -Infinity
}
);
console.log(statistics);
// { total: 490, count: 5, max: 300 }У цьому прикладі акумулятор — об’єкт. На кожній ітерації ми змінюємо його властивості та повертаємо той самий об’єкт.
Якщо потрібно уникати мутації акумулятора, можна створювати новий об’єкт:
const numbers = [10, 20, 30];
const result = numbers.reduce(
(accumulator, number) => ({
sum: accumulator.sum + number,
count: accumulator.count + 1
}),
{
sum: 0,
count: 0
}
);
console.log(result); // { sum: 60, count: 3 }Для простих обчислень мутація локального акумулятора зазвичай є нормальною. Незмінний підхід може бути корисним, якщо об’єкт акумулятора використовується в інших частинах програми.
Одна з найпрактичніших можливостей reduce — групування елементів за певною властивістю.
Наприклад, згрупуємо товари за категоріями:
const products = [
{ name: "Клавіатура", category: "Комп'ютери", price: 1500 },
{ name: "Миша", category: "Комп'ютери", price: 800 },
{ name: "Футболка", category: "Одяг", price: 700 },
{ name: "Джинси", category: "Одяг", price: 1800 },
{ name: "Кава", category: "Продукти", price: 250 }
];
const productsByCategory = products.reduce((groups, product) => {
const category = product.category;
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(product);
return groups;
}, {});
console.log(productsByCategory);Результат:
{
"Комп'ютери": [
{ name: "Клавіатура", category: "Комп'ютери", price: 1500 },
{ name: "Миша", category: "Комп'ютери", price: 800 }
],
"Одяг": [
{ name: "Футболка", category: "Одяг", price: 700 },
{ name: "Джинси", category: "Одяг", price: 1800 }
],
"Продукти": [
{ name: "Кава", category: "Продукти", price: 250 }
]
}Для перевірки наявності групи також можна використовувати оператор нульового присвоєння:
const grouped = products.reduce((groups, product) => {
(groups[product.category] ??= []).push(product);
return groups;
}, {});Такий запис означає: якщо масиву для категорії ще немає, створити порожній масив, а потім додати до нього товар.
reduce зручно використовувати для побудови частотного словника.
Наприклад, підрахуємо кількість кожного статусу замовлення:
const orders = [
{ id: 1, status: "new" },
{ id: 2, status: "paid" },
{ id: 3, status: "new" },
{ id: 4, status: "shipped" },
{ id: 5, status: "paid" }
];
const statusCounts = orders.reduce((counts, order) => {
counts[order.status] = (counts[order.status] ?? 0) + 1;
return counts;
}, {});
console.log(statusCounts);
// { new: 2, paid: 2, shipped: 1 }Цей підхід працює не лише зі статусами, а й зі словами, тегами, ідентифікаторами та іншими повторюваними значеннями.
Хоча для простого перетворення елементів краще використовувати map, іноді reduce дає змогу одночасно відфільтрувати та перетворити дані.
Наприклад, залишимо лише успішні платежі та отримаємо їхні суми:
const payments = [
{ id: 1, amount: 100, status: "success" },
{ id: 2, amount: 250, status: "failed" },
{ id: 3, amount: 80, status: "success" }
];
const successfulAmounts = payments.reduce((amounts, payment) => {
if (payment.status === "success") {
amounts.push(payment.amount);
}
return amounts;
}, []);
console.log(successfulAmounts); // [100, 80]Те саме можна зробити виразніше за допомогою комбінації filter і map:
const successfulAmounts = payments
.filter((payment) => payment.status === "success")
.map((payment) => payment.amount);Вибір залежить від завдання:
filter + map — коли потрібні послідовні прості операції;
reduce — коли результат має складнішу структуру або потрібно виконати все за один прохід.
Розглянемо практичніше завдання: для кожної категорії потрібно порахувати кількість товарів і загальну вартість.
const products = [
{ name: "Клавіатура", category: "Комп'ютери", price: 1500 },
{ name: "Миша", category: "Комп'ютери", price: 800 },
{ name: "Футболка", category: "Одяг", price: 700 },
{ name: "Джинси", category: "Одяг", price: 1800 }
];
const summary = products.reduce((result, product) => {
const category = product.category;
if (!result[category]) {
result[category] = {
count: 0,
totalPrice: 0
};
}
result[category].count += 1;
result[category].totalPrice += product.price;
return result;
}, {});
console.log(summary);
// {
// "Комп'ютери": { count: 2, totalPrice: 2300 },
// "Одяг": { count: 2, totalPrice: 2500 }
// }Акумулятор у цьому прикладі має такий тип:
{
[category]: {
count: number,
totalPrice: number
}
}Це типовий сценарій для підготовки даних перед відображенням у звіті або на сторінці статистики.
Якщо передати початкове значення, reduce безпечно працює з порожнім масивом:
const numbers = [];
const sum = numbers.reduce((total, number) => {
return total + number;
}, 0);
console.log(sum); // 0Без другого аргументу метод спробує взяти перший елемент масиву як початкове значення:
const numbers = [10, 20, 30];
const sum = numbers.reduce((total, number) => total + number);
console.log(sum); // 60Але для порожнього масиву такий код завершиться помилкою:
const numbers = [];
const sum = numbers.reduce((total, number) => total + number);
// TypeError: Reduce of empty array with no initial valueТому рекомендований стиль — завжди вказувати початкове значення:
const sum = numbers.reduce((total, number) => total + number, 0);Тип початкового значення має відповідати результату:
const names = ["Anna", "Oleh", "Iryna"];
const result = names.reduce((text, name) => {
return `${text}${name}; `;
}, "");
console.log(result); // Anna; Oleh; Iryna;reduce не потрібно використовувати для кожної операції з масивом.
map, якщоПотрібно перетворити кожен елемент і отримати масив такої самої довжини:
const numbers = [1, 2, 3];
const doubled = numbers.map((number) => number * 2);
console.log(doubled); // [2, 4, 6]filter, якщоПотрібно залишити лише елементи, що відповідають умові:
const numbers = [1, 2, 3, 4];
const evenNumbers = numbers.filter((number) => number % 2 === 0);
console.log(evenNumbers); // [2, 4]find, якщоПотрібно знайти перший елемент:
const users = [
{ id: 1, name: "Anna" },
{ id: 2, name: "Oleh" }
];
const user = users.find((user) => user.id === 2);
console.log(user); // { id: 2, name: "Oleh" }reduce, якщоПотрібно:
отримати одне підсумкове значення;
побудувати об’єкт або іншу структуру;
згрупувати елементи;
підрахувати кількість;
виконати кілька агрегацій за один прохід.
returnCallback-функція повинна повертати оновлений акумулятор:
const numbers = [1, 2, 3];
const sum = numbers.reduce((total, number) => {
total + number;
}, 0);
console.log(sum); // undefinedПравильний варіант:
const sum = numbers.reduce((total, number) => {
return total + number;
}, 0);Або з неявним поверненням:
const sum = numbers.reduce((total, number) => total + number, 0);Якщо результатом має бути об’єкт, початкове значення теж має бути об’єктом:
const words = ["one", "two", "three"];
const grouped = words.reduce((result, word) => {
result[word.length] ??= [];
result[word.length].push(word);
return result;
}, {});Якби замість {} було передано [], структура акумулятора не відповідала б логіці групування.
Під час групування ми можемо змінювати акумулятор, але не повинні без потреби змінювати самі елементи початкового масиву:
const users = [
{ name: "Anna", role: "admin" },
{ name: "Oleh", role: "user" }
];
const usersByRole = users.reduce((groups, user) => {
groups[user.role] ??= [];
groups[user.role].push(user);
return groups;
}, {});У цьому прикладі змінюється лише новий об’єкт usersByRole, а сам масив users залишається тим самим.
reduceІноді складний reduce важче прочитати, ніж кілька простих методів:
const result = users
.filter((user) => user.role === "admin")
.map((user) => user.name);Не варто замінювати цю зрозумілу послідовність на reduce, якщо це не дає реальної користі.
reduce перетворює масив на одне підсумкове значення.
Акумулятор може бути числом, рядком, масивом або об’єктом.
Callback повинен повертати оновлений акумулятор.
Початкове значення краще вказувати завжди.
За допомогою reduce можна підсумовувати, підраховувати та групувати дані.
Для простих перетворень і фільтрації зазвичай зрозуміліші map і filter.
Складні агрегації зручніше виконувати за один прохід через reduce.