Как работает reduce() в JavaScript, когда его нужно применять и какие крутые вещи можно с ним делать
В этой статье вы узнаете про метод reduce()
и какие клевые штуки можно вытворять с его помощью, выйдя за рамки его общеизвестного применения.
Перевод статьи How JavaScript’s Reduce method works, when to use it, and some of the cool things it can do
👉Мой Твиттер — там много из мира фронтенда, да и вообще поговорим🖖. Подписывайтесь, будет интересно: ) ✈️
reduce()
в JavaScript это один из краеугольных камней функционального программирования. Давайте изучим то, как он работает, когда его нужно применять и то какие крутые вещи можно с его помощью вытворять.
Простая редукция
Когда использовать: когда у вас есть массив чисел и вам надо всех их сложить.
const euros = [29.76, 41.85, 46.5];
const sum = euros.reduce((total, amount) => total + amount);
sum // 118.11
Как использовать:
- В этом примере
reduce()
принимает два параметра,total
и число с которым сейчас идёт работа. - Метод проходится по каждому числу в массиве, как бы это было с циклом
for
. - Когда цикл только начинается,
total
имеет значение первого числа с начала массива (29.76), а числом в обработке становится следующее по этому же массиву число (41.85). - Конкретно в этом примере, нам надо прибавить настоящее число к
total
. - Такое вычисление повторяется для каждого числа в массиве и каждый раз настоящее число меняется на следующее число в массиве справа.
- Когда уже нет чисел в массиве, метод отдаёт значение
total
.
ES5 версия JavaScript метода reduce
Если вы до этого никогда не использовали ES6 синтаксис , то не дайте примеру выше испугать вас. Это тоже самое, что написано ниже:
var euros = [29.76, 41.85, 46.5];
var sum = euros.reduce( function(total, amount){
return total + amount
});sum // 118.11
Мы используем const
вместо var
и мы заменим слово function
на =>
(стрелочная функция) после параметров, а ещё мы не будем писать слово return
.
Я буду писать на ES6 синтаксисе в оставшихся примерах, так как это коротко и оставляет меньше пространства для возникновения ошибок.
Находим среднее число с JavaScript методом reduce
Тут вместо вывода суммы вам надо разделить её на длину массива перед тем, как вы вернете последнее значение.
Вы можете это сделать воспользовавшись преимуществами других аргументов доступных в reduce()
. Первый из них это index
. Как и с циклом for
, index
показывает количество раз, которое редюсер пробежался по массиву. Последний аргумент это уже сам массив.
const euros = [29.76, 41.85, 46.5];
const average = euros.reduce((total, amount, index, array) => {
total += amount;
if( index === array.length-1) {
return total/array.length;
}else {
return total;
}
});average // 39.37
Map и Filter как редюсеры
Если вы можете использовать функцию reduce
, чтобы найти среднее число, то следовательно, вы можете использовать её любым образом, в принципе, каким только захотите.
Для примера, вы могли бы увеличить вдвое total
или разделить на два каждое число перед тем, как сложить их вместе, ну или вы бы могли использовать if
в редукторе для того, чтобы сложить числа, которые больше 10. Я считаю, что в JavaScript метод reduce
даёт вам что-то вроде мини CodePen, в котором вы можете записать совершенно любую логику. Он просто пропустит её через каждое число в массиве и отдаст одно значение.
Но фишка в том, что вам не обязательно отдавать только одно значение. Вы можете заредюсить массив в новый массив.
Для примера, давайте сократим массив в другой массив, в котом каждое число будет увеличено вдвое. Для этого нам надо выставить изначальное значение для аккумулятора на пустой, уже изначальный массив.
Изначальное значение — это значение параметра total
при старте функции. Вы можете выставить это значение добавив запятую, за которой следует изначальное значение внутри круглых скобок, но после фигурных скобок (выделено жирным в примере ниже).
const average = euros.reduce((total, amount, index, array) => {
total += amount
return total/array.length
}, 0);
В предыдущих примерах исходным значением был ноль, поэтому я просто пропускал его. Пропуская указание изначального значения, total
будет по дефолту получать значение первого числа в массиве.
Указывая исходное значение, как пустой массив, мы можем в последствии добавлять каждое значение в total
. Если мы хотим с помощью reduce
превратить массив значений в ещё один массив, в котором каждое значение будет умножено на два, то нам нужно добавлять amount * 2
. А затем отдавать total
, когда уже не останется чисел для добавления.
const euros = [29.76, 41.85, 46.5];
const doubled = euros.reduce((total, amount) => {
total.push(amount * 2);
return total;
}, []);doubled // [59.52, 83.7, 93]
Мы создали новый массив, в котом каждое число умножается на два. Мы могли бы также отфильтровать числа, которые нам не нужны, просто добавив if
внутри функции.
const euro = [29.76, 41.85, 46.5];
const above30 = euro.reduce((total, amount) => {
if (amount > 30) {
total.push(amount);
}
return total;
}, []);above30 // [ 41.85, 46.5 ]
В общем, это как методы map()
и filter()
, только переписанные в reduce()
.
Но для этих примеров было бы разумнее использовать map
или filter
, так как их попросту визуально легче воспринимать. Преимущество метода reduce
становится очевидным, когда вам надо сделать map
и filter
вместе, и при этом у вас довольно большие объемы данных для обработки.
При создании цепочки с map
и filter
, получается то, что вы делаете одну и ту же работу дважды. Вы отфильтровываете каждое значение и затем вы пробегаетесь с указанными параметрами функции по каждому оставшемуся из них. А с reduce
вы можете отфильтровать и пробежаться по всему массиву за один подход.
Используйте map
и filter
, но когда вы начнете выстраивать цепочку с множеством методов, то помните, что куда быстрее с этими данными применить reduce
.
Ведём учёт данных с помощью reduce
Когда использовать: когда у вас есть коллекция данных и вам надо узнать то, сколько типов каждого элемента находится в этой коллекции.
const fruitBasket = ['banana', 'cherry', 'orange', 'apple', 'cherry', 'orange', 'apple', 'banana', 'cherry', 'orange', 'fig' ];const count = fruitBasket.reduce( (tally, fruit) => {
tally[fruit] = (tally[fruit] || 0) + 1 ;
return tally;
} , {})count // { banana: 2, cherry: 3, orange: 3, apple: 2, fig: 1 }
Чтобы подсчитать количество элементов в массиве, изначальным значением должен быть пустой объект, а не массив, как было в предыдущем примере.
Так как мы собираемся возвращать объект, то мы теперь можем хранить пару ключ-значение в total
.
fruitBasket.reduce( (tally, fruit) => {
tally[fruit] = 1;
return tally;
}, {})
Сначала нам надо выдать имя первому ключу, назвав его как наше первое значение и затем дать ему значение 1
.
Это даст нам объект, где все названия фруктов будут в виде ключей и каждый будет иметь значение 1
. А нам нужно увеличить значение каждого фрукта, если он повторяется.
Для этого, на втором цикле мы проверяем есть ли в total
ключ с данным фруктом. Если нет, то мы создаем данный ключ. А если есть, то мы увеличиваем его значение на единицу.
fruitBasket.reduce((tally, fruit) => {
if (!tally[fruit]) {
tally[fruit] = 1;
} else {
tally[fruit] = tally[fruit] + 1;
}
return tally;
}, {});
Я описал такую же логику, но более сжатым способом, который вы видели немного выше.
Сливаем массив воедино с помощью reduce
Мы можем использовать reduce
, чтобы сливать воедино вложенные значения, которые в конечном итоге обретут форму единого массива.
Мы выставляем изначальное значение на пустой массив и далее конкатенируем данное значение с total
.
const data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
const flat = data.reduce((total, amount) => {
return total.concat(amount);
}, []);flat // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Куда чаще информация вложена более сложными способами. Для примера, давайте представим, что нам надо просто получить все цвета в переменной data
, находящейся ниже.
const data = [
{a: 'happy', b: 'robin', c: ['blue','green']},
{a: 'tired', b: 'panther', c: ['green','black','orange','blue']},
{a: 'sad', b: 'goldfish', c: ['green','red']}
];
Мы пройдемся по каждому объекту и возьмём оттуда нужные цвета. Это мы сделаем, просто указав forEach
пробежаться по amount.c
, где при каждой итерации, вложенный массив будет добавляться в total
.
const colors = data.reduce((total, amount) => {
amount.c.forEach( color => {
total.push(color);
})
return total;
}, [])Цвета
//['blue','green','green','black','orange','blue','green','red']
Если нам понадобятся только уникальные значения, то тогда нам надо будет проверить есть ли заданное значение в total
, прежде чем отправлять его в этот массив.
const uniqueColors = data.reduce((total, amount) => {
amount.c.forEach( color => {
if (total.indexOf(color) === -1){
total.push(color);
}
});
return total;
}, []);uniqueColors // [ 'blue', 'red', 'green', 'black', 'orange']
Пайплайн с reduce()
Довольно интересным моментом в методе reduce()
является то, что вы можете можете работать с функциями, как с числами и строками.
Предположим, что у нас есть набор простых математических функций, с помощью которых мы можем прибавлять, убавлять, умножать на два и делить на два значение amount
.
function increment(input) { return input + 1;}function decrement(input) { return input — 1; }function double(input) { return input * 2; }function halve(input) { return input / 2; }
В общем, дальше нам надо будет увеличить значение на 1
, потом умножить на два, потом вычесть единицу.
Конечно, мы бы могли написать функцию, которая берёт input
и возвращает (input + 1) * 2 -1
. Но тут проблема в том, что мы знаем то, что собираемся увеличить значение, потом удвоить его и потом вычесть единицу и затем разделить его на два, в какой-нибудь момент в будущем. Мы не хотим переписывать нашу функцию каждый раз, так что мы сделаем пайплайн с помощью reduce()
.
Пайплайн (pipeline) это термин, который используется для набора функций, которые последовательно изменяют какое-либо значение в уже финальный результат. Наш пайплайн будет состоять из трех функций, в том порядке, в котором мы хотим их использовать.
let pipeline = [increment, double, decrement];
В общем, теперь мы используем reduce()
не на массиве значений, а на пайплайне из функций. Такая конструкция сработает, потому что мы выставили изначальное значение на то, которое хотим в дальнейшем трансформировать.
const result = pipeline.reduce(function(total, func) {
return func(total);
}, 1);result // 3
Так как пайплайн у нас является массивом, то он может быть легко модифицирован. Если мы хотим что-нибудь увеличить на 1
три раза подряд, потом вдвое увеличить, вычесть 1
и разделить на два, то нам будет достаточно просто изменить пайплайн.
var pipeline = [
increment,
increment,
increment,
double,
decrement,
halve
];
Сама функция останется такой же.
Избегаем глупых ошибок
Если вы не передадите изначальное значение, то reduce()
будет считать, что им является первый элемент в массиве. Всё работало отлично в первых примерах, потому что мы работали со списком чисел.
Если вы захотите подсчитать фрукты и упустите изначальное значение, то ничего у вас тогда не выйдет. Вообще, упущение момента указания изначального значения это ошибка, которую легко пропустить и именно её нужно проверять первым делом при дебаггинге.
Ещё частенько забывают отдавать total
. Вы должны возвращать какое-нибудь значение, что reduce()
продолжал работать. Всегда проверяйте дважды и убеждайтесь в том, что вы отдаёте нужное вам значение.