Callback
Callback (обратный вызов) — функция, переданная как аргумент другой функции и вызываемая позже, когда произойдёт определённое событие или завершится операция.
Зачем нужно
Callback — фундаментальный паттерн JavaScript. Он позволяет писать асинхронный код, создавать абстракции через функции высшего порядка и реализовывать событийную модель. Без callback невозможны обработчики событий, таймеры и методы массивов.
Где используется
Обработчики событий DOM, таймеры (setTimeout/setInterval), методы массивов (map/filter/reduce), Node.js API (fs, http), middleware, подписки.
Предпосылки
Function Declaration, Function Expression, Arrow Function
Базовый паттерн
function doSomething(callback) {
const result = 42;
callback(result); // вызываем переданную функцию
}
doSomething(function(value) {
console.log('Результат:', value); // "Результат: 42"
});
Функция высшего порядка
Функция, принимающая или возвращающая другую функцию:
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log); // 0, 1, 2
repeat(3, i => console.log(i * 10)); // 0, 10, 20
Callback в методах массивов
map — преобразование
const prices = [100, 200, 300];
const withTax = prices.map(price => price * 1.2);
console.log(withTax); // [120, 240, 360]
// Callback получает: (element, index, array)
const indexed = prices.map((price, i) => `${i + 1}. ${price} руб.`);
console.log(indexed); // ['1. 100 руб.', '2. 200 руб.', '3. 300 руб.']
filter — фильтрация
const users = [
{ name: 'Иван', age: 25, active: true },
{ name: 'Мария', age: 17, active: true },
{ name: 'Петр', age: 30, active: false },
];
const activeAdults = users.filter(u => u.age >= 18 && u.active);
console.log(activeAdults); // [{ name: 'Иван', age: 25, active: true }]
reduce — агрегация
const orders = [
{ product: 'Книга', price: 500 },
{ product: 'Ручка', price: 50 },
{ product: 'Тетрадь', price: 100 },
];
const total = orders.reduce((sum, order) => sum + order.price, 0);
console.log(total); // 650
// Группировка
const words = ['яблоко', 'банан', 'ананас', 'абрикос', 'банан'];
const grouped = words.reduce((acc, word) => {
const letter = word[0];
acc[letter] = acc[letter] || ;
acc[letter].push(word);
return acc;
}, {});
// { я: ['яблоко'], б: ['банан', 'банан'], а: ['ананас', 'абрикос'] }
forEach, find, some, every
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(n => console.log(n)); // вывести каждый
const found = numbers.find(n => n > 3); // 4
const hasEven = numbers.some(n => n % 2 === 0); // true
const allPositive = numbers.every(n => n > 0); // true
Асинхронные callback
setTimeout / setInterval
console.log('Начало');
setTimeout(() => {
console.log('Через 1 секунду');
}, 1000);
console.log('Конец');
// Вывод: Начало → Конец → Через 1 секунду
Обработчики событий
document.querySelector('#btn').addEventListener('click', (event) => {
console.log('Кнопка нажата!', event.target);
});
window.addEventListener('resize', () => {
console.log('Размер окна изменён');
});
Error-first callback (Node.js)
Паттерн, где первый аргумент callback — ошибка:
// Типичный Node.js-стиль
function readFile(path, callback) {
// имитация асинхронного чтения
setTimeout(() => {
if (!path) {
callback(new Error('Путь не указан'), null);
return;
}
callback(null, 'содержимое файла');
}, 100);
}
readFile('/data.txt', (err, data) => {
if (err) {
console.error('Ошибка:', err.message);
return;
}
console.log('Данные:', data);
});
Callback Hell (Ад callback)
// Вложенные callback — трудно читать и поддерживать
getUser(userId, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, (err, orders) => {
if (err) return handleError(err);
getOrderDetails(orders[0].id, (err, details) => {
if (err) return handleError(err);
displayDetails(details);
});
});
});
// Решение 1: Именованные функции
function onUser(err, user) {
if (err) return handleError(err);
getOrders(user.id, onOrders);
}
function onOrders(err, orders) {
if (err) return handleError(err);
getOrderDetails(orders[0].id, onDetails);
}
function onDetails(err, details) {
if (err) return handleError(err);
displayDetails(details);
}
getUser(userId, onUser);
// Решение 2: Promise (современный подход)
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => displayDetails(details))
.catch(handleError);
Написание функций с callback
// Собственная функция, принимающая callback
function fetchData(url, onSuccess, onError) {
fetch(url)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => onSuccess(data))
.catch(err => onError(err));
}
fetchData(
'/api/users',
data => console.log('Пользователи:', data),
err => console.error('Ошибка:', err)
);
Callback с настройками
function animate(element, options, onComplete) {
const { duration = 300, easing = 'linear' } = options;
element.style.transition = `all ${duration}ms ${easing}`;
element.style.opacity = '0';
setTimeout(() => {
if (onComplete) onComplete;
}, duration);
}
animate(myElement, { duration: 500 }, () => {
console.log('Анимация завершена');
});
Гарантия вызова callback
function processAsync(data, callback) {
let called = false;
function safeCallback(err, result) {
if (called) return; // защита от двойного вызова
called = true;
callback(err, result);
}
try {
// ... обработка
safeCallback(null, data);
} catch (err) {
safeCallback(err, null);
}
}
Частые ошибки
1. Вызов вместо передачи
// Неправильно — вызов функции, а не передача
setTimeout(doSomething, 1000); // doSomething вызвана СРАЗУ
// Правильно — передача ссылки
setTimeout(doSomething, 1000);
// Если нужны аргументы:
setTimeout( => doSomething(arg1, arg2), 1000);
2. Потеря this в callback
class Counter {
constructor { this.count = 0; }
increment { this.count++; }
startAuto {
// Неправильно — this потерян
// setInterval(this.increment, 1000);
// Правильно — bind или стрелка
setInterval( => this.increment, 1000);
}
}
3. Забытая обработка ошибок
// Плохо — ошибка молча проглочена
getData(url, (err, data) => {
console.log(data); // может быть null!
});
// Хорошо — всегда проверять err
getData(url, (err, data) => {
if (err) {
console.error('Ошибка:', err);
return;
}
console.log(data);
});
Практика
- Напиши функцию
each(array, callback), вызывающую callback для каждого элемента - Реализуй
myMap(array, callback), возвращающую новый массив - Создай таймер, вызывающий callback каждую секунду
- Напиши функцию с error-first callback паттерном
- Перепиши вложенные callback в цепочку с именованными функциями