Що таке функції вищого порядку в JavaScript (hof)
Функція вищого порядку (HOF) - функція, що приймає іншу функцію як аргумент, повертає функцію як результат, або робить і те, і інше.
Теорія
TL;DR
- Функції в JavaScript - це значення, їх можна передавати як числа чи рядки
- HOF = приймає функцію як вхід, або повертає функцію, або обидва варіанти
- Вбудовані приклади, які ти вже знаєш:
map,filter,reduce,forEach - Використовуй HOF коли одна і та сама операція повторюється з різною поведінкою - передай поведінку, замість того щоб дублювати код
- Правило вибору: якщо ловиш себе на написанні одного й того самого циклу з трохи різною операцією всередині - HOF тут доречний
Швидкий приклад
// map() - це HOF: приймає функцію і застосовує до кожного елементу
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2); // [2, 4, 6]
// multiplier() - це HOF: повертає нову налаштовану функцію
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
console.log(double(5)); // 10map отримує num => num * 2 як аргумент. multiplier будує і повертає нову функцію, налаштовану через factor. Обидві є HOF.
Навіщо потрібні HOF
Без HOF ти дублюєш логіку. З ними пишеш один раз і передаєш те, що змінюється. В цьому і є сенс.
Уяви трансформацію масиву: без map щоразу пишеш for-цикл, оголошуєш новий масив, вручну пушиш результати. З map ти просто кажеш «ось що робити з кожним елементом» - і метод бере на себе все інше. Операція залишається незмінною; поведінка, яку ти передаєш, варіюється.
Я бачив, як розробники копіювали той самий цикл п'ять разів з трохи різними операціями всередині. Один виклик map вирішував би кожен випадок в один рядок.
Це стало можливим тому що в JavaScript функції є об'єктами першого класу (first-class citizens). Їх можна присвоювати змінним, передавати як аргументи, повертати з інших функцій. HOF просто використовують це свідомо.
Коли використовувати
- Трансформація колекцій:
map,filter,reduceдля будь-яких списків з API або UI - Обробка подій:
addEventListenerприймає callback - твоя функція запускається коли подія спрацьовує - Асинхронні ланцюжки:
Promise.then()приймає функцію що виконується при resolve - Middleware в Express: кожен middleware - це HOF що обгортає наступний обробник додатковою логікою
- Спеціалізовані функції:
multiplier(2)даєdouble,multiplier(3)даєtriple, з одного визначення
Для простої одноразової логіки де звичайний цикл читається краще - HOF не потрібен.
Як це працює всередині
V8 обробляє функції як об'єкти. Коли передаєш функцію в HOF, V8 копіює посилання на цей об'єкт, а не сам код. Коли HOF повертає функцію, повернута функція захоплює зовнішню область видимості через замикання (closure) - V8 виділяє closure cell у купі (heap) з посиланнями на лексичні змінні, тримаючи їх живими поки існує внутрішня функція. Вбудовані методи як Array.prototype.map реалізовані на C++ і викликають твій JS-callback на кожній ітерації.
Типові помилки
Забуваєш викликати повернуту функцію:
const doubler = multiplier(2); // Повертає функцію
console.log(multiplier(2)); // Виведе [Function] - не число!
console.log(doubler(5)); // 10 - ось що потрібноmultiplier(2) повертає функцію. Її ще потрібно викликати з реальним значенням.
Мутуєш функцію що передана як аргумент:
// Погано: додає властивість до оригінального об'єкта функції
function badHOF(fn) {
fn.customProp = 'mutated'; // Змінює оригінал у зовнішньому коді
return fn;
}
// Добре: обгортаєш, не торкаючись оригіналу
function goodHOF(fn) {
return (...args) => fn(...args);
}Функції - це об'єкти. Додавши до них властивість, змінюєш оригінал поза межами HOF.
Втрата контексту this в callback:
const obj = { value: 42 };
[1, 2].forEach(function() {
console.log(this.value); // undefined - контекст загублено
});
[1, 2].forEach(function() {
console.log(this.value); // 42 - явне прив'язування повертає контекст
}.bind(obj));Нестабільні HOF у залежностях useEffect в React:
// Погано: нова функція на кожен рендер запускає ефект щоразу
useEffect(() => { fetchData(increment); }, [increment]);
// Добре: useCallback стабілізує посилання
const increment = useCallback(() => setCount(c => c + 1), []);React порівнює залежності за посиланням. Нова функція на кожен рендер виглядає як змінена залежність і повторно запускає ефект.
Де зустрічається в реальних проектах
Array.map/filter/reduce: кожен список у React, ланцюжки в Lodash- Express middleware:
app.use(authMiddleware(handler))- кожен middleware обгортає наступний Promise.then(): будує ланцюжок асинхронних операцій через передачу обробників- React
useCallback: мемоїзує функцію для стабільного посилання між рендерами - Lodash
_.curry/_.partial: будують спеціалізовані функції із загальних
Питання на співбесіді
Q: Яка різниця між HOF і callback?
A: Callback - це будь-яка функція передана як аргумент. HOF - це функція що її приймає. map є HOF; num => num * 2 є callback.
Q: Реалізуй map самостійно.
A:
function myMap(arr, fn) {
const result = [];
for (let i = 0; i < arr.length; i++) {
result.push(fn(arr[i], i, arr));
}
return result;
}Q: Як замикання (closure) пов'язані з HOF?
A: Більшість HOF що повертають функції, покладаються на замикання. Повернута функція зберігає доступ до змінних зовнішньої області видимості - наприклад factor всередині multiplier - навіть після завершення зовнішнього виклику.
Q: Чи є у HOF вплив на продуктивність?
A: У важких циклах - так. Повернуті функції створюють closure-алокації в купі (heap). Для масивів від 1M+ елементів ручний цикл може бути в 2-3 рази швидшим у V8. У більшості прикладного коду ця різниця не має значення.
Q: Чому не мемоїзований HOF у deps масиві useEffect в React 18 спричиняє нескінченний цикл?
A: Кожен рендер створює нове посилання на функцію. React порівнює залежності за посиланням, тому нова функція виглядає як змінена залежність. Ефект повторно запускається, тригерить рендер, який створює ще одне нове посилання. useCallback з правильним масивом залежностей розриває цей цикл.
Приклади
Базовий: логер для будь-якої функції
function withLogging(fn) {
return (...args) => {
console.log('Викликається:', fn.name, 'з аргументами:', args);
const result = fn(...args);
console.log('Результат:', result);
return result;
};
}
function add(a, b) { return a + b; }
const loggedAdd = withLogging(add);
loggedAdd(3, 4);
// Викликається: add з аргументами: [3, 4]
// Результат: 7withLogging повертає нову функцію що обгортає будь-яку передану функцію. Оригінальний add залишається незмінним. Це патерн декоратор: поведінка розширюється без змін у вихідній функції.
Середній рівень: валідація маршруту в Express
const express = require('express');
const app = express();
app.use(express.json());
// HOF: приймає обробник, повертає новий обробник з вбудованою валідацією
function validateUser(fn) {
return (req, res, next) => {
if (!req.body.username) {
return res.status(400).send('Missing username');
}
return fn(req, res, next);
};
}
const userHandler = (req, res) => res.json({ user: req.body.username });
app.post('/user', validateUser(userHandler));
// POST /user { username: 'alice' } => 200 { user: 'alice' }
// POST /user {} => 400 "Missing username"validateUser обгортає будь-який обробник маршруту перевіркою. Сам обробник нічого не знає про валідацію. Заміни validateUser на requireAdmin або checkRateLimit - патерн залишається ідентичним.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.