Функции высшего порядка

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);

Практика

  1. Перепиши императивный код с циклами на цепочку map/filter/reduce
  2. Реализуй pipe и используй для обработки строки: trim, lowercase, replace spaces with dashes
  3. Напиши HOF retry(fn, attempts) — повторяет вызов fn до attempts раз при ошибке
  4. Создай комбинаторы предикатов not, and, or — используй с filter
  5. Реализуй debounce и throttle — протестируй с input и scroll
  6. Реализуй memoize для рекурсивной функции Фибоначчи — сравни скорость
  7. Напиши once(fn) — обёртка, позволяющая вызвать fn только один раз

Связанные темы

Ресурсы