Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. JavaScript
Code

Как использовать Map, Filter и Reduce в JavaScript

by
Difficulty:IntermediateLength:LongLanguages:

Russian (Pусский) translation by Anton Lisovsky (you can also view the original English article)

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

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

В данной статье мы взглянем на то что я называю "большая тройка" операторы списков: map, filter и reduce. Понимание как работают вышеперечисленные, три функции, важный шаг в сторону написания чистого, функционального кода, что в свою очередь откроет вам двери к мощным техниками функционального и реактивного программирования.

Также это означает, что у вас не будет больше необходимости писать цикл for.

Любопытно? Давайте же начнём.

Map от списка к списку

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

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

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

Под капотом, map передаёт три аргумента каллбеку:

  1. Текущий элемент массива
  2. Индекс в массиве текущего элемента
  3. Весь массив для которого был вызван map

Давайте взглянем на код.

map на практике

Предположим у нас есть приложение, в котором содержится массив всех задач, которые стоит выполнить в течении дня. Каждый task является объектом, у которого есть свойства name и duration:

Возможно мы хотим создать новый массив только с name для каждого task, что в свою очередь позволит нам взглянуть на всё задачи, которые мы выполнили на сегодня. Используя цикл for, мы бы написали что-то, что выглядит следующим образом:

JavaScript также предоставляет нам цикл forEach. Он работает примерно также как и цикл for, но нам не нужно сравнивать индекс элемента с длинной массива, всё это делается за нас:

Используя map, мы можем написать:

Я добавил index и array параметры, чтобы вы не забывали, что мы можем ими воспользоваться, если это необходимо. Так как в примере я их не использую, можно их убрать и код будет продолжать работать.

Есть два важных отличия между этими двумя подходами:

  1. Используя map, вам не придётся управлять состоянием цикла for самостоятельно.
  2. Вы можете взаимодействовать с элементами напрямую, вместо индексирования массива.
  3. Вам не придётся создавать новый массив и добавлять (push) элементы в него. map возвращает конечный результат за один подход, нам остаётся лишь добавить возвращаемое значение новой переменной.
  4. Не забывайте добавить return вашему каллбеку. Если этого не сделать, мы получим новый массив заполненный undefined.

Всё функции, которые мы будем разбирать сегодня обладают теми же характеристиками.

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

Использование цикла forEach решает обе проблемы. Но у map всё равно есть по крайней мере два преимущества:

  1. forEach возвращает undefined, поэтому его нельзя связать с другими методами массива. map возвращает массив, тем самым мы можем связывать его с другими методами массива.
  2. map возвращает массив с конечным результатом, нам не приходится изменять массив внутри цикла.

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

Пришло время упомянуть, если вы используете Node, тестируете примеры в консоли браузера Firefox или используете Babel или Traceur, можно писать короткую стрелочную функцию из ES6:

Со стрелочной функцией нет необходимости использовать return, вся функция помещается на одной строке.

Пожалуй более читабельный результат получить нельзя.

Подводные камни

В каллбеке передаваемом map должен быть явный return или map возвратит массив заполненный undefined. Не так сложно запомнить что мы должны возвращать (return) значение, но в свою очередь можно и забыть об этом.

Если вы забудете об этом, map не покажет ошибок. Вместо этого, будет возвращён пустой массив. Такие тихие ошибки на удивление сложно искать.

К счастью данный подводный камень относится только к map. Но это часто встречаемая ошибка, на которой я должен заострить внимание: Всегда добавляйте return в ваши каллбеки!

Реализация

Изучение реализации важный шаг на пути полного понимания работы функции. Давайте напишем наш собственный, небольшой map для лучшего понимания, что происходит внутри. Если хотите ознакомиться с оригинальной реализацией, взгляните на Mozilla полифил на MDN.

Данный код принимает массив и каллбек функцию в качестве аргументов. Затем создаётся новый массив; каллбек вызывается для каждого элемента массива, который мы передали изначально; результат добавляется в новый массив; новый массив возвращается в качестве результата. Если вы запустите код в консоли, получите тот же результат, как и прежде. Не забудьте инициализировать tasks, перед тем как тестировать код!

Мы используем цикл for внутри функции, детали реализации спрятаны и вместо этого мы работаем с абстракцией.

Тем самым наш код более декларативный - показывает что делать, а не то как это делать. Вы должны оценить насколько код более читабельный, простой в поддержке и отладке ошибок.

Фильтрация шума

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

Также как и map, filter определяется в Array.prototype. Метод доступен для любого массива, в качестве первого аргумента передаётся каллбек. filter вызывает каллбек для каждого элемента массива, в результате возвращается новый массив, содержащий только элементы, для которых каллбек возвратил true.

Также как и map, filter передаёт каллбеку три аргумента:

  1. Текущий элемент
  2. Текущий индекс
  3. Массив, для которого вызывается filter

filter на практике

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

Используя forEach, мы напишем:

С filter:

В коде выше я не использовал аргументы index и array для нашего каллбека, так как в них нет необходимости.

Также как и map, filter позволяет нам:

  • Избежать мутации массива внутри forEach или for циклах
  • добавить результат новой переменной, вместо передачи значений в новый массив, который мы создали ранее

Подводные камни

Каллбек, который вы передаёте map должен содержать return, если вы хотите, чтобы всё работало, так как вы задумали. С filter, вы также должны использовать return и убедиться, что он возвращает булево значение.

Если забыть о return, каллбек возвратит undefined, и результат filter всегда будет false. Вместо ошибки будет возвращён пустой массив!

В том случае если возвращается что-то отличное от true или false, тогда filter попытается решить что вы пытаетесь получить, согласно правилам приведения JavaScript. Скорее всего вы опять же получите ошибку. И также как и забыв return, ошибка будет на так очевидна.

Никогда не забывайте добавлять return в каллбеки. Также для filter значение всегда должно быть true или false. Это поможет вам сохранить рассудок, в случае ошибок.

Реализация

Опять же лучший способ понять как работает код - написать его. Давайте создадим наш собственный filter. Ребята из Mozilla написали полифил, который мы можем изучить.

Reduce массива

map создаёт новый массив, меняя каждый элемент массива индивидуально. filter создаёт новый массив убирая элементы, которые не соответствуют условиям. reduce в свою очередь, берёт все элементы в массиве, складывает их в новое значение.

Также как map и filter, reduce определяется в Array.prototype и также доступен для любого массива, и вы передаёте каллбек в качестве первого аргумента. Помимо этого можно передать необязательный второй аргумент: значение, индекс с которого стоит начать складывать элементы вашего массива.

reduce передаёт каллбеку четыре аргумента:

  1. Текущее значение
  2. Предыдущее значение
  3. Текущий индекс
  4. Массив, для которого вы вызываете reduce

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

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

reduce на практике

Так как reduce функция, которая поначалу многим может показаться слегка непонятной, мы начнём, шаг за шагом разбирать что-то более простое.

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

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

Для начала, мы вызовем reduce для нашего списка чисел (numbers). Мы передадим его каллбеку, который принимает предыдущее значение и текущее значение в качестве аргументов и вернём результат сложения их вместе. Так как мы передали 0 в качестве второго аргумента reduce, 0 будет использоваться, как предыдущее (previous) значение при первой итерации.

Взглянем, как это работает шаг за шагом:

Итерация Предыдущее Текущее Общее
1 0 1 1
2 1 2 3
3 3 3 6
4 6 4 10
5 10 5 15

Если вы не любитель таблиц, запустите этот кусочек кода в консоли:

Напомню: reduce складывает все элементы массива на каждой интерации, комбинируя их таким образом, как вы укажете в каллбеке. При каждой итерации, каллбек имеет доступ к предыдущему значению, которое на данный момент равно общему значению или аккумулированному значению; также как к текущему значению; текущему индексу и всему массиву, если они нужны вам.

Давайте вернёмся к примеру с приложением для задач на день. Мы получили список названий задач благодаря map и отфильтровали список используя filter.

Что если вы хотите узнать общее количество времени, которое было затрачено на работу, сегодня?;

Используя цикл forEach решение будет выглядеть следующим образом:

Решение с reduce:

Просто.

На этом почти всё. Почти, потому что JavaScript предоставляет малоизвестный метод, под названием reduceRight. В примере выше, reduce начинает с первого элемента массива, затем процесс итерации проходит слева направо:

reduceRight делает тоже самое, но в противоположном направлении:

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

Подводные камни

Три огромных подводный камня с reduce:

  1. Забыли return
  2. Забыли начальное значение
  3. Ожидать, что reduce вернёт массив, когда он возвращает одно значение

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

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

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

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

Реализация

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

Обратите внимание на две вещи:

  1. В этот раз я использовал название accumulator вместо previous. Такое часто можно встретить на практике.
  2. Я присвоил accumulator начальное значение, если пользователь предоставляет данные и значение будет равно 0, в том случае если данных от пользователя не будет. Настоящий reduce работает таким же образом.

Всё вместе: Map, Filter, Reduce и связывание

На данный момент, возможно описанные методы кажутся не такими уж впечатляющими.

Довольно справедливо: map, filter и reduce, по одиночки не такие уж интересные.

Настоящая сила этих методов заключается в их связываемости.

Давайте предположим я хочу сделать следующее:

  1. Собрать все выполненные задачи за два дня.
  2. Сконвертировать время потраченное на выполнение задач в часы, вместо минут.
  3. Отфильтровать задачи, на выполнение, которых ушло два часа или больше.
  4. Сложить всё это.
  5. Умножить результат на часовую ставку.
  6. Показать результат в долларовом эквиваленте.

Для начала, давайте определим наши задачи на понедельник и вторник:

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

Или более лаконично:

Если вы дошли до этого места, всё должно стать очевидным. Однако есть две странные особенности, которые стоит объяснить.

Сперва, на строке 10 я написал:

Две вещи следуют объяснить:

  1. Знак плюс перед accumulator и current конвертирует значению в число. Если этого не сделать, возвращаемое значение будет представлять из себя бесполезную строку, "12410075100".
  2. Если не обернуть сумму в квадратные скобки, reduce вернёт обычное значение, а не массив. В результате вы получите TypeError, так как map может использоваться только для массива!

Следующий момент, который покажется странным - последний reduce, а именно:

Вызов map возвращает массив содержащий одно значение. Здесь мы вызываем reduce для получения значения.

Другой способ сделать это - убрать вызов reduce и проиндексировать массив, который получается после работы map:

Это абсолютно правильно. Если вам более комфортно использовать индекс массива, я не буду вас останавливать.

Но всё же лучше этого не делать. Лучше всего использовать эти функции в, так сказать, среде реактивного программирования, где вы можете обойтись без использования индексов массива. Данная привычка позволит вам быстрее выучить техники реактивного программирования.

Наконец, давайте посмотрим на нашего друга, как бы с этим справился цикл forEach:

Неплохо, но всё же громоздко.

Заключение и следующие шаги

В этом туториале вы изучили, как map, filter и reduce работают; как их использовать; также мы поверхностно рассмотрели, как методы реализованы. Все они позволяют избежать мутации состояния, необходимое условие для циклов for и forEach и теперь вы должны прекрасно представлять, как связать данные методы вместе.

Теперь я думаю вы ждёте не дождётесь дальнейшей практики и дополнительного чтения. Вот три отличных рекомендации куда двигаться дальше:

  1. Замечательный набор упражнения от Jafar Husain по функциональному программированию на JavaScript, в завершении которого вы найдёте вводную информацию по Rx.js
  2. Курс инструктора Envato Tuts+ Jason Rhodes по функциональному программированию на JavaScript
  3. Самый адекватный путеводитель по функциональному программированию, объясняющий на более глубоком уровне, почему следует избегать мутаций, а также о функциональном мышлении в своей основе

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

Если вам понравился материал и вы хотите больше, время от времени проверяйте мой профиль; свяжитесь со мной на Twitter (@PelekeS); или посетите мой блог http://peleke.me.

Вопросы, комментарии или непонятные моменты? Оставляйте их ниже и я сделаю всё возможное, чтобы ответить на каждый из них индивидуально.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.