Функции — это значения
В JavaScript функция — такое же значение, как число или строка. Её можно положить в переменную, запихнуть в массив, передать в другую функцию или вернуть из неё. Одного этого факта достаточно, чтобы открыть для себя целый стиль программирования:
Когда привыкаешь воспринимать функции как обычные значения, функции высшего порядка перестают казаться какой-то магией. Функция высшего порядка — это просто функция, которая либо принимает другую функцию в качестве аргумента, либо возвращает функцию, либо и то и другое сразу. Вот и всё определение.
Функция как аргумент в JavaScript
Самый распространённый случай — функция, которая принимает колбэк и сама его вызывает. Вы такими уже наверняка пользовались, даже не задумываясь об этом.
forEach — это функция высшего порядка: ты передаёшь ей свою функцию, а она вызывает её для каждого элемента. setTimeout — тоже высшего порядка: принимает функцию и вызывает её после задержки. Ты описываешь, что нужно сделать, а они разбираются, когда и сколько раз.
Свою функцию высшего порядка пишут по тому же принципу. Вот крошечный пример — колбэк вызывается только если условие истинно:
action — это параметр, в котором просто лежит функция. Вызов action() запускает то, что в него передали.
Три метода, которыми вы реально будете пользоваться: map, filter, reduce
У массивов в JavaScript есть методы высшего порядка, которые заменяют большинство циклов for, которые вы бы иначе писали руками. Освойте эту тройку — и куча повседневного кода станет короче и понятнее.
map — преобразуем каждый элемент
map вызывает переданную функцию для каждого элемента и собирает возвращённые значения в новый массив. Длина остаётся той же, а содержимое — преобразованное. Исходный массив при этом не меняется.
filter — оставляем только подходящие элементы
filter оставляет только те элементы, для которых колбэк вернул истинное значение, а остальные отбрасывает. На выходе получаем новый массив — возможно, короче исходного.
reduce — сворачиваем массив в одно значение
reduce — самый универсальный из трёх. В колбэк приходят аккумулятор и текущий элемент, а то, что ты вернёшь, станет аккумулятором на следующем шаге. Второй аргумент (здесь 0) — это стартовое значение.
Эти методы отлично сочетаются в цепочке. И вот тут такой стиль начинает по-настоящему окупаться:
Читается сверху вниз: оставляем оплаченные заказы, вытаскиваем цену, суммируем. Никаких циклов, никаких изменяемых счётчиков, никаких ошибок на единицу.
Функция, возвращающая функцию
Вторая половина истории про функции высшего порядка в javascript. Функция, которая собирает и возвращает другую функцию:
multiplyBy(2) выполняется один раз и возвращает совершенно новую функцию. Эта новая функция по-прежнему «помнит» factor — это и есть замыкание, и ему будет посвящена отдельная страница. А пока запомните главное: вызывая multiplyBy с разными аргументами, вы получаете разные специализированные функции, построенные по одному шаблону.
Такой приём встречается повсюду:
Одно определение — и сразу две переиспользуемые функции. Гораздо удобнее, чем писать warn и info руками и потом следить, чтобы они не разъехались.
Именованные функции против встроенных колбэков
Колбэк-функцию в JavaScript можно передать как стрелочную прямо на месте, а можно — по имени. Оба варианта рабочие, выбирайте тот, который читается понятнее:
Когда вы передаёте isEven (без скобок), вы передаёте саму функцию. А вот если добавить (), функция тут же вызовется и передастся её результат — классическая ошибка новичков:
nums.filter(isEven); // правильно: передаём функцию
nums.filter(isEven()); // неправильно: вызываем isEven без аргументов и передаём результат
Если колбэк разрастается больше чем на пару строк, вынесите его в отдельную именованную функцию. Код вокруг от этого обычно только выигрывает.
Разбираем на конкретном примере
Функции высшего порядка раскрываются во всей красе, когда вы собираете программу из маленьких кусочков. Допустим, у вас есть список товаров, и нужно получить названия доступных по цене позиций, которые есть в наличии, причём в верхнем регистре:
У каждого хелпера — одна задача. У каждого метода массива — одно преобразование. Цепочка читается как описание того, что вам нужно, а не того, как это зациклить.
Когда не стоит их использовать
Методы высшего порядка — штука классная, но заменять ими обычный цикл подходит не всегда:
- Если нужно выйти из перебора на полпути, обычный
forилиfor...ofсbreakбудет понятнее, чем попытки как-то «вырваться» изforEach. - Если внутри колбэка асинхронный код,
mapиforEachего не дождутся. Используйтеfor...ofсawaitили связкуPromise.allсmap. - Если колбэк мутирует общее состояние — вы уходите от сильных сторон этого стиля. Лучше взять обычный цикл или переписать так, чтобы возвращались новые значения.
Там, где они уместны, map, filter и reduce убирают из повседневного кода почти весь циклический бойлерплейт. А если пихать их везде подряд — читаемость страдает. Выбирайте инструмент, который яснее всего передаёт замысел.
Дальше: объекты
Функции — не единственные значения, на которых строится код. Объекты в JavaScript — главная рабочая лошадка для того, чтобы собирать связанные данные и поведение вместе. И именно из них, кстати, состояли почти все массивы, которые вы только что фильтровали и маппили. Об этом — следующая страница.
Часто задаваемые вопросы
Что такое функция высшего порядка в JavaScript?
Это функция, которая делает хотя бы одно из двух: принимает другую функцию как аргумент или возвращает функцию в качестве результата. Array.prototype.map, setTimeout и addEventListener — все они являются функциями высшего порядка: вы передаёте им колбэк, а они сами его вызывают в нужный момент.
Чем отличаются map, filter и reduce?
map проходит по массиву, преобразует каждый элемент и возвращает новый массив той же длины. filter оставляет только те элементы, на которых колбэк вернул истинное значение, — на выходе массив может быть короче. reduce сворачивает массив в одно значение, последовательно комбинируя элементы. Все три метода — это функции высшего порядка, которым вы передаёте колбэк.
Зачем возвращать функцию из другой функции?
Чтобы делать маленькие настраиваемые хелперы и не дублировать логику. Например, multiplyBy(n) возвращает новую функцию, которая умножает на n: из одного определения вы получаете сразу multiplyBy(2) и multiplyBy(10). Этот паттерн построен на замыканиях и постоянно встречается в обработчиках событий, middleware и утилитных библиотеках.