Array.prototype.reduce()
Метод reduce()
(редукувати, згорнути) примірників Array
виконує передану користувачем функцію зворотного виклику на кожному з елементів масиву, підряд, передаючи в неї повернене значення від обробки попереднього елементу. Кінцевим результатом обробки всіх елементів масиву функцією reduce()
стає єдине фінальне значення.
Під час першого виконання функції зворотного виклику "результату виконання попереднього кроку" іще не існує. Замість нього може бути використано початкове значення (аргумент initialValue
), якщо його було передано. Інакше — функція використає замість нього елемент за індексом 0, і почне виконання з наступного (з індексу 1 замість 0).
Спробуйте його в дії
Синтаксис
reduce(callbackFn)
reduce(callbackFn, initialValue)
Параметри
callbackFn
Функція для виконання на кожному елементі масиву. Її повернене значення стає значенням параметра
accumulator
при наступному закликуcallbackFn
. При останньому заклику повернене значення стане поверненим значеннямreduce()
. Ця функція викликається з наступними аргументами:accumulator
Результат виконання попереднього виклику
callbackFn
. При першому виклику цим значенням єinitialValue
, якщо воно задане; інакше це значення –array[0]
.currentValue
:Значення поточного елемента. При першому виклику цим значенням є
array[0]
, якщоinitialValue
задано; інакше це значення –array[1]
.currentIndex
:Позиція-індекс
currentValue
в масиві. При першому виклику це значення – 0, якщоinitialValue
задано; інакше це значення – 1.array
:Масив, на якому було викликано
reduce()
.
initialValue
Необов'язковеЗначення, яким ініціалізується
accumulator
під час першого виконання функції зворотного виклику. ЯкщоinitialValue
задане, тоcallbackFn
почне виконання з першим значенням масиву якcurrentValue
. ЯкщоinitialValue
не задане, тоaccumulator
ініціалізується першим значенням масиву, іcallbackFn
починає виконання з другого значення масиву якcurrentValue
. В такому випадку, якщо масив – порожній (тобто немає першого значення, аби повернути його якaccumulator
), викидається помилка.
Повернене значення
Значення, що є результатом виконання reduce()
до кінця крізь весь масив.
Винятки
TypeError
Викидається, якщо масив не містить елементів, а
initialValue
– не задано.
Опис
Метод reduce()
є ітеративним методом. Він запускає функцію зворотного виклику – "редуктор" на всіх елементах масиву, в порядку зростання індексів, та підсумовує їх до єдиного значення. Повернене значення callbackFn
щоразу передається в callbackFn
при наступному заклику як accumulator
. Кінцеве значення accumulator
(те, котре повернено з callbackFn
при завершальній ітерації масиву) стає поверненим значенням reduce()
. Більше про те, як загалом працюють такі методи, читайте в розділі ітеративних методів.
callbackFn
закликається лише для тих індексів масиву, що мають присвоєні значення. Вона не закликається для порожніх комірок у розріджених масивах.
На відміну від інших ітеративних методів, reduce()
не приймає аргументу thisArg
. callbackFn
завжди отримує this
зі значенням undefined
, котре замінюється на globalThis
, якщо callbackFn
є несуворою функцією.
reduce()
є центральною концепцією функційного програмування, в котрій неможливо змінювати будь-яке значення, тож для збору всіх значень до масиву треба повертати на кожній ітерації нове значення акумулятора. Така домовленість поширюється на reduce()
JavaScript: слід використовувати розгортання чи якусь іншу методику копіювання, де це можливо, і створювати як нове значення акумулятора нові масиви й об'єкти, а не видозмінювати старий акумулятор. При потребі змінити акумулятор замість його копіювання слід не забути повернути в функції зворотного виклику видозмінений об'єкт, інакше наступна ітерація отримає undefined
. Проте зверніть увагу на те, що копіювання акумулятора може призвести до збільшення використання пам'яті та погіршення продуктивності — дивіться детальніше в розділі Коли не варто використовувати reduce(). У таких випадках, щоб уникнути поганої продуктивності та незрозумілого коду, краще використовувати простий цикл for
.
Метод reduce()
є узагальненим. Він лишень очікує, що значення this
матиме властивість length
, а також властивості з цілочисловими ключами.
Крайові випадки
Якщо масив містить лише один елемент (незалежно від його позиції), і значення initialValue
передано не було, або ж якщо initialValue
було передано, проте сам масив порожній, то повернеться саме значення без викликання callbackFn
.
Якщо було передано initialValue
і масив не порожній, то метод reduce()
завжди викличе функцію зворотного виклику, починаючи з індексу 0.
Якщо initialValue
не передане, то метод reduce()
буде по різному себе поводити з масивами довжиною більшою за 1, рівною 1 та рівною 0, як показано в наступному прикладі:
const getMax = (a, b) => Math.max(a, b);
// функція зворотного виклику виконується на кожному елементі масиву, починаючи з 0
[1, 100].reduce(getMax, 50); // 100
[50].reduce(getMax, 10); // 50
// функція зворотного виклику виконується один раз для елементу з індексом 1
[1, 100].reduce(getMax); // 100
// функція зворотного виклику не виконується
[50].reduce(getMax); // 50
[].reduce(getMax, 1); // 1
[].reduce(getMax); // TypeError
Приклади
Як працює reduce(), якщо не вказано початкове значення
Наведений нижче код демонструє, що відбувається, якщо викликати reduce()
на масиві й не надати функції початкового значення.
const array = [15, 16, 17, 18, 19];
function reducer(accumulator, currentValue, index) {
const returns = accumulator + currentValue;
console.log(
`accumulator: ${accumulator}, currentValue: ${currentValue}, index: ${index}, returns: ${returns}`,
);
return returns;
}
array.reduce(reducer);
Функція зворотного виклику закликається чотири рази, з наступними аргументами та поверненими значеннями під час кожного виклику:
accumulator |
currentValue |
index |
Повернене значення | |
---|---|---|---|---|
Перший виклик | 15 |
16 |
1 |
31 |
Другий виклик | 31 |
17 |
2 |
48 |
Третій виклик | 48 |
18 |
3 |
66 |
Четвертий виклик | 66 |
19 |
4 |
85 |
Параметр array
ніколи не змінюється протягом процесу – він завжди [15, 16, 17, 18, 19]
. Значення, повернене reduce()
, буде значенням, поверненим останнім закликом функції зворотного виклику (85
).
Як працює reduce() зі вказаним початковим значенням
Нижче виконаймо редукцію такого самого масиву, застосувавши такий самий алгоритм, проте передамо число 10
як параметр initialValue
, другим аргументом до функції reduce()
:
[15, 16, 17, 18, 19].reduce(
(accumulator, currentValue) => accumulator + currentValue,
10,
);
Функція зворотного виклику буде закликана п'ять разів, з наступними аргументами та поверненими значеннями під час кожного виклику:
accumulator |
currentValue |
index |
Повернене значення | |
---|---|---|---|---|
Перший виклик | 10 |
15 |
0 |
25 |
Другий виклик | 25 |
16 |
1 |
41 |
Третій виклик | 41 |
17 |
2 |
58 |
Четвертий виклик | 58 |
18 |
3 |
76 |
П'ятий виклик | 76 |
19 |
4 |
95 |
В цьому випадку reduce()
поверне значення 95
.
Сума значень в масиві об'єктів
Щоб просумувати значення, що містяться в масиві об'єктів, необхідно передати initialValue
, щоб кожний з елементів був опрацьований заданою функцією.
const objects = [{ x: 1 }, { x: 2 }, { x: 3 }];
const sum = objects.reduce(
(accumulator, currentValue) => accumulator + currentValue.x,
0,
);
console.log(sum); // 6
Послідовний конвеєр функцій
Функція pipe
(конвеєр) приймає послідовність функцій та повертає нову функцію. Коли нова функція викликається з аргументом, то послідовність функцій викликається послідовно, і кожна з них отримує значення, повернене попередньою функцією.
const pipe =
(...functions) =>
(initialValue) =>
functions.reduce((acc, fn) => fn(acc), initialValue);
// Цеглинки для використання в компонуванні
const double = (x) => 2 * x;
const triple = (x) => 3 * x;
const quadruple = (x) => 4 * x;
// Скомпоновані функції для множення конкретних значень
const multiply6 = pipe(double, triple);
const multiply9 = pipe(triple, triple);
const multiply16 = pipe(quadruple, quadruple);
const multiply24 = pipe(double, triple, quadruple);
// Використання
multiply6(6); // 36
multiply9(9); // 81
multiply16(16); // 256
multiply24(10); // 240
Почерговий запуск промісів
Послідовне виконання промісів – це по суті той самий конвеєр функцій, показаний в попередньому розділі, але виконаний асинхронно.
// Порівняйте це з конвеєром: fn(acc) змінено на acc.then(fn),
// а initialValue гарантовано є промісом
const asyncPipe =
(...functions) =>
(initialValue) =>
functions.reduce((acc, fn) => acc.then(fn), Promise.resolve(initialValue));
// Цеглинки для використання в компонуванні
const p1 = async (a) => a * 5;
const p2 = async (a) => a * 2;
// Скомпоновані функції також можуть повертати непроміси, оскільки всі
// значення, зрештою, загортаються в проміси
const f3 = (a) => a * 3;
const p4 = async (a) => a * 4;
asyncPipe(p1, p2, f3, p4)(10).then(console.log); // 1200
Функцію asyncPipe
також можна реалізувати за допомогою async
та await
, що краще демонструє її подібність до pipe
:
const asyncPipe =
(...functions) =>
(initialValue) =>
functions.reduce(async (acc, fn) => fn(await acc), initialValue);
Застосування reduce() до розріджених масивів
Метод reduce()
пропускає в розріджених масивах відсутні елементи, але не пропускає значення undefined
.
console.log([1, 2, , 4].reduce((a, b) => a + b)); // 7
console.log([1, 2, undefined, 4].reduce((a, b) => a + b)); // NaN
Виклик reduce() на об'єктах-немасивах
Метод reduce()
зчитує з this
властивість length
, а тоді звертається до кожної властивості, чий ключ є невід'ємним цілим числом, меншим за length
.
const arrayLike = {
length: 3,
0: 2,
1: 3,
2: 4,
3: 99, // ігнорується reduce(), оскільки length – 3
};
console.log(Array.prototype.reduce.call(arrayLike, (x, y) => x + y));
// 9
Коли не варто використовувати reduce()
Багатоцільові функції вищого порядку, подібні до reduce()
, можуть бути потужними, але іноді їх важко зрозуміти, особливо для менш досвідчених розробників на JavaScript. Якщо код стає зрозумілішим при використанні інших методів масиву, розробники повинні зважити компроміс між читабельністю та іншими перевагами використання reduce()
.
Зверніть увагу на те, що reduce()
завжди еквівалентний циклу for...of
, за винятком того, що замість внесення змін до змінної у верхній області видимості ми тепер повертаємо нове значення для кожної ітерації:
const val = array.reduce((acc, cur) => update(acc, cur), initialValue);
// Це рівносильно до:
let val = initialValue;
for (const cur of array) {
val = update(val, cur);
}
Як зазначено вище, причина того, що може виникнути бажання використовувати reduce()
, – імітування практик функціонального програмування щодо беззмінності даних. Таким чином, розробники, які дотримуються беззмінності акумулятора, часто копіюють весь акумулятор для кожної ітерації, отак:
const names = ["Аліса", "Богдан", "Тетяна", "Борис", "Аліса"];
const countedNames = names.reduce((allNames, name) => {
const currCount = Object.hasOwn(allNames, name) ? allNames[name] : 0;
return {
...allNames,
[name]: currCount + 1,
};
}, {});
Цей код працює повільно, оскільки кожна ітерація повинна копіювати весь об'єкт allNames
, який може бути великим, в залежності від того, скільки є унікальних імен. Цей код має найгіршу продуктивність – O(N^2)
, де N
– довжина names
.
Кращий варіант – змінювати об'єкт allNames
на кожній ітерації. Проте якщо allNames
все одно змінюється, то можна перетворити reduce()
на простий цикл for
, що набагато зрозуміліше:
const names = ["Аліса", "Богдан", "Тетяна", "Борис", "Аліса"];
const countedNames = names.reduce((allNames, name) => {
const currCount = allNames[name] ?? 0;
allNames[name] = currCount + 1;
// повернути allNames, бо інакше – наступна ітерація отримає undefined
return allNames;
}, Object.create(null));
const names = ["Аліса", "Богдан", "Тетяна", "Борис", "Аліса"];
const countedNames = Object.create(null);
for (const name of names) {
const currCount = countedNames[name] ?? 0;
countedNames[name] = currCount + 1;
}
Таким чином, якщо акумулятор є масивом або об'єктом, і на кожній ітерації цей масив або об'єкт копіюється, можна випадково ввести у код квадратичну складність, що призводить до швидкого погіршення продуктивності на великих даних. Таке траплялося в реальному коді — дивіться, наприклад, Пришвидшення Tanstack Table у 1000 разів завдяки змінам у 1 рядку коду.
Частина прийнятних ситуацій для використання reduce()
подана вище (перш за все, сумування масиву, послідовне виконання промісів та конвеєр функцій). В інших випадках існують кращі варіанти, ніж reduce()
.
-
Сплощення масиву масивів. Краще використати
flat()
.const flattened = array.reduce((acc, cur) => acc.concat(cur), []);
const flattened = array.flat();
-
Групування об'єктів за властивістю. Краще використати
Object.groupBy()
.const groups = array.reduce((acc, obj) => { const key = obj.name; const curGroup = acc[key] ?? []; return { ...acc, [key]: [...curGroup, obj] }; }, {});
const groups = Object.groupBy(array, (obj) => obj.name);
-
Зчеплення масивів, що містяться в масиві об'єктів. Краще використати
flatMap()
.const friends = [ { name: "Анна", books: ["Біблія", "Вогнесміх"] }, { name: "Борислав", books: ["Хіба ревуть воли, як ясла повні", "Кайдашева сім'я"], }, { name: "Аліса", books: ["Бот: Атакамська криза", "Культ"] }, ]; const allBooks = friends.reduce((acc, cur) => [...acc, ...cur.books], []);
const allBooks = friends.flatMap((person) => person.books);
-
Усунення з масиву дублікатів. Краще використати
Set
іArray.from()
.const uniqArray = array.reduce( (acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]), [], );
const uniqArray = Array.from(new Set(array));
-
Видалення або додавання елементів масиву. Краще використати
flatMap()
.// Беремо масив чисел і розбиваємо цілі квадрати на їхні квадратні корені const roots = array.reduce((acc, cur) => { if (cur < 0) return acc; const root = Math.sqrt(cur); if (Number.isInteger(root)) return [...acc, root, root]; return [...acc, cur]; }, []);
const roots = array.flatMap((val) => { if (val < 0) return []; const root = Math.sqrt(val); if (Number.isInteger(root)) return [root, root]; return [val]; });
Якщо елементи масиву лише видаляються, також можна використати
filter()
. -
Пошук елементів або перевірка того, що елементи задовольняють умові. Краще використати
find()
іfindIndex()
, абоsome()
іevery()
. Ці методи мають додаткову перевагу: вони повертають результат, щойно він відомий, без ітерування всього масиву.const allEven = array.reduce((acc, cur) => acc && cur % 2 === 0, true);
const allEven = array.every((val) => val % 2 === 0);
У тих випадках, коли reduce()
є найкращим варіантом, документація та семантичне іменування змінних можуть допомогти полегшити проблему з читабельністю.
Специфікації
Сумісність із браузерами
desktop | mobile | server | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
reduce
|
Chrome Full support 3 | Edge Full support 12 | Firefox Full support 3 | Internet Explorer Full support 9 | Opera Full support 10.5 | Safari Full support 5 | WebView Android Full support 37 | Chrome Android Full support 18 | Firefox for Android Full support 4 | Opera Android Full support 14 | Safari on iOS Full support 4 | Samsung Internet Full support 1.0 | Deno Full support 1.0 | Node.js Full support 0.10.0 |