Последняя функция из нашей тройки — метод reduce, который используется для агрегации (название в других языках accumulate, fold или, по-русски, "свёртка"). Он устроен немного сложнее, чем map и filter, но, в целом, сохраняет общий подход с передачей функции.

Как обычно, начнём с примера на циклах. Реализуем код, находящий самого взрослого пользователя:

const users = [
  { name: 'Igor', age: 19 },
  { name: 'Danil', age: 1 },
  { name: 'Vovan', age: 4 },
  { name: 'Matvey', age: 16 },
];

let oldest = users[0];
for (const user of users) {
  if (user.age > oldest.age) {
    oldest = user;
  }
}

console.log(oldest); // => { name: 'Igor', age: 19 }

Основное отличие агрегации от отображения и фильтрации в том, что результатом агрегации может быть любой тип данных — как примитивный так и составной, например, массив. Кроме того, агрегация нередко подразумевает инициализацию начальным значением. В примере выше она выполняется на строчке let oldest = users[0];.

Посмотрим ещё один пример агрегации — группировку имён пользователей по возрасту:

import _ from 'lodash';

const users = [
  { name: 'Petr', age: 4 },
  { name: 'Igor', age: 19 },
  { name: 'Vovan', age: 4 },
  { name: 'Matvey', age: 16 },
];

const usersByAge = {};
for (const { age, name } of users) {
  // Функция _.has() проверяет наличие свойства в объекте,
  // Мы подключили библиотеку Lodash, чтобы воспользоваться этой функцией.
  if (_.has(usersByAge, age)) {
    usersByAge[age].push(name);
  } else {
    usersByAge[age] = [name];
  }
}

console.log(usersByAge);
// => { 4: [ 'Petr', 'Vovan' ], 16: [ 'Matvey' ], 19: [ 'Igor' ] }

В этом примере результатом агрегации становится объект, в свойствах которого записаны массивы. Этот результат в самом начале инициируется пустым объектом, а затем постепенно, на каждой итерации, "наполняется" нужными данными. Значение, которое накапливает результат агрегации, принято называть словом "аккумулятор". В примерах выше это oldest и usersByAge.

Реализуем первый пример, используя reduce:

const oldest = users.reduce(
  (acc, user) => user.age > acc.age ? user : acc, // callback-функция
  users[0], // аккумулятор (начальное значение)
);

console.log(oldest); // => { name: 'Igor', age: 19 }

Метод reduce принимает на вход два параметра — функцию-обработчик (callback) и начальное значение аккумулятора. Поиск самого взрослого пользователя аналогичен поиску максимального (или минимального) числа в массиве. Соответственно, аккумулятор должен быть инициализирован первым пользователем. Этот же аккумулятор возвращается наружу.

Callback-функция, передаваемая в reduce — самая важная часть и ключ к пониманию работы всего механизма агрегации. Она принимает на вход два значения. Первый — текущее значение аккумулятора, второй — текущий обрабатываемый элемент. Задача функции — вернуть новое значение аккумулятора. reduce никак не анализирует содержимое аккумулятора. Всё, что он делает, передаёт его в каждый новый вызов до тех пор, пока не будет обработана вся коллекция, и в конце концов вернёт его наружу. Подчеркну, что возвращать аккумулятор надо всегда, даже если он не изменился.

Второй пример с использованием reduce выглядит так:

import _ from 'lodash';

// предварительно подготовим callback-функцию
const cb = (acc, { age, name }) => {
  if (_.has(acc, age)) {
    acc[age].push(name);
  } else {
    acc[age] = [name];
  }
  return acc;
};

const usersByAge = users.reduce(cb, {});

Код практически не изменился, за исключением того, что ушёл цикл и появился возврат аккумулятора из анонимной функции.

Реализация

const myReduce = (collection, callback, init) => {
  let acc = init;
  for (const item of collection) {
    acc = callback(acc, item); // Заменяем старый аккумулятор новым
  }
  return acc;
};

reduce — очень мощная функция. Формально, можно работать, используя одну лишь её, так как она может заменить и отображение и фильтрацию. Но делать так не стоит. Агрегация управляет состоянием (аккумулятором) явно. Такой код всегда сложнее и требует больше действий. Поэтому, если задачу возможно решить отображением или фильтрацией, то так и нужно делать.


Дополнительные материалы

  1. Метод reduce
  2. Функция has из библиотеки Lodash
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →