Пошук уроків, статей та іншого контенту
Навчіться передавати функцію іншій функції та викликати її у потрібний момент як callback. Розберіть практичні приклади та зрозумійте, чому цей підхід широко використовується в JavaScript.
Callback-функція — це функція, яку передають як аргумент іншій функції, щоб викликати її пізніше.
У JavaScript функції є об'єктами першого класу. Це означає, що їх можна:
зберігати у змінних;
передавати як аргументи;
повертати з інших функцій;
зберігати в масивах і властивостях об'єктів.
Найпростіший приклад:
function greet(name) {
console.log(`Привіт, ${name}!`);
}
function processUser(callback) {
const userName = "Олена";
callback(userName);
}
processUser(greet);Результат:
Привіт, Олена!У цьому прикладі:
greet — callback-функція;
processUser — функція, яка приймає callback;
callback(userName) — момент виклику переданої функції.
Щоб передати функцію, потрібно вказати її ім'я без круглих дужок:
function sayHello() {
console.log("Привіт!");
}
function execute(callback) {
callback();
}
execute(sayHello);Якщо додати дужки, функція буде викликана одразу, а результат її виконання буде передано замість самої функції:
function sayHello() {
console.log("Привіт!");
return "Результат виконання";
}
function execute(callback) {
console.log(callback);
}
execute(sayHello());
// Привіт!
// Результат виконанняПравильна передача:
execute(sayHello);Неправильна передача:
execute(sayHello());Це одна з найпоширеніших помилок під час роботи з callback-функціями.
Функція, яка приймає callback, сама визначає, коли його викликати:
function calculate(a, b, operation) {
return operation(a, b);
}
function add(firstNumber, secondNumber) {
return firstNumber + secondNumber;
}
function multiply(firstNumber, secondNumber) {
return firstNumber * secondNumber;
}
console.log(calculate(5, 3, add)); // 8
console.log(calculate(5, 3, multiply)); // 15Функція calculate не знає деталей операції. Вона лише передає числа callback-функції:
return operation(a, b);Завдяки цьому одну функцію можна використовувати з різною поведінкою.
Callback не обов'язково потрібно попередньо зберігати в окрему змінну. Його можна передати безпосередньо:
function calculate(a, b, operation) {
return operation(a, b);
}
const result = calculate(10, 4, function (firstNumber, secondNumber) {
return firstNumber - secondNumber;
});
console.log(result); // 6Аналогічний запис зі стрілковою функцією:
const result = calculate(10, 4, (firstNumber, secondNumber) => {
return firstNumber - secondNumber;
});
console.log(result); // 6Якщо тіло стрілкової функції складається з одного виразу, можна скоротити запис:
const result = calculate(10, 4, (firstNumber, secondNumber) => firstNumber - secondNumber);
console.log(result); // 6Функція, яка приймає іншу функцію як аргумент або повертає функцію, називається функцією вищого порядку.
Приклад функції вищого порядку:
function repeatAction(count, action) {
for (let index = 0; index < count; index += 1) {
action(index);
}
}
repeatAction(3, (index) => {
console.log(`Ітерація: ${index}`);
});Результат:
Ітерація: 0
Ітерація: 1
Ітерація: 2repeatAction відповідає за повторення, а переданий callback — за дію під час кожної ітерації.
Такий поділ відповідальності робить код гнучкішим:
repeatAction(3, () => {
console.log("Завантаження...");
});
repeatAction(3, (index) => {
console.log(index * 2);
});Методи масивів активно використовують callback-функції. Найпоширеніші з них:
forEach;
map;
filter;
find;
some;
every;
sort.
forEachforEach викликає callback для кожного елемента масиву:
const users = ["Олена", "Андрій", "Марія"];
users.forEach((user, index) => {
console.log(`${index + 1}. ${user}`);
});Callback отримує такі аргументи:
поточний елемент;
індекс елемента;
весь масив.
const numbers = [10, 20, 30];
numbers.forEach((number, index, array) => {
console.log({
number,
index,
arrayLength: array.length
});
});forEach не створює новий масив і не повертає перетворений результат.
mapmap викликає callback для кожного елемента та створює новий масив із результатів:
const prices = [100, 250, 80];
const pricesWithTax = prices.map((price) => {
return price * 1.2;
});
console.log(pricesWithTax); // [120, 300, 96]
console.log(prices); // [100, 250, 80]Callback у map повинен повернути значення для нового масиву.
Скорочений варіант:
const doubledNumbers = [1, 2, 3].map((number) => number * 2);
console.log(doubledNumbers); // [2, 4, 6]filterfilter залишає елементи, для яких callback повертає true:
const ages = [12, 18, 25, 16, 30];
const adults = ages.filter((age) => age >= 18);
console.log(adults); // [18, 25, 30]Callback у filter повинен повертати логічне значення:
true — елемент потрапить у новий масив;
false — елемент буде пропущено.
findfind повертає перший елемент, для якого callback повернув true:
const products = [
{ id: 1, name: "Клавіатура" },
{ id: 2, name: "Миша" },
{ id: 3, name: "Монітор" }
];
const product = products.find((item) => item.id === 2);
console.log(product);
// { id: 2, name: "Миша" }Якщо відповідного елемента немає, find повертає undefined.
some та everysome перевіряє, чи є хоча б один елемент, що відповідає умові:
const numbers = [2, 4, 7, 8];
const hasOddNumber = numbers.some((number) => number % 2 !== 0);
console.log(hasOddNumber); // trueevery перевіряє, чи всі елементи відповідають умові:
const positiveNumbers = [3, 8, 12];
const areAllPositive = positiveNumbers.every((number) => number > 0);
console.log(areAllPositive); // trueРозглянемо функцію, яка приймає список замовлень і callback для кожного замовлення:
const orders = [
{ id: 101, customer: "Олена", total: 1200 },
{ id: 102, customer: "Андрій", total: 850 },
{ id: 103, customer: "Марія", total: 2400 }
];
function processOrders(orderList, callback) {
orderList.forEach((order) => {
callback(order);
});
}
processOrders(orders, (order) => {
console.log(
`Замовлення #${order.id} для ${order.customer}: ${order.total} грн`
);
});Ту саму функцію можна використати для іншої дії:
processOrders(orders, (order) => {
if (order.total >= 2000) {
console.log(`Замовлення #${order.id} має безкоштовну доставку`);
}
});Механізм обробки списку залишився незмінним, а поведінку визначає callback.
Синхронний callback виконується одразу під час роботи поточної функції.
function runImmediately(callback) {
console.log("Початок");
callback();
console.log("Кінець");
}
runImmediately(() => {
console.log("Callback виконується синхронно");
});Результат:
Початок
Callback виконується синхронно
КінецьДо синхронних callback належать callback-функції в більшості методів масивів: map, filter, forEach, find.
Асинхронний callback виконується пізніше — після завершення певної операції або через певний час.
Наприклад, setTimeout приймає callback і час затримки:
console.log("Початок");
setTimeout(() => {
console.log("Callback після затримки");
}, 1000);
console.log("Кінець");Результат:
Початок
Кінець
Callback після затримкиНавіть якщо затримка дорівнює 0, callback не виконується миттєво:
console.log("Перший рядок");
setTimeout(() => {
console.log("Callback");
}, 0);
console.log("Другий рядок");Результат:
Перший рядок
Другий рядок
CallbackЦе відбувається тому, що асинхронний callback буде виконаний після завершення поточного синхронного коду.
Callback часто використовують, щоб повідомити: певна операція завершилася.
function loadData(callback) {
setTimeout(() => {
const data = {
status: "success",
items: ["JavaScript", "HTML", "CSS"]
};
callback(null, data);
}, 500);
}
loadData((error, data) => {
if (error) {
console.error("Сталася помилка:", error);
return;
}
console.log("Дані отримано:", data);
});У цьому прикладі callback отримує два аргументи:
error — інформацію про помилку або null;
data — результат операції.
Такий стиль називають error-first callback. Він історично широко використовувався в асинхронному JavaScript-коді.
Приклад із помилкою:
function divide(firstNumber, secondNumber, callback) {
if (secondNumber === 0) {
callback(new Error("Ділити на нуль не можна"), null);
return;
}
callback(null, firstNumber / secondNumber);
}
divide(10, 2, (error, result) => {
if (error) {
console.error(error.message);
return;
}
console.log(result); // 5
});
divide(10, 0, (error, result) => {
if (error) {
console.error(error.message); // Ділити на нуль не можна
return;
}
console.log(result);
});Callback може використовувати змінні із зовнішньої області видимості. Це можливо завдяки замиканням:
function createGreeting(greeting) {
return function (name) {
console.log(`${greeting}, ${name}!`);
};
}
const sayHello = createGreeting("Привіт");
const sayGoodbye = createGreeting("До побачення");
sayHello("Олено");
sayGoodbye("Андрію");Функція, повернута з createGreeting, запам'ятовує значення greeting.
Callback також може зберігати доступ до таких змінних:
function createCounter(startValue) {
let count = startValue;
return function () {
count += 1;
console.log(count);
};
}
const counter = createCounter(0);
counter(); // 1
counter(); // 2
counter(); // 3Іноді callback має отримати не лише значення, яке передає основна функція.
function processValue(value, callback) {
const result = value * 2;
callback(result, value);
}
processValue(5, (result, originalValue) => {
console.log(`Початкове значення: ${originalValue}`);
console.log(`Результат: ${result}`);
});Можна також створити обгортку через стрілкову функцію:
function greetUser(name, greeting) {
console.log(`${greeting}, ${name}!`);
}
const users = ["Олена", "Андрій", "Марія"];
users.forEach((user) => {
greetUser(user, "Вітаємо");
});У цьому випадку callback приймає елемент масиву, а потім викликає greetUser із додатковим аргументом.
thisПід час передавання методу об'єкта як callback можна втратити значення this.
const user = {
name: "Олена",
sayName() {
console.log(this.name);
}
};
user.sayName(); // Олена
setTimeout(user.sayName, 100);
// У строгому режимі this буде undefinedМетод передали окремо, тому він більше не викликається як user.sayName().
Один зі способів виправити це — використати стрілкову функцію:
setTimeout(() => {
user.sayName();
}, 100);Інший спосіб — прив'язати контекст за допомогою bind:
setTimeout(user.sayName.bind(user), 100);bind створює нову функцію, у якій this назавжди прив'язано до переданого об'єкта.
Callback-функції дають змогу:
передавати поведінку як параметр;
повторно використовувати одну й ту саму логіку;
розділяти загальний алгоритм і конкретну дію;
працювати з асинхронними операціями;
обробляти події користувача;
створювати гнучкі функції вищого порядку.
Наприклад, замість кількох майже однакових функцій можна створити одну універсальну:
function transformNumbers(numbers, transformer) {
return numbers.map(transformer);
}
const numbers = [1, 2, 3, 4];
const squares = transformNumbers(numbers, (number) => number ** 2);
const doubled = transformNumbers(numbers, (number) => number * 2);
console.log(squares); // [1, 4, 9, 16]
console.log(doubled); // [2, 4, 6, 8]Неправильно:
processUser(greet());Тут greet викликається одразу.
Правильно:
processUser(greet);function execute(callback) {
callback; // Функція лише згадується, але не викликається
}Правильно:
function execute(callback) {
callback();
}return у mapНеправильно:
const doubled = [1, 2, 3].map((number) => {
number * 2;
});
console.log(doubled); // [undefined, undefined, undefined]У фігурних дужках потрібно явно повернути результат:
const doubled = [1, 2, 3].map((number) => {
return number * 2;
});Або використати скорочений синтаксис:
const doubled = [1, 2, 3].map((number) => number * 2);forEach, коли потрібен результатforEach не створює новий масив:
const doubled = [1, 2, 3].forEach((number) => number * 2);
console.log(doubled); // undefinedДля перетворення масиву потрібно використовувати map:
const doubled = [1, 2, 3].map((number) => number * 2);
console.log(doubled); // [2, 4, 6]Кілька асинхронних операцій, вкладених одна в одну, можуть зробити код важким для читання:
getUser((user) => {
getOrders(user, (orders) => {
getPayments(orders, (payments) => {
console.log(payments);
});
});
});Такий стиль називають callback hell. Для складних асинхронних сценаріїв у сучасному JavaScript зазвичай використовують Promise та async/await.
Callback-функції при цьому не зникають: вони все одно залишаються важливою частиною JavaScript і використовуються багатьма API.
Створіть функцію filterByCondition, яка:
приймає масив і callback;
перевіряє кожен елемент через callback;
повертає новий масив лише з елементами, для яких callback повернув true.
Приклад очікуваного використання:
const numbers = [3, 8, 11, 20, 25];
const evenNumbers = filterByCondition(numbers, (number) => {
return number % 2 === 0;
});
console.log(evenNumbers); // [8, 20]Один із можливих варіантів реалізації:
function filterByCondition(items, condition) {
const result = [];
items.forEach((item) => {
if (condition(item)) {
result.push(item);
}
});
return result;
}
const numbers = [3, 8, 11, 20, 25];
const evenNumbers = filterByCondition(numbers, (number) => {
return number % 2 === 0;
});
const largeNumbers = filterByCondition(numbers, (number) => {
return number > 10;
});
console.log(evenNumbers); // [8, 20]
console.log(largeNumbers); // [11, 20, 25]Callback-функція — це функція, передана іншій функції як аргумент.
Callback викликається в потрібний момент усередині основної функції.
Функції потрібно передавати без дужок: run(callback), а не run(callback()).
Функції, які приймають або повертають інші функції, називаються функціями вищого порядку.
Методи forEach, map, filter, find, some та every використовують callback-функції.
Callback може бути синхронним або асинхронним.
Під час передавання методів об'єкта потрібно враховувати контекст this.
Callback-функції допомагають відокремити алгоритм від конкретної дії та зробити код повторно використовуваним.