Функции, возвращающие функции

Функция высшего порядка (Higher-Order Function) — функция, которая принимает функции как аргументы или возвращает функцию как результат; это ключевой инструмент функционального программирования в JavaScript.

Зачем нужно

Возврат функции позволяет создавать специализированные версии функций, откладывать вычисления, реализовывать каррирование, мемоизацию и декораторы. Это основа для таких паттернов как фабрики функций, middleware (Express, Redux), и оберток поведения.

Где используется

  • Каррирование и частичное применение
  • Декораторы: мемоизация, throttle, debounce, retry
  • Фабрики функций-обработчиков
  • Middleware: Express, Redux Thunk
  • React: HOC (Higher-Order Components)

Фабрика функций

// Создаём специализированные функции из одной обобщённой
function createMultiplier(factor) {
  return (n) => n * factor; // замыкает factor
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
const half   = createMultiplier(0.5);

console.log(double(5)); // 10
console.log(triple(4)); // 12
console.log(half(8));   // 4

// Фабрика валидаторов
function createRangeValidator(min, max) {
  return (value) => {
    if (value < min) return `Минимум: ${min}`;
    if (value > max) return `Максимум: ${max}`;
    return null; // валидно
  };
}

const validateAge   = createRangeValidator(0, 150);
const validateScore = createRangeValidator(0, 100);

console.log(validateAge(25));    // null — OK
console.log(validateAge(-1));    // 'Минимум: 0'
console.log(validateScore(105)); // 'Максимум: 100'

Декораторы функций

// 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; // 'Инициализация!'
initApp; // тишина — повторный вызов игнорируется
initApp; // тишина

// memoize — кеширование результатов
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 fib = memoize(function(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
});

console.log(fib(40)); // мгновенно с кешем

Частичное применение (partial application)

function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

function add(a, b, c) { return a + b + c; }

const add10 = partial(add, 10);
console.log(add10(5, 3));  // 18

// Конфигурируемые запросы
function createApiRequest(baseUrl, defaultHeaders) {
  return function(endpoint, options = {}) {
    return fetch(`${baseUrl}${endpoint}`, {
      headers: { ...defaultHeaders, ...options.headers },
      ...options
    });
  };
}

const api = createApiRequest('https://api.example.com', {
  'Authorization': 'Bearer token123',
  'Content-Type': 'application/json'
});

api('/users');           // GET https://api.example.com/users
api('/posts', { method: 'POST', body: '{}' });

Композиция функций

// compose: f(g(x)) — применяет справа налево
const compose = (...fns) => (x) =>
  fns.reduceRight((acc, fn) => fn(acc), x);

// pipe: f(g(x)) — применяет слева направо
const pipe = (...fns) => (x) =>
  fns.reduce((acc, fn) => fn(acc), x);

const trim     = (s) => s.trim();
const toLower  = (s) => s.toLowerCase();
const addGreet = (s) => `Привет, ${s}!`;

const greet = pipe(trim, toLower, addGreet);
console.log(greet('  ИВАН  ')); // 'Привет, иван!'

throttle и debounce

// throttle: не чаще раза в delay мс
function throttle(fn, delay) {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      return fn.apply(this, args);
    }
  };
}

// debounce: вызов только после тишины delay мс
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout( => fn.apply(this, args), delay);
  };
}

const onScroll = throttle( => updateUI, 100);
window.addEventListener('scroll', onScroll);

const onSearch = debounce((query) => fetchResults(query), 300);
input.addEventListener('input', (e) => onSearch(e.target.value));

Частые ошибки

1. Потеря this при возврате функции

function wrapMethod(obj, methodName) {
  const original = obj[methodName];
  return function(...args) {
    return original(...args); // this потерян!
    // return original.apply(obj, args); // правильно
  };
}

2. Неправильная мемоизация с изменяемыми аргументами

// JSON.stringify объектов с методами даёт '{}'
const cache = memoize(fn);
cache({ x: 1 }); // ключ: '{"x":1}'
cache({ x: 1 }); // тот же ключ — из кеша
cache({ x: 2 }); // другой ключ — вычисляется
// Но: объекты с циклическими ссылками вызовут ошибку

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

Ресурсы