Функции, возвращающие функции
Функция высшего порядка (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 }); // другой ключ — вычисляется
// Но: объекты с циклическими ссылками вызовут ошибку