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);
});

Практика

  1. Напиши функцию each(array, callback), вызывающую callback для каждого элемента
  2. Реализуй myMap(array, callback), возвращающую новый массив
  3. Создай таймер, вызывающий callback каждую секунду
  4. Напиши функцию с error-first callback паттерном
  5. Перепиши вложенные callback в цепочку с именованными функциями

Связанные темы

Ресурсы