Как работает reduce() в JavaScript, когда его нужно применять и какие крутые вещи можно с ним делать

Stas Bagretsov
8 min readOct 21, 2019

--

В этой статье вы узнаете про метод 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() продолжал работать. Всегда проверяйте дважды и убеждайтесь в том, что вы отдаёте нужное вам значение.

--

--

Stas Bagretsov

Надеюсь верую вовеки не придет ко мне позорное благоразумие. webdev/sports/books