Главная | Все статьи | Дневник студента

JS. Просто о сложном: filter, map, reduce

Время чтения статьи ~8 минут
Статья написана студентом Хекслета. Мнение автора может не совпадать с позицией редакции
JS. Просто о сложном: filter, map, reduce главное изображение

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

Этому весьма способствует сложившаяся традиция демонстрировать логику таких функций абстрактными примерами, в которых суммируются какие-то a и b:

// Пример с MDN

var total = [0, 1, 2, 3].reduce(function(a, b) {
  return a + b;
});

// Вроде бы ничего сложного, но как 
// это использовать в своем коде — совершенно непонятно.

Я предлагаю разобрать принцип действия filter, map и reduce на примере, приближенном к жизни и наконец-то разложить все по полочкам.

Жизненный пример

Представим, что, отчаявшись разобраться с функциями высшего порядка самостоятельно, вы создаете топик в блоге с приблизительно таким заголовком:

'Пробую использовать функции высшего порядка,
 но ничего не выходит!!!!!'

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

const comments = [
   {
    id: 1,
    authorName: 'Глеб Фильтеровский',
    authorRating: 5404,
    text: 'Присоединяюсь, из трех функций понял только filter!'
  },
  {
    id: 2,
    authorName: 'Иван Редьюсов',
    authorRating: 348,
    text: 'Используйте console.log для отладки и сами все поймете!'
  },
  {
    id: 3,
    authorName: 'Анна Мэп',
    authorRating: 1892,
    text: 'Посмотрите гайды на YouTube, там все объясняется.'
  },
  {
    id: 4,
    authorName: 'Анна Мэп',
    authorRating: 1892,
    text: 'Кстати, использовать console.log — отличная идея!'
  },
  {
    id: 5,
    authorName: 'Иван Редьюсов',
    authorRating: 348,
    text: 'Если вы приложите ревью, нам будет проще вам помочь.'
  },
];

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

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

I. Filter

Функция filter самая простая и понятная из великолепной тройки. Она проходится по массиву и отбирает только те элементы, которые подходят под заданное условие. А те, которые не подходят, соответственно, игнорирует.

Допустим, мы хотим выбрать из нашей коллекции только те комментарии, в которых упоминается console.log: в конце концов, отладка — это самый важный инструмент для понимания происходящего в коде, наверняка такой совет поступит не единожды.

Применим filter:

// Вот так выглядит наша функция:

comments.filter((comment) => comment.text.match(/console.log/));

// если вы еще не знаете, что такое 
// регулярные выражения, ничего страшного. 
// Метод .match(/console.log/) проверяет, 
// содержится ли в искомой строке 
// подстрока 'console.log', вот, собственно, и все.


Что здесь произошло? В функцию filter мы передали callback — по большому счету, просто функцию, аргументом которой является наш элемент коллекции — комментарий пользователя.

Для каждого элемента коллекции мы выбрали интересующую нас деталь, а именно, текст комментария comment.text, и проверили, содержит ли текст подстроку «console.log». Если это так, callback вернет true, и весь наш объект-комментарий будет добавлен в результат. В противном случае, callback вернет false, и результат не изменится.

Вот, кстати, и он:

[
  {
    id: 2,
    authorName: 'Иван Редьюсов',
    authorRating: 348,
    text: 'Используйте console.log для отладки и сами все поймете!'
  },
  {
    id: 4,
    authorName: 'Анна Мэп',
    authorRating: 1892,
    text: 'Кстати, использовать console.log — отличная идея!'
  }
]

Мы успешно отфильтровали комментарии по условию, дело сделано!

Рекомендую вам самостоятельно потестировать этот код на repl.it, чтобы убедиться, что все работает именно так. Последуйте совету Ивана Редьюсова — используйте отладку и посмотрите, как добавляются элементы ;)

II. Map

Функция map немного сложнее. Для каждого обработанного элемента коллекции она добавит в результат один элемент, измененный так, как мы укажем в callback функции.

[1, 2, 3].map((num) => 5);
// [5, 5, 5]

В этом примере мы даже не изменили, а заменили элементы. Функция вернула 5 на каждый элемент изначального массива. Практического смысла в этом немного, но сам механизм вы должны понимать — мы могли бы вставить вместо числа массив или объект, и функция map также заполнила бы результирующий массив указанными сущностями в соотношении 1:1 (один элемент изначального массива — один элемент конечного массива, вне зависимости от его внутренней сложности).

Вернемся к нашему массиву комментариев. Допустим, мы хотим получить коллекцию имен всех пользователей, которые отписались в вашем топике:

// Вот так выглядит наша функция

comments.map((comment) => comment.authorName);

// Мы взяли из каждого комментария имя автора 
// и добавили его в результат:

[
  'Глеб Фильтеровский',
  'Иван Редьюсов',
  'Анна Мэп',
  'Анна Мэп',
  'Иван Редьюсов'
]

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

import _ from 'lodash';

_.uniq(comments.map((comment) => comment.authorName));

[
  'Глеб Фильтеровский',
  'Иван Редьюсов',
  'Анна Мэп',
]

Отображение сработало как надо, и мы получили интересующие нас детали. Попробуйте самостоятельно извлечь из комментариев другие элементы. Измените их прямо в callback функции — к примеру, извлеките рейтинг пользователей и переведите его в двоичную систему счисления!

III. Reduce

Функция reduce, наверное, самая сложная из нашей тройки, ведь, помимо элементов коллекции, в ней появляется аккумулятор, с которым нужно научиться правильно работать. Эта функция производит «свертку», то есть, берет элементы из коллекции и из их множества создает какую-то одну новую сущность. Например, из массива — объект или число.

Технически функция reduce может заменить и filter, и map, но это, скорее всего, введет в заблуждение ваших коллег-программистов, поэтому старайтесь применять каждую функцию по прямому назначению. Для фильтрации — фильтрацию, для свертки — свертку.

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

// Вот так выглядит наша функция

comments.reduce((acc, comment) => {
  if (_.has(acc, comment.authorName)) {
    acc[comment.authorName].push(comment.text);
    return acc;
  }
  return { ...acc, [comment.authorName]: [comment.text] };
}, {});

Давайте пошагово разберем, что здесь происходит.

comments.reduce((acc, comment) => {
  /*тут будет логика нашей callback функции*/
}, {});

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

comments.reduce((acc, comment) => {}, {}); 
// после запятой мы указываем тип, 
// к которому должна свестись наша коллекция.

Во-вторых, у нас появляется инициализатор типа, к которому мы сводим коллекцию. В нашем случае это объект, поэтому ставим {}. На этом месте мог бы быть массив [], строка '' или число.

Теперь давайте разберемся с внутренней логикой callback функции.

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

if (_.has(acc, comment.authorName)) {
    acc[comment.authorName].push(comment.text);
    return acc;
  }

Что происходит здесь? Мы проверяем, есть ли в аккумуляторе искомый ключ. Для этого используем функцию _.has() из библиотеки Lodash. Если ключ есть, то добавляем текст комментария в значение ключа (оно представлено массивом).

И возвращаем аккумулятор — это важно!

return { ...acc, [comment.authorName]: [comment.text] };

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

И вот он, долгожданный результат:

{
  'Глеб Фильтеровский': [ 
    'Присоединяюсь, из трех функций понял только filter!'
  ],
  'Иван Редьюсов': [
    'Используйте console.log для отладки и сами все поймете!',
    'Если вы приложите ревью, нам будет проще вам помочь.'
  ],
  'Анна Мэп': [
    'Посмотрите гайды на YouTube, там все объясняется.',
    'Кстати, использовать console.log — отличная идея!'
  ]
}

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

IV. Бонус — функция forEach и особый синтаксис

Среди функций высшего порядка есть еще один любопытный экземпляр, функция forEach. Она используется для перебора элементов массива прямо как цикл for...of. Но, в отличие от цикла, более гибко встраивается в синтаксис функций высшего порядка.

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

comments
  .filter(/* выбрали комментарии */)
  .map(/* тут же извлекли из каждого имя пользователя */)
  .forEach(/* создали html-элемент, вставили в него имя пользователя и прикрепили к списку на веб-сайте — почему бы и нет? */);

Не стану подробно расписывать этот пример, думаю, теперь вы можете провернуть такую операцию самостоятельно!

Заключение

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

Желаю удачи в освоении JavaScript!

Аватар пользователя Андрей Агафонов
Андрей Агафонов 28 сентября 2020
33
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 2 мая
профессия
от 6 300 ₽ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 2 мая
профессия
от 6 183 ₽ в месяц
Ручное тестирование веб-приложений
4 месяца
с нуля
Старт 2 мая
профессия
от 6 300 ₽ в месяц
Разработка приложений на языке Java
10 месяцев
с нуля
Старт 2 мая
профессия
от 5 025 ₽ в месяц
новый
Сбор, анализ и интерпретация данных
9 месяцев
с нуля
Старт 2 мая
профессия
от 6 300 ₽ в месяц
Разработка веб-приложений на Laravel
10 месяцев
с нуля
Старт 2 мая
профессия
от 5 840 ₽ в месяц
Создание веб-приложений со скоростью света
5 месяцев
c опытом
Старт 2 мая
профессия
от 9 900 ₽ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 2 мая
профессия
от 6 300 ₽ в месяц
Разработка бэкенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 2 мая
профессия
новый
Автоматизированное тестирование веб-приложений на JavaScript
8 месяцев
c опытом
в разработке
Старт 2 мая
профессия
Верстка с использованием последних стандартов CSS
5 месяцев
с нуля
Старт в любое время