JS: Функции
2026-02-26 17:19 Diff

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

reduce() устроен немного сложнее, чем map() и filter(), но, в целом, сохраняет общий подход с передачей функции. Реализуем код, находящий общее количество денег у группы людей. Здесь сразу прослеживается агрегация, нам нужно свести количество денег всех пользователей к одному значению:

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

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

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

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

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

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

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

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

Разберем пошагово работу функции reduce(). В функцию передается колбек, который принимает два параметра acc и user. Чтобы лучше понять работу, нужно проследить чему равны значения этих параметров на каждой итерации:

  1. На первой итерации acc равен пустому объекту, это начальное значение аккумулятора задается вторым параметром, users.reduce(cb, {}) — здесь вторым параметром передается пустой объект. Параметр user равен первому элементу массива, то есть { name: 'Petr', age: 4 }. В пустом объекте создается массив под ключом user.age и в этот массив добавляется текущее имя пользователя. В итоге acc становится равен объекту { 4: ['Petr'] }. Из функции возвращается acc — это значение будет аккумулятором на следующей итерации
  2. На второй итерации acc равен значению, которое вернулось из предыдущей итерации, это объект { 4: ['Petr'] }. Параметр user равен второму элементу массива { name: 'Igor', age: 19 }. В аккумуляторе acc нет ключа с возрастом текущего пользователя, поэтому добавляется новый ключ и массив. После заполнения acc равен { 4: ['Petr'], 19: ['Igor'] }, этот объект возвращается из функции
  3. На этой итерации acc равен объекту, вернувшемуся из прошлой итерации { 4: ['Petr'], 19: ['Igor'] }. Параметр user равен { name: 'Ivan', age: 4 }. Значение свойства user.age равно 4 — этот ключ уже имеется в аккумуляторе, поэтому новый ключ не создается, а текущий пользователь добавляется в существующий массив. В итоге аккумулятор равен объекту { 4: ['Petr', 'Ivan'], 19: ['Igor'] } и он возвращается из функции
  4. Последняя итерация. Параметр acc равен { 4: ['Petr', 'Ivan'], 19: ['Igor'] }, а user равен { name: 'Matvey', age: 16 }. Ключа 16 в аккумуляторе нет, поэтому добавляется новый массив в ключ 16, в этот массив добавляется текущий пользователь. В итоге acc будет равен { 4: ['Petr', 'Ivan'], 16: ['Matvey'], 19: ['Igor'] }, этот объект возвращается и в итоге будет результатом работы всего редьюса, так как это последняя итерация

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

Как думать о редьюсе

Распишем алгоритм, который поможет правильно подступаться к задачам, в которых требуется редьюс. Представьте, что перед вами список курсов с уроками внутри них и вам нужно посчитать количество всех уроков. Например, такое может быть нужно для вычисления длительности программы обучения. На Хекслете подобные задачи встречаются регулярно.

Здесь мы видим два курса, в которых суммарно 5 уроков. Попробуем теперь высчитать это число программно. Первый вопрос, на который надо ответить, является ли данная операция агрегацией? Ответ - Да, так как мы сводим исходные данные, к какому-то вычисляемому результату. Дальше смотрим, чем является результат операции. В нашем случае это число, которое вычисляется как сумма уроков в каждом курсе. Значит начальным значением аккумулятора будет 0 (тут можно освежить). Теперь примерный алгоритм:

  1. Инициализируем накапливаемый результат нулем
  2. Обходим коллекцию курсов по одному
    • Прибавляем к аккумулятору количество уроков в текущем курсе

Этот алгоритм будет идентичным в любом варианте решения, как через цикл, так и через редьюс:

Реализация

Напишем свою собственную функцию myReduce(), работающую аналогично методу массива reduce():