Если вы не читали первую часть — лучше начать с неё, потому что здесь мы будем разбирать и дополнять примеры кода оттуда.

Это вторая часть рассказа про то, как устроены коллбэки в JavaScript.

В первой части мы на примере заказа пиццы разобрались с тем, зачем нужны коллбэки. Сейчас посмотрим на то, как работают запросы к серверу, как получать данные через AJAX.

Практически любое приложение на JavaScript взаимодействует с сервером. Нам постоянно приходится получать данные с помощью методологии AJAX. Попробуем запросить информацию о произвольном аккаунте с GitHub и вывести её в консоль. Такая программа может выглядеть так:


const loadData = function(url, cb) {
  const xhr = new XMLHttpRequest();
  xhr.open(`GET`, url);
  xhr.responseType = `json`;
  // После получения данных выполним cb.
  xhr.addEventListener(`load`, cb);
  xhr.send();
}

loadData(`https://api.github.com/users/AntonovIgor`, function (evt) {
  const response = evt.currentTarget.response;
  console.log(response);
});

В этом коде мы делаем запрос к серверу с помощью объекта XMLHttpRequest. Для удобства мы создали функцию loadData, которая принимает два параметра: адрес сервера, который отдаст нам набор информации, и функцию обратного вызова. Вызов этой функции произойдёт сразу после получения данных от сервера. Если вас смутило наименование параметра cb — не переживайте. Это общепринятый вариант обозначения функций обратного вызова (сокращение от callback). При виде cb становится очевидно, что в этот параметр должна передаваться ссылка на функцию.

Мы не будем детально рассматривать содержимое функции loadData, т.к. это не относится напрямую к теме статьи. Если коротко, то эта функция делает запрос к серверу и вызывает нашу функцию после получения ответа.

Обратите внимание, как мы вызываем функцию loadData. В первый параметр мы передаём адрес сервера, а во второй — ссылку на функцию. В этот раз мы не описывали функцию отдельно, а сделали это прямо «в параметре» — создали анонимную функцию. Подобный подход часто применяется, когда функция нужна один раз. В таких ситуациях нет смысла писать отдельный код — достаточно создать анонимную функцию в нужном месте.

Наша функция сработает сразу после получения ответа от сервера (произойдёт событие load) и выведет ответ сервера в консоль.

Коллбэки повсюду

Даже если вам не требуется работать с сервером, довольно высоки шансы столкнуться с функциями обратного вызова.

Не верите? Хорошо, а как насчёт стандартных возможностей вроде перебирающих методов массивов: forEach, every, some, reduce, filter или функции сортировки sort, или методе addEventListener (второй параметр принимает функцию, которая будет вызвана при наступлении события). Список можно продолжать до бесконечности.

Всякий раз, когда вы пишете код, похожий на этот, вы применяете функции обратного вызова:


[1, 2, 3, 4].forEach(function(it) {
  console.log(it)
});

Аналогично с установкой обработчиков событий. Каждый раз, когда вы подписываетесь на событие с помощью addEventListener, через её второй параметр вы определяете функцию обратного вызова, которая сработает при наступлении события. В мире JavaScript коллбэки повсюду.

Как передать параметры в коллбэк-функцию

Это очень важный вопрос, и в начале понимания сути коллбэк-функций он может загнать в тупик.

Проблема в том, что мы привыкли передавать параметры явно: пишем имя функции, открываем круглые скобки и передаем значения. Этот подход применяется постоянно, но с коллбэк-функциями дело обстоит иначе. Если мы укажем круглые скобки, то произойдёт вызов функции, а нам требуется передать ссылку на неё. Как поступить в этом случае?

Для этого нужно вспомнить про замыкания. Функции в JavaScript могут возвращать в качестве результата выполнения другие функции. А уже у них будет доступ к вышестоящей области видимости.

С таким подходом легко решить задачу передачи параметров в коллбэк-функцию.

Пример

Код заказа пиццы, который мы написали в первой части, выглядел так:


const makePizza = function (title, cb) {
 console.log(`Заказ на приготовление пиццы «${title}» получен. Начинаем готовить…`); 
 setTimeout(cb, 3000);
}

const readBook = function () {
 console.log(`Читаю книгу «Колдун и кристалл»…`);
}

const eatPizza = function () {
  console.log(`Ура! Пицца готова, пора подкрепиться.`);
}

makePizza(`Пеперонни`, eatPizza);
readBook();

Давайте модифицируем его и добавим для функции eatPizza параметр drink, через который будем передавать напиток:


const makePizza = function (title, cb) {
 console.log(`Заказ на приготовление пиццы «${title}» получен. Начинаем готовить…`); 
 setTimeout(cb, 3000);
}

const readBook = function () {
 console.log(`Читаю книгу «Колдун и кристалл»…`);
}

const eatPizza = function (drink) {
 return function() {
   console.log(`Ура! Пицца готова, пора подкрепиться и запить ${drink}.`);
 }  
}

makePizza(`Пепперони`, eatPizza(`Coca-Cola`));
readBook();

Первое, что мы сделали — внесли изменения в функцию eatPizza. Теперь она принимает параметр drink и возвращает новую функцию. В теле новой функции происходит вывод информации в консоль. Помимо текста, который у нас был, мы добавили вывод информации о напитке (drink). Теперь самое интересное. Функция eatPizza перестаёт быть функцией обратного вызова, вместо неё эту роль будет исполнять функция, которую возвращает eatPizza.

Мы изменили тело функции, и теперь нам требуется обновить вызов makePizza. Вторым параметром мы указываем не ссылку на eatPizza, а вызов функции, передав информацию о напитке. Получается, на место второго параметра будет передана новая функция, полученная в результате выполнения eatPizza.

Если представленный пример вызывает затруднение — обязательно перечитайте теорию замыканий.

Метод bind

Рассмотренный способ — не единственный. Любую задачу можно решить несколькими способами, и передача параметров коллбэк-функции — не исключение. Мы можем воспользоваться методом bind, который есть у любой функции.

Метод bind всё упрощает. Результатом его вызова будет новая функция, которой мы можем передать произвольный набор параметров и задать контекст выполнения. Давайте исправим код заказа пиццы, используя метод bind.


const makePizza = function (title, cb) {
 console.log(`Заказ на приготовление пиццы «${title}» получен. Начинаем готовить…`); 
 setTimeout(cb, 3000);
}

const readBook = function () {
 console.log(`Читаю книгу «Колдун и кристалл»…`);
}

const eatPizza = function (drink) {
   console.log(`Ура! Пицца готова, пора подкрепиться и запить ${drink}.`);
}

makePizza(`Пепперони`, eatPizza.bind(null, `Coca-Cola`));
readBook();

Мы вернули функцию eatPizza к первоначальному состоянию, избавившись от возврата новой функции. В остальном всё без изменений. При передаче функции обратного вызова мы воспользовались методом bind. Первый параметр (null) позволяет задать произвольный контекст для функции, но так как эта возможность нас не интересует, мы передаем null. Во втором параметре мы передаем значение для параметра drink. Мы можем передать любое количество параметров для функции обратного вызова, перечисляя их через запятую.

Ад коллбэков

Коллбэк-функции просты в применении и открывают много возможностей, но с большой силой приходит большая ответственность. Легко потеряться во вложенности коллбэк-функций и познать все прелести ада функций обратного вызова. Любым подходом нужно пользоваться с осторожностью. Как не попасть в ад обратных вызов, мы поговорим в одной из ближайших статей.