Пошук уроків, статей та іншого контенту
Дізнайтеся, що таке функції вищого порядку та як вони можуть приймати інші функції або повертати нові. Розберіть, як цей підхід дозволяє створювати гнучкі та повторно використовувані абстракції.
Функція вищого порядку (higher-order function) — це функція, яка виконує хоча б одну з умов:
приймає іншу функцію як аргумент;
повертає функцію як результат;
робить і те, і інше.
У JavaScript функції є об'єктами першого класу. Це означає, що їх можна:
зберігати у змінних;
передавати як аргументи;
повертати з інших функцій;
зберігати в масивах або властивостях об'єктів.
function greet(name) {
return `Привіт, ${name}!`;
}
const anotherGreet = greet;
console.log(anotherGreet("Олено"));
// Привіт, Олено!У цьому прикладі змінна anotherGreet посилається на ту саму функцію, що й greet. Дужки під час присвоєння не використовуються:
const fn = greet; // передаємо функцію
const result = greet(); // викликаємо функціюФункція може приймати іншу функцію як параметр. Функція, яку передають як аргумент, зазвичай називається callback-функцією.
function processUser(name, callback) {
const normalizedName = name.trim();
return callback(normalizedName);
}
function makeUppercase(value) {
return value.toUpperCase();
}
const result = processUser(" olena ", makeUppercase);
console.log(result);
// OLENAprocessUser не знає, що саме потрібно зробити з іменем. Вона лише готує дані та передає їх у callback. Це робить функцію гнучкою: поведінку можна змінювати без переписування processUser.
Те саме можна записати за допомогою стрілкової функції:
const result = processUser(" olena ", (name) => `Користувач: ${name}`);
console.log(result);
// Користувач: olenaУ стандартній бібліотеці JavaScript багато методів масивів є функціями вищого порядку.
mapМетод map приймає функцію та створює новий масив, застосувавши її до кожного елемента.
const prices = [100, 250, 80];
const pricesWithTax = prices.map((price) => price * 1.2);
console.log(pricesWithTax);
// [120, 300, 96]
console.log(prices);
// [100, 250, 80]map не змінює початковий масив. Callback отримує:
поточний елемент;
індекс;
весь масив.
const names = ["Олена", "Андрій", "Марія"];
const labels = names.map((name, index) => `${index + 1}. ${name}`);
console.log(labels);
// ["1. Олена", "2. Андрій", "3. Марія"]filterfilter повертає новий масив лише з тими елементами, для яких callback повернув true.
const users = [
{ name: "Олена", isActive: true },
{ name: "Андрій", isActive: false },
{ name: "Марія", isActive: true },
];
const activeUsers = users.filter((user) => user.isActive);
console.log(activeUsers);
// [
// { name: "Олена", isActive: true },
// { name: "Марія", isActive: true }
// ]Callback для filter має повертати логічне значення або вираз, який JavaScript може привести до true чи false.
reducereduce послідовно обробляє всі елементи та зводить їх до одного результату: числа, рядка, об'єкта, масиву тощо.
const expenses = [120, 80, 250];
const total = expenses.reduce((sum, expense) => sum + expense, 0);
console.log(total);
// 450Другий аргумент reduce — початкове значення акумулятора. У прикладі це 0.
Можна створити об'єкт із масиву:
const products = [
{ category: "books", name: "JavaScript" },
{ category: "games", name: "Chess" },
{ category: "books", name: "Algorithms" },
];
const groupedProducts = products.reduce((groups, product) => {
if (!groups[product.category]) {
groups[product.category] = [];
}
groups[product.category].push(product.name);
return groups;
}, {});
console.log(groupedProducts);
// {
// books: ["JavaScript", "Algorithms"],
// games: ["Chess"]
// }Розглянемо функцію, яка виконує операцію над усіма елементами масиву.
function transformArray(array, transform) {
const result = [];
for (const item of array) {
result.push(transform(item));
}
return result;
}
const numbers = [1, 2, 3, 4];
const doubled = transformArray(numbers, (number) => number * 2);
const squared = transformArray(numbers, (number) => number ** 2);
console.log(doubled);
// [2, 4, 6, 8]
console.log(squared);
// [1, 4, 9, 16]Алгоритм обходу масиву реалізований один раз. Змінюється лише функція transform.
Такий підхід допомагає розділити:
механізм — як перебирати елементи;
правило — що робити з кожним елементом.
Функція може створювати та повертати іншу функцію.
function createMultiplier(multiplier) {
return function (number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5));
// 10
console.log(triple(5));
// 15createMultiplier(2) повертає функцію, яка пам'ятає значення multiplier, що дорівнює 2.
Зі стрілковими функціями цей приклад коротший:
const createMultiplier = (multiplier) => (number) => number * multiplier;
const double = createMultiplier(2);
console.log(double(7));
// 14Така функція називається фабрикою функцій. Вона створює функції з певною конфігурацією.
Коли внутрішня функція використовує змінні зовнішньої функції, виникає замикання (closure).
function createCounter(initialValue = 0) {
let value = initialValue;
return function () {
value += 1;
return value;
};
}
const counter = createCounter(10);
console.log(counter());
// 11
console.log(counter());
// 12
console.log(counter());
// 13Після завершення createCounter змінна value не зникає, оскільки на неї посилається повернена функція.
Замикання часто використовують, щоб:
приховати внутрішній стан;
створювати налаштовані функції;
зберігати конфігурацію;
реалізовувати просту інкапсуляцію.
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount <= 0) {
throw new Error("Сума поповнення має бути додатною");
}
balance += amount;
},
withdraw(amount) {
if (amount <= 0 || amount > balance) {
throw new Error("Некоректна сума зняття");
}
balance -= amount;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance());
// 1300
console.log(account.balance);
// undefinedЗмінна balance недоступна безпосередньо ззовні. Змінювати та читати її можна лише через методи, які повернула фабрика.
Замість окремої функції для кожного правила можна створити фабрику валідаторів.
function createMinLengthValidator(minLength) {
return function (value) {
return value.length >= minLength;
};
}
const isLongEnough = createMinLengthValidator(8);
console.log(isLongEnough("javascript"));
// true
console.log(isLongEnough("code"));
// falseМожна поєднати кілька правил у функцію validateAll:
function validateAll(value, validators) {
return validators.every((validator) => validator(value));
}
const isLongEnough = createMinLengthValidator(8);
const hasNumber = (value) => /\d/.test(value);
const hasUppercaseLetter = (value) => /[A-ZА-ЯІЇЄ]/.test(value);
const password = "Secure123";
const isValid = validateAll(password, [
isLongEnough,
hasNumber,
hasUppercaseLetter,
]);
console.log(isValid);
// truevalidateAll працює з будь-якими правилами, якщо вони приймають значення та повертають true або false.
Композиція — це об'єднання кількох функцій у нову функцію. Результат однієї функції стає аргументом іншої.
const trim = (value) => value.trim();
const toLowerCase = (value) => value.toLowerCase();
const addExclamationMark = (value) => `${value}!`;
const normalizeMessage = (message) => {
return addExclamationMark(toLowerCase(trim(message)));
};
console.log(normalizeMessage(" HELLO "));
// hello!Якщо функцій стає багато, вкладені виклики можуть погіршувати читабельність. Можна створити універсальну функцію композиції:
function compose(...functions) {
return (initialValue) => {
return functions.reduceRight(
(value, currentFunction) => currentFunction(value),
initialValue,
);
};
}
const trim = (value) => value.trim();
const toLowerCase = (value) => value.toLowerCase();
const addExclamationMark = (value) => `${value}!`;
const normalizeMessage = compose(
addExclamationMark,
toLowerCase,
trim,
);
console.log(normalizeMessage(" HELLO "));
// hello!compose виконує функції справа наліво:
trim;
toLowerCase;
addExclamationMark.
Для виконання зліва направо можна створити pipe:
function pipe(...functions) {
return (initialValue) => {
return functions.reduce(
(value, currentFunction) => currentFunction(value),
initialValue,
);
};
}
const normalizeMessage = pipe(
trim,
toLowerCase,
addExclamationMark,
);
console.log(normalizeMessage(" HELLO "));
// hello!pipe часто читається природніше, оскільки порядок функцій збігається з порядком обробки даних.
Callback-функції також часто використовуються з асинхронним кодом. Наприклад, setTimeout приймає функцію, яку буде викликано пізніше.
function runLater(message, delay, callback) {
setTimeout(() => {
callback(message);
}, delay);
}
runLater("Операцію завершено", 500, (message) => {
console.log(message);
});У сучасному JavaScript для складніших асинхронних сценаріїв часто використовують Promise та async/await, але принцип залишається тим самим: функція передається іншій функції для подальшого виклику.
Функції вищого порядку корисні для:
обробки колекцій через map, filter, reduce;
створення конфігурованих функцій;
валідації даних;
логування та вимірювання часу виконання;
повторного використання спільної логіки;
обробки подій;
middleware у серверних фреймворках;
композиції операцій;
приховування стану через замикання.
Наприклад, можна створити обгортку для вимірювання часу виконання функції:
function measureTime(functionToMeasure) {
return (...args) => {
const start = Date.now();
const result = functionToMeasure(...args);
const end = Date.now();
console.log(`Виконання зайняло ${end - start} мс`);
return result;
};
}
function calculateSum(numbers) {
return numbers.reduce((sum, number) => sum + number, 0);
}
const measuredCalculateSum = measureTime(calculateSum);
console.log(measuredCalculateSum([1, 2, 3, 4, 5]));Оператор ...args дає змогу обгортці приймати довільну кількість аргументів, а ...args під час виклику передає їх далі у вихідну функцію.
thisПід час створення обгорток важливо не втратити контекст this. Стрілкові функції не мають власного this, тому для методів об'єктів потрібно враховувати спосіб виклику.
const user = {
name: "Олена",
greet() {
return `Привіт, ${this.name}!`;
},
};
const greet = user.greet;
console.log(greet());
// Привіт, undefinedПісля присвоєння метод викликається як звичайна функція, тому this більше не посилається на user.
Для фіксації контексту можна використати bind:
const boundGreet = user.greet.bind(user);
console.log(boundGreet());
// Привіт, Олена!bind повертає нову функцію, яка викликає початкову функцію з указаним значенням this.
Неправильно:
processUser("Олена", makeUppercase());Тут makeUppercase() викликається одразу, а його результат передається як callback.
Правильно:
processUser("Олена", makeUppercase);Функцію передають без дужок, щоб інша функція могла викликати її пізніше.
return у функціїНеправильно:
const createMultiplier = (multiplier) => {
(number) => number * multiplier;
};У цьому випадку зовнішня функція нічого не повертає.
Правильно:
const createMultiplier = (multiplier) => {
return (number) => number * multiplier;
};Або в скороченому записі:
const createMultiplier = (multiplier) => (number) => number * multiplier;Функція, яка змінює зовнішні змінні, може бути складнішою для тестування та повторного використання.
let total = 0;
function addToTotal(value) {
total += value;
}Краще повертати нове значення:
function add(value, total) {
return total + value;
}
const newTotal = add(100, 50);
console.log(newTotal);
// 150reduceУ reduce потрібно правильно визначати початкове значення акумулятора. Якщо масив може бути порожнім, явне початкове значення особливо важливе.
const numbers = [];
const total = numbers.reduce((sum, number) => sum + number, 0);
console.log(total);
// 0Функції вищого порядку корисні, але не кожен простий фрагмент потрібно перетворювати на складну систему композицій. Абстракція має:
зменшувати дублювання;
покращувати читабельність;
спрощувати тестування;
робити код гнучкішим.
Якщо через абстракцію код стає важчим для розуміння, вона може бути недоречною.
Функція вищого порядку приймає функцію, повертає функцію або робить обидві дії.
Callback — це функція, передана іншій функції як аргумент.
map, filter і reduce — поширені вбудовані функції вищого порядку.
Фабрики функцій дають змогу створювати налаштовані функції.
Замикання дозволяють внутрішнім функціям зберігати доступ до змінних зовнішньої функції.
Композиція та pipe допомагають об'єднувати невеликі функції в більші операції.
Під час роботи з методами об'єктів потрібно враховувати контекст this.
Хороша абстракція повторно використовує логіку, але не ускладнює код без потреби.