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

Уменьшать? Могу ли я делать вещи меньше? Меньше… э?

Ну… Да и нет.

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

MDN определяет reduce как:

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

Это хорошее определение, которое может привести вас к мысли, что вы всегда должны выдавать одно значение, например число или строку. Хотя технически это верно, вы все равно можете создать другой массив (который содержит много значений) или объект (настолько сложный, насколько вы хотите) или массив объектов. Это означает, что вы можете reduce преобразовать массив из 3 строк в одно число или в массив из 200 очень сложных объектов. Теперь, если это не является мощным, я не знаю, что это такое.

Как это работает?

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

Сама по себе внутренняя работа reduce проста.
абстрактным рецептом будет:

  1. взять массив значений,
  2. добавить функцию, которая будет запускаться для каждого элемента,
  3. убедитесь, что функция возвращает значение,
  4. использовать это возвращаемое значение при работе со следующим элементом,
  5. добавить щепотку соли,
  6. примите возврат функции в последнем элементе в качестве окончательного значения.

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

Вот пример создания объекта из массива значений:

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

Начиная с малого

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

Функции высшего порядка

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

Википедия определяет их как:

В математике и информатике функция высшего порядка - это функция, которая выполняет по крайней мере одно из следующего:
- принимает одну или несколько функций в качестве аргументов (то есть процедурных параметров),
- в качестве результата возвращает функцию.

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

Начать сокращение

Базовую функцию reduce легко определить:

myArray.reduce(callbackFunction, initialValue)

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

Вы с полным правом можете спросить: а как насчет первого элемента массива? Где «результат» предыдущего шага? Это очень хороший вопрос! Поэтому reduce принимает еще один параметр - initialValue. Мы можем жестко запрограммировать начальное значение для первого элемента, как если бы оно было возвратом из предыдущего элемента. Например: если бы мы должны были написать функцию, которая суммировала бы все числа в массиве (спойлер: что мы собираемся сделать), естественным начальным значением было бы ноль, потому что это отличный отправная точка для добавления.

Очень функциональный

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

const callbackFunction = function(
    accumulator, 
    currentValue, 
    currentIndex, 
    array) { ... }

Хотя сначала это кажется немного пугающим, на самом деле это довольно легко понять:

  • accumulator представляет значение, возвращенное из предыдущего вызова функции. Если мы находимся на первом элементе, как вы, наверное, догадались, он будет содержать initialValue.
  • currentValue - текущий элемент массива, для которого вызывается функция.
  • currentIndex представляет индекс текущего элемента.
  • array - это массив, который мы сокращаем прямо сейчас.

В большинстве случаев вас будут интересовать accumulator и currentValue, потому что они позволят вам улучшить конечный результат. accumulator и array полезно иметь для работы с еще более сложной логикой сокращения (где вам нужно знать, какую часть массива вы уже прошли).

Если вам когда-нибудь понадобится вспомнить, что делает каждая часть редуктора, вот удобное изображение:

Наш первый редуктор - уменьшение массива до одного значения

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

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

Итак, как это работает?

Если представить себе это как рабочий процесс, это действительно просто:

  1. Первый элемент имеет начальное значение (0) в качестве аккумулятора, а текущий элемент - 100 (первый элемент). Они складываются (0 + 100 = 100) и возвращаются.
  2. Аккумулятор для второго элемента - это возвращаемое значение от вызова функции для первого элемента (100). Текущий (второй) элемент равен 200. Они складываются (100 + 200 = 300) и возвращаются в качестве аккумулятора для следующего элемента.
  3. Третий элемент получает аккумулятор 300 (возвращаемое значение из предыдущего шага). Текущий элемент - 300. Когда мы складываем их вместе (300 + 300 = 600), мы возвращаем результат, и, поскольку элементов больше нет, 600 принимается как окончательное значение.

Это может показаться тривиальным, но именно так работает reduce даже в более сложных случаях.

Здесь пара моментов:

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

Следующая задача - изменение каждого элемента массива

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

Вот как могло бы выглядеть потенциальное исправление:

Уф. Здесь много чего происходит. К счастью, логика reduce осталась прежней:

  1. Первый элемент попадает в функцию (Майкл Скотт), а начальное значение - пустой массив. Это потому, что мы будем заполнять этот массив исправленными людьми и возвращать этот постоянно растущий массив на следующий шаг. Имя человека исправляется (michael → Michael), и исправленный человек добавляется в массив (который теперь состоит из 1 элемента: Michael Scott).
  2. Второй шаг получает аккумулятор из предыдущего шага (который представляет собой массив с 1 человеком) и текущий элемент (которым является Джим Халперт). Человек фиксируется (Джим → Джим) и добавляется в массив (который теперь содержит 2 человека: Майкла и Джима). Этот массив возвращается и становится аккумулятором для третьего элемента.
  3. Третий шаг получает аккумулятор с двумя людьми (Майкл и Джим) и текущий (третий) элемент (дуайт). Дуайт исправляется, и мы возвращаем массив со всеми тремя элементами (Майкл, Джим и Дуайт - вероятно, пошалить). Поскольку это наш последний элемент, это также будет окончательное значение.

Мы также можем попытаться визуализировать это:

Давай получим мету

До сих пор вы видели, как работать с массивами разных данных и как получать разные результаты на основе этих данных. Но как насчет создания чего-то действительно особенного? А как насчет создания универсальной функции, которая будет делать то же самое, что и функция map()?

Давай попробуем и посмотрим, что получится.

Мы создали новую функцию под названием mapFunction(). Эта функция является универсальной функцией карты, которая просматривает все элементы массива и применяет заданную функцию к каждому из них, а затем возвращает новый массив. Это то же самое, что и встроенная функция map().

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

const upperAll = function(element) { return element.toUpperCase(); }

После этого вызываем нашу mapFunction с новой функцией:

const upperPeople = mapFunction(people, upperAll);

Еще упражнения?

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

Уменьшить все вещи

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

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

Инвестируйте в преобразование данных с помощью reduce, и вы получите от этого огромную пользу.

Но подождите, а как насчет исходного примера? 😮

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