Три функции JavaScript для освоения метода Reduce

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

Давайте приподнимем эту завесу таинственности, пополним багаж знаний о них и напишем что-нибудь на JavaScript! Начнём с основ: узнаем, что такое редьюсеры и как выглядит функция reduce. Затем создадим более сложные функции и пошагово разберём их выполнение. И наконец, применим эти функции к результатам, полученным с API базы данных о фильмах theMovieDb.org. Итак, приступим!

Основы

Библиотеки типа Redux задействуют редьюсеры для управления состоянием больших веб-приложений. Однако функции редьюсера можно использовать и для изменения состояния отдельной структуры данных.

Рассмотрим пример, иллюстрирующий свёртку массива до целого числа:

const add = (accumulator, currentValue) => accumulator + currentValue // функция редьюсера
[1,2,3,4,5].reduce(add) // => 15 // метод, в котором происходит выполнение функции редьюсера

Здесь редьюсер add() принимает два аргумента: аккумулятор и текущее значение. В методе Array.Reduce() выполняется редьюсер, а его возвращаемое значение устанавливается равным аккумулятору. Это продолжится до тех пор, пока итерация массива не завершится. При вызове аккумулятор имеет исходное значение. Это необязательная переменная. Если исходное значение не задано, аккумулятору устанавливается значение первого элемента массива.

Если пока немного непонятно, не переживайте! Мы скоро всё подробно разберём. Начнём с создания функции, которая воспроизводит поведение Array.Reduce():

const reduce = (array, reducer, initValue) => {
    let accumulator = (!initValue) ? array.shift() : initValue
    array.forEach((el) => accumulator = reducer(accumulator, el))
    return accumulator
}

let reduce = reduce([1,2,3], add) // => 6

let reduceWithInitValue = reduce([1,2,3], add, 10)) // => 16

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

// пример использования const reduceWithInitValue
array = [1,2,3], initialValue = 10 

// в первой итерации
accumulator = initialValue = 10
currentValue = 1 
reducer = add(10, 1)
accumulator = reducer

// вторая итерация
accumulator = 11
currentValue = 2
reducer = add(11, 2)
accumulator = reducer

// третья итерация 
accumulator = 13
currentValue = 3
reducer = add(13, 3) 
accumalator = reducer

// конечный результат => 16

В этом примере показывается, как значение аккумулятора на каждой итерации увеличивается, а текущее значение добавляется к аккумулятору через функцию редьюсера (в нашем случае это функция add). Усвоив всю эту информацию, вы будете на правильном пути к пониманию метода reduce!

Следующий этап

Допустим, у нас есть большой набор данных, который надо свести к пересекающимся результатам. Как это сделать? Одно из решений  —  выполнить перебор данных и создать единый массив, содержащий все перекрывающиеся результаты. Начнём с написания функции, которая сворачивает всю пересекающуюся информацию:

const intersectionWithReduce = (...arrays) => {
  // массивы  => [ [ 1, 2, 3, 4 ], [ 2, 3, 4 ], [ 3, 45, 5 ] ]
  const reducer = (accumulator, currentArray) =>
    currentArray.filter((currentArrayItem) =>
      accumulator.includes(currentArrayItem)
    );
  return reduce(arrays, reducer);
};

const intersect = intersectionWithReduce([1,2,3,4], [2,3,4,9], [3,45,5,2], [3]) // => 3

Не забывайте, что reduce() заменит аккумулятор на возвращаемое значение функции reducer. А reducer в intersectionWithReduce() отсеивает все элементы массива currentArray, которые не включены в аккумулятор. Затем в аккумулятор устанавливается возвращаемое значение прошедшего фильтрацию массива currentArray. Это продолжится до тех пор, пока в аккумуляторе не останется один массив всех пересекающихся значений. Посмотрим, как это делается:

// пример использования const intersect
arrays = [[1, 2, 3, 4], [2, 3, 4, 9], [3, 45, 5, 2], [3]];

// первая итерация
accumulator = [1, 2, 3, 4];
currentArray = [2, 3, 4, 9];
//currentArray.filter(arrayItem => accumalator.includes(arrayItem)
reducer = [2, 3, 4, 9].filter((arrayItem) => [1, 2, 3, 4].includes(arrayItem));

// вторая итерация
accumulator = [2, 3, 4];
currentArray = [3, 45, 5, 2];
reducer = [3, 45, 5, 2].filter((arrayItem) => [2, 3, 4].includes(arrayItem));

// третья итерация
accumulator = [3, 2];
currentArray = [3];
reducer = [3].filter((arrayItem) => [3, 2].includes(arrayItem));

// конечный результат => [3]

И вот теперь начинаем задействовать потенциал редьюсеров! Напишем функцию, которая создаёт единый массив всех результатов без дублирования:

const joinWithReduce = (...arrays) => {
  const reducer = (accumulator, currentArray) =>
    accumulator.concat(
      currentArray.filter(
        (currentArrayItem) => !accumulator.includes(currentArrayItem)
      )
    );
  return reduce(arrays, reducer);
};

joinWithReduce() конкатенирует или добавляет к аккумулятору значения currentArray, которые ещё не включены в аккумулятор. Надеюсь, вы уже можете предвидеть, как будет выполняться эта функция. Разберём её в последний раз:

// пример использования const joined
arrays = [
  [1, 10, 15, 20],
  [5, 1, 7],
  [1, 10, 15, 5],
];

// первая итерация
accumulator = [1, 10, 15, 20];
currentValue = [5, 1, 7];
// acc.concat(curr.filter(el => !acc.includes(el)));
reducer = [(1, 10, 15, 20)].concat(
  [5, 1, 7].filter(
    (currentArrayItem) => ![1, 10, 15, 20].includes(currentArrayItem)
  )
);
// вторая итерация
accumulator = [1, 10, 15, 20, 5, 7];
currentValue = [1, 10, 15, 5];
reducer = [(1, 10, 15, 20, 5, 7)].concat(
  [1, 10, 15, 5].filter(
    (currentArrayItem) => ![1, 10, 15, 20, 5, 7].includes(currentArrayItem)
  )
);

// конечный результат => [ 1, 10, 15, 20, 5, 7]

Reduce в действии

А теперь испытаем reduce в деле: используем созданные функции для сворачивания данных, полученных на запросы к API. Задача  —  найти фильмы, которые входят в 20 лучших по кассовым сборам и количеству голосов пользователей. Затем объединим всё это с результатами API-запросов. Первым делом надо запросить эти результаты с URL-адреса конечной точки:

let moviesByVote = "https://api.themoviedb.org/3/discover/movie?api_key=7d5fc19bc307c5d1ca314e7fb11bf51e&language=en-US&sort_by=vote_count.desc&include_adult=false&include_video=false&page=1"
let moviesByRevenue = "https://api.themoviedb.org/3/discover/movie?api_key=7d5fc19bc307c5d1ca314e7fb11bf51e&language=en-US&sort_by=revenue.desc&include_adult=false&include_video=false&page=1"

async function movies(url) {
    let response = await fetch(url)
    return response.json();
}

Мы написали асинхронную функцию movies(), чтобы вернуть объект promise с данными. Промисы позволяют писать неблокирующий асинхронный код. Давайте создадим промис для каждого ответа с API и задействуем метод Promise.All() для создания массива результатов, возвращаемых из объектов promise:

async function movies(url) {
    let response = await fetch(url)
    return response.json();
}

Promise.all([movies(moviesByVote), movies(moviesByRevenue)]).then((values) => {
    values[0] // => содержит ответ moviesByVote (фильмы по количеству голосов пользователей)
    values[1] // => содержит ответ moviesByRevenue (фильмы по кассовым сборам)
})

Теперь будем сворачивать названия фильмов, поэтому соберём всю эту информацию в массив:

async function movies(url) {
    let response = await fetch(url)
    return response.json();
}

function getTitles(responseObject) {
    const responseArray = Object.values(responseObject)
    const movieData = responseArray[3]
    return movieData.map((movie) => { return movie.title })
}

Promise.all([movies(moviesByVote), movies(moviesByRevenue)]).then((values) => {
    const moviesByVoteTitles = getTitles(values[0]) // => содержит названия фильмов по количеству голосов пользователей 
    const moviesByRevenueTitles = getTitles(values[1]) // => содержит названия фильмов по кассовым сборам 
})

Object.Value() преобразует response object (объект-ответ) в массив значений. После чего getTitles() отделяет от ответа данные о фильмах и использует их для создания массива с названиями фильмов. Теперь всё готово к реализации функций! Вызовем в данных функцию intersectionWithReduce() и посмотрим, что мы получим:

async function movies(url) {
    let response = await fetch(url)
    return response.json();
}
function getTitles(responseObject) {
    const responseArray = Object.values(responseObject)
    const movieData = responseArray[3]
    return movieData.map((movie) => movie.title)
}

Promise.all([movies(moviesByVote), movies(moviesByRevenue)]).then((values) => {
    const moviesByVoteTitles = getTitles(values[0]) // => содержит названия фильмов по количеству голосов пользователей 
    const moviesByRevenueTitles = getTitles(values[1]) // => содержит названия фильмов по кассовым сборам 
    const intersectingTitles = intersectionWithReduce(moviesByRevenueTitles, moviesByVoteTitles) 
    intersectingTitles // => [ «Мстители», «Аватар», «Мстители: Война бесконечности», «Титаник» ]
})

intersectingTitles содержит массив из четырёх очень популярных фильмов. Теперь для объединения данных используем joinWithReduce():

async function movies(url) {
    let response = await fetch(url)
    return response.json();
}
function getTitles(responseObject) {
    const responseArray = Object.values(responseObject)
    const movieData = responseArray[3]
    return movieData.map((movie) => movie.title)
}

Promise.all([movies(moviesByVote), movies(moviesByRevenue)]).then((values) => {
    const moviesByVoteTitles = getTitles(values[0]) // => содержит названия фильмов по количеству голосов пользователей 
    const moviesByRevenueTitles = getTitles(values[1]) // => содержит названия фильмов по кассовым сборам 
    const joinTitles = joinWithReduce(moviesByRevenueTitles, moviesByVoteTitles)
    joinTitles.length // => 36
})

В joinTitles у нас массив из 36 фильмов. Этот список не содержит дублей тех четырёх фильмов из двух первых массивов.

Заключение

Надеюсь, эта статья помогла вам понять, как с помощью reduce можно прокачать навыки работы с JavaScript. Код для финальной задачи доступен здесь. В нём есть все созданные нами функции. Спасибо за внимание и не забывайте продолжать работать кодом!

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

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Anthony Jimenez: Learn These Three JavaScript Functions and Become a Reduce Master!