JavaScript 101: метод массива Reduce

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

Чтобы восстановить справедливость, рассмотрим его возможности и, что наиболее важно, удобство в применении. 

Назначение метода Reduce

Метод reduce применяет функцию callback к каждому элементу, но в отличие от методов map и forEach сохраняет последний результат вызова этой функции. В итоге он возвращает последнее полученное значение.

Смысл этого сложного определения сводится к простой идее: данный метод помогает уменьшить массив элементов. Как правило, reduce сокращает список элементов до одного значения, но этим его возможности не ограничиваются.

А самое интересное в отношении reduce заключается в том, что его можно задействовать как map, filter или любой другой предпочитаемый вами метод. Зачем его ненавидеть, когда можно с ним подружиться!

Синтаксис метода Reduce

С виду данный метод довольно прост. Он требует наличия двух параметров: callback и initialValue (начальное значение): 

array.reduce(callback, initialValue)

По-настоящему сложной может оказаться сигнатура функции callback. Опасения вызваны наличием у нее нескольких необязательных параметров. 

Атрибуты первого параметра callback:

  • previousValue  —  возвращаемое значение от предыдущего выполнения функции callback или, если это первое значение, то оно является содержимым второго параметра, переданного в метод reduce (initialValue). 
  • currentValue  —  текущий обрабатываемый элемент массива. 
  • currentIndex (необязательный)  —  индекс текущего элемента массива. Целесообразен в случае зависимости логики от положения элемента. В противном случае можно обойтись и без него. 
  • array (необязательный)  —  обрабатываемый массив целиком. Если в нем нет необходимости, вы его не указываете. 

initialValue, второй параметр метода reduce, представляет собой значение, которое присваивается previousValue в функции callback при первом ее вызове. 

Применение метода Reduce

На этом этапе начинается все самое увлекательное. Что можно cделать с помощью метода reduce? Да что угодно! 

Как говорилось ранее, вы можете рассматривать reduce как строительный блок для других методов.

Пример с методом map:

let myArr = [1,2,3,4,5]

//метод map
console.log(myArr.map( x => x * 2) )
/// возвращает [2,4,6,8,10]

//метод reduce
myArr.reduce( (list, current) => {
list.push(current * 2)
return list
}, [])

При каждой итерации мы получаем новый массив с удвоенными значениями элементов. Начальное значение, т.е. второй параметр reduce,  —  пустой новый массив. 

Пример с методом filter:

let myArr = [1,"hi", 2, "bye", 3, "another string", 4]

//метод filter
let onlyStrings = myArr.filter( elem => typeof elem == "string")
// возвращает ["hi", "bye", "another string"]


//метод reduce
let onlyStrings = myArr.reduce( (list, elem) => {
if(typeof elem == "string") {
list.push(elem)
}
return list
}, [])

Улавливаете принцип? Тело функции callback позволяет делать все что угодно. Вы можете воспроизвести любой другой метод. 

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

Но вы здесь не за этим. Конечно, это все любопытно, но если бы вы хотели отфильтровать список, вы бы воспользовались filter. Если бы потребовалось преобразовать каждый элемент, вы бы задействовали map. На что же еще способен reduce?

Дополнительные практические примеры с reduce

Метод reduce также часто используется для сокращения набора элементов до одного. Приведу пример. Один из моих проектов был посвящен поиску наилучшей темы на основе генетического алгоритма. В этом проекте я вычислял общий показатель “совершенства” темы путем сложения мер различий (distances) между исследуемой темой и ее идеальным аналогом. Задача решалась следующим образом:

function getThemeScore(theme, ideals) {
let score = 0;

score = ideals.reduce((prev, curr) => prev + getDistance(theme, curr), score)
if(score === 0) return 0.0001
return score;
}

Рассмотрим метод reduce в рамках реального практического примера. Я сокращаю список  —  длины переменной  —  сложных объектов до одного, а именно score. С этой целью для score изначально устанавливается значение 0, затем проводится итерация по списку, и текущее значение каждой темы (мера отличия данной темы от “идеала”) складывается с суммарным показателем.

Окончательный результат  —  сложение всех мер различий. 

Конечно, в данном примере подошел бы и метод forEach, поскольку итерация проводится по всему списку элементов. Однако reduce предусматривает очень простой синтаксис для таких случаев. 

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

const isLogggedIn = (req) => {
if(req.user) {
return req
}
throw new UserNotLoggedInError("User is not logged-in!")
}

const isJWTValid = (req) => {
if(validateJWT(req.headers.token)) {
return req
}
throw new JWTError("JWT token expired")
}

const parseBody = (req) => {
if(req.type === "json") {
try {
req.body = JSON.parse(req.body)
} catch (e) {
throw new InvalidJSONBody(e)
}
}
return req
}

let middlewareList = [isLoggedIn, isJWTValid, parseBody]

function runMiddlewares(req, middlewares) {

try {
req = middlewares.reduce( (currentReq, middleware) => middleware(currentReq), req)
} catch (e) {
console.error("There was a problem with the request: ", e.message)
}
}

runMiddlewares(request, middlewareList)

Здесь я попытался привести пример конвейера функций, наиболее приближенный к практическим реалиям. Если вы занимались разработкой бэкенда, то сталкивались с похожей ситуацией, обязывающей к выполнению нескольких операций валидации и преобразования “запроса”. 

Эту логику можно изолировать в отдельные функции (как правило, отдельные библиотеки) и затем добавить в список, в котором они смогут выполняться в конвейере подобным образом. 

Каждая функция либо преобразует, либо проводит валидацию объекта, возвращаемого предыдущей функцией. И поскольку данный сценарий приближен к жизни, то при появлении проблемы мы выбрасываем ошибки для прерывания цикла.

Как вариант, мы можем найти способ собрать все ошибки и в конце принять решение: разрешить запросу дальнейшее выполнение или нет. 

Но суть в том, что метод reduce позволяет запустить весь конвейер в одной строке кода! 

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

На самом деле, вы должно быть разочарованы подобно ребенку, который узнал секрет фокуса с отделением пальца руки. Метод reduce  —  это не волшебная палочка, а новый эффективный инструмент в арсенале разработчика. Пользуйтесь им в свое удовольствие! 

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Fernando Doglio: JavaScript 101: The Array Reduce Method

Предыдущая статья#01TheNotSoToughML | Что означает “подогнать линию”
Следующая статьяКак быстро создать и развернуть веб-приложение на Python