Кратко
СкопированоМетод массива 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
в try
, но лучше всегда указывать начальное значение.
Как понять
СкопированоИспользование reduce
похоже на методы for
, 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
мы передаём начальное значение аккумулятора.
// Если мы хотим получить из массива объект, то стартовым значением аккумулятора будет объект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 и использование спред-синтаксиса в качестве аргумента функции.
Давайте создадим такую функцию, назовём её sum
:
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
.