Функции высшего порядка
Higher-Order Function (HOF) — функция, которая принимает другую функцию как аргумент и/или возвращает функцию как результат. Это фундамент функционального программирования в JavaScript.
Зачем нужно
Функции высшего порядка позволяют абстрагировать поведение. Вместо написания отдельных циклов для фильтрации, трансформации, поиска — используем общие HOF (map, filter, reduce) с разными callback-функциями. Код становится декларативным, переиспользуемым и легко тестируемым.
Где используется
Повсеместно в JavaScript: обработка массивов (map, filter, reduce), обработчики событий (addEventListener), промисы (.then), React-компоненты (HOC), middleware (Express, Redux), функциональные утилиты (debounce, throttle, memoize).
Предпосылки
Function Declaration, Чистые функции, Парадигмы
Два типа функций высшего порядка
1. Принимает функцию как аргумент
// repeat — принимает action (функцию)
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log); // 0, 1, 2
repeat(3, (i) => console.log(i * 10)); // 0, 10, 20
// addEventListener — принимает callback
button.addEventListener('click', handleClick);
// setTimeout — принимает callback
setTimeout( => console.log('Готово'), 1000);
2. Возвращает функцию как результат
// Фабрика функций
function multiplier(factor) {
return (number) => number * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Декоратор — оборачивает функцию
function withLogging(fn) {
return function (...args) {
console.log(`Вызов ${fn.name} с аргументами:`, args);
const result = fn(...args);
console.log(`Результат:`, result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// Вызов add с аргументами: [2, 3]
// Результат: 5
3. Делает оба (принимает и возвращает)
// Функция unless — принимает предикат и действие, возвращает функцию
function unless(test, then) {
return (...args) => {
if (!test(...args)) return then(...args);
};
}
const logIfNotZero = unless(
(n) => n === 0,
(n) => console.log(n)
);
[0, 1, 0, 2, 3].forEach(logIfNotZero); // 1, 2, 3
Встроенные HOF для массивов
map — преобразование каждого элемента
// arr.map(callback) → новый массив с результатами callback для каждого элемента
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]
const users = [
{ name: 'Иван', age: 25 },
{ name: 'Мария', age: 30 },
{ name: 'Пётр', age: 20 },
];
const names = users.map(user => user.name);
// ['Иван', 'Мария', 'Пётр']
const formatted = users.map(user => `${user.name} (${user.age})`);
// ['Иван (25)', 'Мария (30)', 'Пётр (20)']
// map передаёт 3 аргумента: element, index, array
const indexed = names.map((name, i) => `${i + 1}. ${name}`);
// ['1. Иван', '2. Мария', '3. Пётр']
filter — отбор элементов по условию
// arr.filter(callback) → новый массив с элементами, для которых callback вернул true
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4, 6, 8, 10]
const adults = users.filter(user => user.age >= 21);
// [{ name: 'Иван', age: 25 }, { name: 'Мария', age: 30 }]
// Цепочка: filter + map
const adultNames = users
.filter(user => user.age >= 21)
.map(user => user.name);
// ['Иван', 'Мария']
// Удаление falsy-значений
const mixed = [0, 'hello', '', null, 42, undefined, false, 'world'];
const truthy = mixed.filter(Boolean);
// ['hello', 42, 'world']
reduce — свёртка массива в одно значение
// arr.reduce(callback, initialValue) → одно значение
// callback(accumulator, currentValue, index, array)
const numbers = [1, 2, 3, 4, 5];
// Сумма
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15
// Максимум
const max = numbers.reduce((acc, n) => n > acc ? n : acc, -Infinity);
// 5
// Подсчёт элементов
const fruits = ['яблоко', 'банан', 'яблоко', 'апельсин', 'банан', 'яблоко'];
const count = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
// { яблоко: 3, банан: 2, апельсин: 1 }
// Группировка
const grouped = users.reduce((acc, user) => {
const key = user.age >= 21 ? 'adults' : 'minors';
acc[key] = acc[key] || ;
acc[key].push(user);
return acc;
}, {});
// Flatten (до появления flat)
const nested = [[1, 2], [3, 4], [5, 6]];
const flat = nested.reduce((acc, arr) => [...acc, ...arr], );
// [1, 2, 3, 4, 5, 6]
// reduce как универсальный HOF (можно реализовать map и filter через reduce)
const mapViaReduce = (arr, fn) =>
arr.reduce((acc, x, i) => [...acc, fn(x, i)], );
const filterViaReduce = (arr, fn) =>
arr.reduce((acc, x, i) => fn(x, i) ? [...acc, x] : acc, );
sort — сортировка (с функцией сравнения)
// arr.sort(compareFn) — МУТИРУЕТ массив! Используй [...arr].sort()
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
// По возрастанию
const asc = [...numbers].sort((a, b) => a - b);
// [1, 1, 2, 3, 4, 5, 6, 9]
// По убыванию
const desc = [...numbers].sort((a, b) => b - a);
// [9, 6, 5, 4, 3, 2, 1, 1]
// Сортировка объектов
const byAge = [...users].sort((a, b) => a.age - b.age);
const byName = [...users].sort((a, b) => a.name.localeCompare(b.name, 'ru'));
// ES2023: toSorted — не мутирует
const sorted = numbers.toSorted((a, b) => a - b);
// numbers не изменился
forEach — выполнение действия для каждого элемента
// arr.forEach(callback) → undefined (ничего не возвращает!)
const items = ['один', 'два', 'три'];
items.forEach((item, index) => {
console.log(`${index}: ${item}`);
});
// Важно:
// - forEach НЕ возвращает массив — используй map для преобразования
// - forEach НЕЛЬЗЯ прервать — используй for...of или find/some
// - forEach НЕ работает с await — используй for...of для async
find и findIndex — поиск элемента
// arr.find(callback) → первый элемент, для которого callback === true, или undefined
const users = [
{ id: 1, name: 'Иван' },
{ id: 2, name: 'Мария' },
{ id: 3, name: 'Пётр' },
];
const maria = users.find(u => u.name === 'Мария');
// { id: 2, name: 'Мария' }
const notFound = users.find(u => u.name === 'Анна');
// undefined
// arr.findIndex(callback) → индекс или -1
const mariaIndex = users.findIndex(u => u.name === 'Мария');
// 1
// ES2023: findLast и findLastIndex — поиск с конца
const lastEven = [1, 2, 3, 4, 5].findLast(n => n % 2 === 0);
// 4
some и every — проверка условия
// arr.some(callback) → true, если хотя бы один элемент удовлетворяет условию
const numbers = [1, 3, 5, 7, 8, 9];
const hasEven = numbers.some(n => n % 2 === 0);
// true (8 — чётное)
// arr.every(callback) → true, если ВСЕ элементы удовлетворяют условию
const allPositive = numbers.every(n => n > 0);
// true
const allEven = numbers.every(n => n % 2 === 0);
// false
// Полезно для валидации
const isFormValid = fields.every(field => field.value.trim() !== '');
const hasErrors = errors.some(err => err.severity === 'critical');
flatMap — map + flatten
// arr.flatMap(callback) → map, затем flat(1)
const sentences = ['Привет мир', 'Как дела', 'JavaScript круто'];
const words = sentences.flatMap(s => s.split(' '));
// ['Привет', 'мир', 'Как', 'дела', 'JavaScript', 'круто']
// Полезно для "one-to-many" трансформаций
const orders = [
{ id: 1, items: ['книга', 'ручка'] },
{ id: 2, items: ['тетрадь'] },
];
const allItems = orders.flatMap(order =>
order.items.map(item => ({ orderId: order.id, item }))
);
// [
// { orderId: 1, item: 'книга' },
// { orderId: 1, item: 'ручка' },
// { orderId: 2, item: 'тетрадь' }
// ]
// Фильтрация + трансформация за один проход
const result = numbers.flatMap(n => n > 3 ? [n * 2] : );
// Эквивалент: numbers.filter(n => n > 3).map(n => n * 2)
Создание собственных HOF
Своя реализация map и filter
function myMap(array, transform) {
const result = ;
for (const item of array) {
result.push(transform(item));
}
return result;
}
myMap([1, 2, 3], n => n * 10); // [10, 20, 30]
function myFilter(array, predicate) {
const result = ;
for (const item of array) {
if (predicate(item)) result.push(item);
}
return result;
}
myFilter([1, 2, 3, 4, 5], n => n % 2 === 0); // [2, 4]
function myReduce(array, callback, initial) {
let acc = initial;
for (let i = 0; i < array.length; i++) {
acc = callback(acc, array[i], i, array);
}
return acc;
}
myReduce([1, 2, 3, 4], (acc, n) => acc + n, 0); // 10
Debounce — задержка выполнения
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout( => fn.apply(this, args), delay);
};
}
// Поиск запускается через 300мс после последнего нажатия клавиши
const debouncedSearch = debounce((query) => {
console.log('Поиск:', query);
}, 300);
input.addEventListener('input', (e) => debouncedSearch(e.target.value));
Throttle — ограничение частоты
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
return fn.apply(this, args);
}
};
}
// Обработчик скролла вызывается не чаще раза в 100мс
const throttledScroll = throttle(() => {
console.log('Скролл:', window.scrollY);
}, 100);
window.addEventListener('scroll', throttledScroll);
Мемоизация
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const factorial = memoize((n) => {
if (n <= 1) return 1;
return n * factorial(n - 1);
});
console.log(factorial(10)); // Вычисляет
console.log(factorial(10)); // Из кэша (мгновенно)
console.log(factorial(8)); // Тоже из кэша (промежуточные значения уже есть)
Once — выполнить только один раз
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const initApp = once(() => {
console.log('Инициализация');
return { ready: true };
});
initApp; // 'Инициализация' → { ready: true }
initApp; // Ничего — возвращает предыдущий результат
Композиция функций
pipe — выполнение слева направо
const pipe = (...fns) => (x) => fns.reduce((v, fn) => fn(v), x);
const processString = pipe(
(s) => s.trim(),
(s) => s.toLowerCase(),
(s) => s.replace(/\s+/g, '-'),
(s) => s.slice(0, 50)
);
console.log(processString(' Hello World ')); // 'hello-world'
// Обработка данных пользователя
const processUser = pipe(
(user) => ({ ...user, name: user.name.trim() }),
(user) => ({ ...user, email: user.email.toLowerCase() }),
(user) => ({ ...user, slug: user.name.toLowerCase().replace(/\s+/g, '-') }),
(user) => ({ ...user, isValid: user.email.includes('@') })
);
const result = processUser({ name: ' Иван Петров ', email: 'IVAN@MAIL.RU' });
// { name: 'Иван Петров', email: 'ivan@mail.ru', slug: 'иван-петров', isValid: true }
compose — выполнение справа налево
const compose = (...fns) => (x) => fns.reduceRight((v, fn) => fn(v), x);
const formatPrice = compose(
(s) => `${s} руб.`, // 3. Добавить 'руб.'
(n) => n.toLocaleString('ru'), // 2. Форматировать число
(n) => Math.round(n * 100) / 100 // 1. Округлить до копеек
);
console.log(formatPrice(1234.567)); // '1 234,57 руб.'
pipe vs compose
// pipe — читается слева направо (порядок выполнения)
// compose — читается справа налево (математическая нотация)
// Эквивалентны:
pipe(trim, lowercase, slugify)(input);
compose(slugify, lowercase, trim)(input);
// В большинстве JS-кодовых баз предпочитают pipe — легче читать
Асинхронный pipe
const pipeAsync = (...fns) => (x) =>
fns.reduce(
(promise, fn) => promise.then(fn),
Promise.resolve(x)
);
const processOrder = pipeAsync(
async (order) => { /* валидация */ return order; },
async (order) => { /* расчёт стоимости */ return order; },
async (order) => { /* сохранение в БД */ return order; },
async (order) => { /* отправка email */ return order; },
);
await processOrder({ items: ['книга', 'ручка'] });
Паттерн: Predicate Combinators
// Предикат — функция, возвращающая boolean
const isEven = (n) => n % 2 === 0;
const isPositive = (n) => n > 0;
// Комбинаторы — HOF, которые создают новые предикаты
const not = (predicate) => (...args) => !predicate(...args);
const and = (...predicates) => (...args) => predicates.every(p => p(...args));
const or = (...predicates) => (...args) => predicates.some(p => p(...args));
const isOdd = not(isEven);
const isPositiveEven = and(isPositive, isEven);
const numbers = [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6];
console.log(numbers.filter(isPositiveEven)); // [2, 4, 6]
console.log(numbers.filter(isOdd)); // [-3, -1, 1, 3, 5]
console.log(numbers.filter(or(isOdd, n => n > 4))); // [-3, -1, 1, 3, 5, 6]
Реальные примеры
Express middleware — HOF
// Фабрика middleware — HOF возвращающая middleware
function roleRequired(role) {
return (req, res, next) => {
if (req.user?.role === role) return next;
res.status(403).json({ error: 'Forbidden' });
};
}
app.get('/admin', authRequired, roleRequired('admin'), adminHandler);
React Higher-Order Component (HOC)
// HOC — функция, принимающая компонент и возвращающая компонент
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, isLoading } = useAuth;
if (isLoading) return <Spinner />;
if (!user) return <Redirect to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);
Event handling — фабрика обработчиков
function createHandler(action) {
return (event) => {
event.preventDefault();
console.log(`Action: ${action}`, event.target);
};
}
saveBtn.addEventListener('click', createHandler('save'));
deleteBtn.addEventListener('click', createHandler('delete'));
Частые ошибки
1. Забытый return в callback с фигурными скобками
// ПЛОХО: забыли return (стрелочная функция с {})
const doubled = [1, 2, 3].map(n => { n * 2 });
// [undefined, undefined, undefined]
// ХОРОШО:
const doubled = [1, 2, 3].map(n => n * 2); // Без {} — implicit return
const doubled = [1, 2, 3].map(n => { return n * 2; }); // С {} — explicit return
2. Передача parseInt в map без осторожности
// ПЛОХО: map передаёт (element, index, array) — parseInt использует index как radix!
['1', '2', '3'].map(parseInt);
// [1, NaN, NaN] — parseInt('2', 1) и parseInt('3', 2) возвращают NaN
// ХОРОШО: обернуть
['1', '2', '3'].map(s => parseInt(s, 10)); // [1, 2, 3]
['1', '2', '3'].map(Number); // [1, 2, 3]
3. Забытое начальное значение в reduce
// ПЛОХО: без начального значения — на пустом массиве TypeError!
.reduce((acc, n) => acc + n); // TypeError: Reduce of empty array
// ХОРОШО: всегда передавай начальное значение
.reduce((acc, n) => acc + n, 0); // 0
4. forEach вместо map для преобразования
// ПЛОХО: forEach для создания нового массива
const result = ;
[1, 2, 3].forEach(n => result.push(n * 2));
// ХОРОШО: map для преобразования
const result = [1, 2, 3].map(n => n * 2);
5. reduce для всего подряд
// ПЛОХО: reduce там, где проще filter + map
const adultNames = users.reduce((acc, user) => {
if (user.age >= 18) acc.push(user.name);
return acc;
}, );
// ХОРОШО: читаемая цепочка
const adultNames = users
.filter(user => user.age >= 18)
.map(user => user.name);
6. sort мутирует массив
// ПЛОХО: sort мутирует оригинал
const nums = [3, 1, 2];
const sorted = nums.sort((a, b) => a - b); // nums тоже изменился!
// ХОРОШО: копируй перед сортировкой
const sorted = [...nums].sort((a, b) => a - b);
// или ES2023:
const sorted = nums.toSorted((a, b) => a - b);
Практика
- Перепиши императивный код с циклами на цепочку map/filter/reduce
- Реализуй
pipeи используй для обработки строки: trim, lowercase, replace spaces with dashes - Напиши HOF
retry(fn, attempts)— повторяет вызов fn до attempts раз при ошибке - Создай комбинаторы предикатов
not,and,or— используй с filter - Реализуй
debounceиthrottle— протестируй с input и scroll - Реализуй
memoizeдля рекурсивной функции Фибоначчи — сравни скорость - Напиши
once(fn)— обёртка, позволяющая вызвать fn только один раз