.reduce()

Швейцарский нож для работы с массивами. Заменяет все остальные методы (не повторяйте дома).

Время чтения: 6 мин

Кратко

Скопировано

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

Пример

Скопировано

Находим сумму элементов:

        
          
          const nums = [1, 2, 3, 4, 5, 6, 7, 8]const sum = nums.reduce(function (currentSum, currentNumber) {  return currentSum + currentNumber}, 0)// 36
          const nums = [1, 2, 3, 4, 5, 6, 7, 8]

const sum = nums.reduce(function (currentSum, currentNumber) {
  return currentSum + currentNumber
}, 0)
// 36

        
        
          
        
      

Создаём новый объект с ID и именем юзера:

        
          
          const users = [  { id: "1", name: "John" },  { id: "2", name: "Anna" },  { id: "3", name: "Kate" },]const usernamesById = users.reduce(function (result, user) {  return {    ...result,    [user.id]: user.name,  }}, {})// { '1': 'John', '2': 'Anna', '3': 'Kate' }
          const users = [
  { id: "1", name: "John" },
  { id: "2", name: "Anna" },
  { id: "3", name: "Kate" },
]

const usernamesById = users.reduce(function (result, user) {
  return {
    ...result,
    [user.id]: user.name,
  }
}, {})
// { '1': 'John', '2': 'Anna', '3': 'Kate' }

        
        
          
        
      

Интерактивный пример:

Открыть демо в новой вкладке

Как пишется

Скопировано

Метод reduce() принимает два параметра: функцию-колбэк и начальное значение для аккумулятора.

Сама функция-колбэк может принимать четыре параметра:

  • acc — текущее значение аккумулятора;
  • item — элемент массива в текущей итерации;
  • index — индекс текущего элемента;
  • arr — сам массив, который мы перебираем.
        
          
          const nums = [1, 2, 3, 4, 5, 6, 7, 8]// Не забываем, что аккумулятор идёт первым!function findAverage(acc, item, index, arr) {  const sum = acc + item  // Если мы на последнем элементе  // вычисляем среднее арифметическое делением на кол-во элементов:  if (index === arr.length - 1) {    return sum / arr.length  }  return sum}const average = nums.reduce(findAverage, 0)// 4.5
          const nums = [1, 2, 3, 4, 5, 6, 7, 8]

// Не забываем, что аккумулятор идёт первым!
function findAverage(acc, item, index, arr) {
  const sum = acc + item

  // Если мы на последнем элементе
  // вычисляем среднее арифметическое делением на кол-во элементов:
  if (index === arr.length - 1) {
    return sum / arr.length
  }

  return sum
}

const average = nums.reduce(findAverage, 0)
// 4.5

        
        
          
        
      

Функция обязательно должна возвращать значение, поскольку в каждой следующей итерации значение в acc будет результатом, который вернулся на предыдущем шаге. Логичный вопрос, который может здесь возникнуть — какое значение принимает acc во время первой итерации? Им будет то самое начальное значение, которое передаётся вторым аргументом в метод reduce().

Начальное значение для аккумулятора можно не указывать явно. В таком случае на первой итерации аккумулятор будет равен значению первого элемента в массиве:

        
          
          const arr = [1, 2, 3]const sum = arr.reduce(function (acc, val) {  return acc + val})console.log(sum)// 6
          const arr = [1, 2, 3]
const sum = arr.reduce(function (acc, val) {
  return acc + val
})
console.log(sum)
// 6

        
        
          
        
      

Во фрагменте выше acc на первой итерации равен 1, а val — 2. Затем к полученному аккумулированному значению 3 прибавляется 3, и возвращается результат.

В этом подходе есть краевой случай. Если массив окажется пустым и начальное значение не будет указано, то JavaScript выдаст ошибку TypeError: Reduce of empty array with no initial value. Этот случай нужно обрабатывать отдельно, например, обернув reduce() в try...catch, но лучше всегда указывать начальное значение.

Как понять

Скопировано

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

Главной особенностью reduce(), которую важно запомнить, является наличие аккумулятора. Аккумулятор — это и есть то новое вычисляемое значение. Во время выполнения функции-колбэка нужно обязательно возвращать его значение, поскольку оно попадает в следующую итерацию, где будет использоваться для дальнейших вычислений. Мы можем представить аккумулятор как переменную, значение которой можно поменять в каждой новой итерации. С помощью второго аргумента в reduce() эта переменная получает своё начальное значение.

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

Задача: вычислить сумму денег на всех счетах.

        
          
          const bankAccounts = [  { id: "123", amount: 19 },  { id: "345", amount: 33 },  { id: "567", amount: 4 },  { id: "789", amount: 20 },]const totalAmount = bankAccounts.reduce(  // Аргумент sum является аккумулятором,  // в нём храним промежуточное значение  function (sum, currentAccount) {    // Каждую итерацию берём текущее значение    // и складываем его с количеством денег    // на текущем счету    return sum + currentAccount.amount  },  0 // Начальное значение аккумулятора)console.log(totalAmount)// 76
          const bankAccounts = [
  { id: "123", amount: 19 },
  { id: "345", amount: 33 },
  { id: "567", amount: 4 },
  { id: "789", amount: 20 },
]

const totalAmount = bankAccounts.reduce(
  // Аргумент sum является аккумулятором,
  // в нём храним промежуточное значение
  function (sum, currentAccount) {
    // Каждую итерацию берём текущее значение
    // и складываем его с количеством денег
    // на текущем счету
    return sum + currentAccount.amount
  },
  0 // Начальное значение аккумулятора
)

console.log(totalAmount)
// 76

        
        
          
        
      

Чтобы понять, как это работает, можно взглянуть на код, который делает то же самое, но уже без reduce():

        
          
          const bankAccounts = [  { id: "123", amount: 19 },  { id: "345", amount: 33 },  { id: "567", amount: 4 },  { id: "789", amount: 20 },]
          const bankAccounts = [
  { id: "123", amount: 19 },
  { id: "345", amount: 33 },
  { id: "567", amount: 4 },
  { id: "789", amount: 20 },
]

        
        
          
        
      

Определяем, где будем хранить сумму, это в нашем случае является аккумулятором. Здесь же определяем начальное значение аккумулятора:

        
          
          let totalAmount = 0for (let i = 0; i < bankAccounts.length; i++) {  const currentAccount = bankAccounts[i]  // В каждой итерации прибавляем  // к текущей сумме количество денег на счету  totalAmount += currentAccount.amount}console.log(totalAmount)// 76
          let totalAmount = 0

for (let i = 0; i < bankAccounts.length; i++) {
  const currentAccount = bankAccounts[i]

  // В каждой итерации прибавляем
  // к текущей сумме количество денег на счету
  totalAmount += currentAccount.amount
}

console.log(totalAmount)
// 76

        
        
          
        
      

И в том, и в том другом примере у нас аккумулятор, где хранится текущее значение и кладётся новое, есть вычисление нового значение. Только reduce() позволяет сделать это в одном месте и в более понятном декларативном стиле.

Подсказки

Скопировано

💡 Ключ к успешному использованию reduce() — внимательно следить за порядком аргументов и не забывать возвращать значение.

На практике

Скопировано

Егор Огарков советует

Скопировано

🛠 reduce() действительно часто применяется для того, чтобы провести математическую операцию для всех элементов массиве и получить в итоге какой-то результат.

🛠 Если вы хотите применить подряд несколько операций filter и map, то с помощью reduce() их можно объединить в одной функции. Иногда это может быть необходимо в целях производительности, поскольку в этом случае будет всего один проход по массиву вместо нескольких в зависимости от количества вызываемых методов. Но стоит помнить, что такой способ не всегда будет хорошо читаться.

Задача: выбрать чётные, вычислить их квадраты и отобрать из них числа больше 50.

        
          
          const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]function filterEven(num) {  return num % 2 === 0}function square(num) {  return num * num}function filterGreaterThanFifty(num) {  return num > 50}
          const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

function filterEven(num) {
  return num % 2 === 0
}

function square(num) {
  return num * num
}

function filterGreaterThanFifty(num) {
  return num > 50
}

        
        
          
        
      

Применяем несколько методов:

        
          
          const result = numbers  .filter(filterEven)  .map(square)  .filter(filterGreaterThanFifty)console.log(result)// [64, 100]
          const result = numbers
  .filter(filterEven)
  .map(square)
  .filter(filterGreaterThanFifty)

console.log(result)
// [64, 100]

        
        
          
        
      

Через один reduce():

        
          
          const result = numbers.reduce(function (res, num) {  if (filterEven(num)) {    const squared = square(num)    if (filterGreaterThanFifty(squared)) {      res.push(squared)    }  }  return res}, [])console.log(result)// [64, 100]
          const result = numbers.reduce(function (res, num) {
  if (filterEven(num)) {
    const squared = square(num)

    if (filterGreaterThanFifty(squared)) {
      res.push(squared)
    }
  }

  return res
}, [])

console.log(result)
// [64, 100]

        
        
          
        
      

🛠 Часто встречается использование reduce() для нормирования значений. Например, для превращения массива с данными пользователей в объект, где ключом будет ID пользователя, а значением — исходный объект. Таким образом можно быстро получать значение объект-пользователя по id, обратившись по ключу к объекту, вместо поиска по массиву:

        
          
          const users = [  { id: "123", name: "Vasiliy", age: 18 },  { id: "345", name: "Anna", age: 22 },  { id: "567", name: "Igor", age: 20 },  { id: "789", name: "Irina", age: 24 },]const usersById = users.reduce(function (result, user) {  result[user.id] = {    name: user.name,    age: user.age,  }  return result}, {})console.log(usersById["567"]);// { name: 'Igor', age: 20 }
          const users = [
  { id: "123", name: "Vasiliy", age: 18 },
  { id: "345", name: "Anna", age: 22 },
  { id: "567", name: "Igor", age: 20 },
  { id: "789", name: "Irina", age: 24 },
]

const usersById = users.reduce(function (result, user) {
  result[user.id] = {
    name: user.name,
    age: user.age,
  }

  return result
}, {})

console.log(usersById["567"]);
// { name: 'Igor', age: 20 }

        
        
          
        
      

На собеседовании

Скопировано
Задать вопрос в рубрику
🤚 Я знаю ответ

Наставник Практикума
Алексей Руденко  отвечает

Скопировано

Решение

Скопировано

Эта задача решается буквально в одну строчку. Давайте посмотрим на решение, а затем немного разберём его.

        
          
          const sum = (...args) => args.reduce((acc, currentValue) => acc + currentValue);sum(1, 2, 3); // 6sum('1', 2, 3); // '123'sum(1); // 1
          const sum = (...args) => args.reduce((acc, currentValue) => acc + currentValue);

sum(1, 2, 3); // 6
sum('1', 2, 3); // '123'
sum(1); // 1

        
        
          
        
      

По условиям мы не знаем, какое количество аргументов будет передано. Мы используем синтаксис остаточных параметров (rest) в сигнатуре функции. Это позволит преобразовать любое количество аргументов функции в массив args.

С массивом гораздо удобнее работать – мы можем использовать метод Array.reduce(). Его колбэк будет вызываться для каждого элемента массива, и значение каждого аргумента будет прибавляться к значению в аккумуляторе. Когда начальное значение аккумулятора не указано, метод использует первый элемент массива, а выполнение колбэк начнёт со второго элемента.

Какую ошибку легко допустить

Скопировано

Мы обсудили выше, что reduce() в своём колбэке использует аккумулятор – это переменная, которая накапливает в себе результаты прошлых итераций. Часто, используя reduce() мы передаём начальное значение аккумулятора.

        
          
          // Если мы хотим получить из массива объект, то стартовым значением аккумулятора будет объектconst list = [  { key: 'name', value: 'John' },  { key: 'age', value: 30 },  { key: 'city', value: 'New York' }];const obj = list.reduce((accumulator, currentItem) => {  accumulator[currentItem.key] = currentItem.value;  return accumulator;}, {});
          // Если мы хотим получить из массива объект, то стартовым значением аккумулятора будет объект

const list = [
  { key: 'name', value: 'John' },
  { key: 'age', value: 30 },
  { key: 'city', value: 'New York' }
];

const obj = list.reduce((accumulator, currentItem) => {
  accumulator[currentItem.key] = currentItem.value;
  return accumulator;
}, {});

        
        
          
        
      

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

Давайте разберём такой вариант этой функции, в которой мы укажем ноль как стартовое значение:

        
          
          const sum = (...args) => args.reduce((acc, currentValue) => acc + currentValue, 0);sum(1, 2, 3); // 6sum('1', 2, 3); // '0123'
          const sum = (...args) => args.reduce((acc, currentValue) => acc + currentValue, 0);

sum(1, 2, 3); // 6
sum('1', 2, 3); // '0123'

        
        
          
        
      

Пока мы имеем дело с числами – результат ожидаемый. Но почему результат выполнения во втором примере с нулём?

Неявное преобразование типов

Скопировано

Да, дело в той особенности языка, которая дарит нам столько весёлых шуток про JavaScript.

Оператор сложения приводит оба операнда к строковому формату в случае, если хотя бы один из них – строка. Таким образом мы и получаем 0125 в примере выше – на первой же итерации строка '1' складывается с нулём, заданным как первоначальное значение для reduce().

Давайте вернёмся к первому решению задачи и посмотрим детальнее, как там решается эта проблема.

        
          
          const sum = (...args) => args.reduce((acc, currentValue) => acc + currentValue);sum('1', 2, 3); // '123'
          const sum = (...args) => args.reduce((acc, currentValue) => acc + currentValue);

sum('1', 2, 3); // '123'

        
        
          
        
      

На первой итерации аккумулятор будет равен значению первого элемента в массиве, а мы получим на первой итерации вот такую операцию: '1' + 2. Как я писал выше, бинарный оператор + приведёт оба операнда к строковому формату в случае, если хотя бы один из них – строка.

Пока в массиве встречаются числа – они будут арифметически складываться. Как только в ряду аргументов наш колбэк встретит строку, он начнёт приводить все аргументы к строке и конкатенировать их.

Граничный случай

Скопировано

Мы успешно справились с главным условием! Но теперь давайте подумаем, как быть, когда в функцию sum() не передали вообще никаких аргументов? Просто проверим, что длина массива ненулевая. Rest-синтаксис создаст пустой массив, если не будет передано никаких аргументов:

        
          
          const sum = (...args) => args.length ? args.reduce((acc, currentValue) => acc + currentValue) : 0;sum('1', 2, 3); // '123'sum(1, 2, 3); // 6sum(); // 0
          const sum = (...args) => args.length ? args.reduce((acc, currentValue) => acc + currentValue) : 0;

sum('1', 2, 3); // '123'
sum(1, 2, 3); // 6
sum(); // 0

        
        
          
        
      

Мы используем тернарный оператор для проверки длины массива, а наша функция по-прежнему умещается лишь в одну строку!

Скопировано

В такой задаче у вас хотят проверить два навыка: владение методом массивов reduce и использование спред-синтаксиса в качестве аргумента функции.

Давайте создадим такую функцию, назовём её sumOrConcat:

        
          
          function sumOrConcat(...rest) {  ...}
          function sumOrConcat(...rest) {
  ...
}

        
        
          
        
      

Первое что мы сделаем — добавим спред-синтаксис в аргументы функции. Таким образом, не зависимо от количества переданных аргументов, внутри функции нам будет доступна переменная rest, которая будет массивом, включающим в себя все переданные при вызове параметры.

Следующий шаг - добавим к нашему массиву (переменная rest) метод reduce. За счёт этого лаконичного метода мы легко выполним условия задачи.

        
          
          function sumOrConcat(...rest) {  return rest.reduce((sum, num) => (sum += num));}
          function sumOrConcat(...rest) {
  return rest.reduce((sum, num) => (sum += num));
}

        
        
          
        
      

В данном случае метод reduce будет складывать и аккумулировать результат для значений типа number, а так же выполнит конкатенацию, если вдруг встретится со значением типа string.