Функции высшего порядка — элегантное решение, которое делает код проще, понятнее и эффективнее. Также при первом знакомстве они — верный источник головной боли, от которой не спасают ни гайды в интернете, ни попытки объяснить происходящее словами «свертка» и «отображение».
Этому весьма способствует сложившаяся традиция демонстрировать логику таких функций абстрактными примерами, в которых суммируются какие-то 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 функции — к примеру, извлеките рейтинг пользователей и переведите его в двоичную систему счисления!
Читайте также: Что такое callback-функция в JavaScript?
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!