Понимание callback-функций (колбеков)

Original author: Mike Vollmer
  • Translation
Callback-функции чрезвычайно важны в языке Javascript. Они есть практически повсюду. Но, несмотря на имеющийся опыт программирования на C/Java, с ними у меня возникли трудности (как и с самой идеей асинхронного программирования), и я решил в этом разобраться. Странно, но я не смог найти в интернете хороших вводных статей о callback-функциях — в основном попадались отрывки документации по функциям call() и apply() или короткие кусочки кода, демонстрирующие их использование, и вот, набив шишек в поисках истины, я решил написать введение в callback-функции самостоятельно.

Функции — это объекты


Чтобы понимать callback-функции, нужно понимать обычные функции. Это может показаться банальностью, но функции в Javascript'е — немного странные штуки.

Функции в Javascript'е — на самом деле объекты. А именно, объекты класса Function, создаваемые конструктором Function. В объекте Function содержится строка с JS-кодом данной функции. Если вы перешли с языка C или Java, это может показаться странным (как код может быть строкой?!), но, вообще говоря, в Javascript'е такое сплошь и рядом. Различие между кодом и данными иногда размывается.

// можно создать функцию, передав в конструктор Function строку с кодом
var func_multiply = new Function("arg1", "arg2", "return arg1 * arg2;");
func_multiply(5, 10); // => 50


Преимущество концепции «функция-как-объект» заключается в том, что код можно передавать в другую функцию точно так же, как обычную переменную или объект (потому что в буквальном понимании код — всего лишь объект).

Передача функции как callback-функции


Передавать функцию в качестве аргумента просто.

// определяем нашу функцию с аргументом callback
function some_function(arg1, arg2, callback) {
    // переменная, генерирующая случайное число в интервале между arg1 и arg2
    var my_number = Math.ceil(Math.random() * (arg1 - arg2) + arg2);
    // теперь всё готово и  мы вызываем callback, куда передаём наш результат
    callback(my_number);
}
// вызываем функцию
some_function(5, 15, function (num) {
    // эта анонимная функция выполнится после вызова callback-функции
    console.log("callback called! " + num);
});


Может показаться глупостью создавать такой перемудрённый код, когда можно вернуть значение нормальным способом, но существуют ситуации, в которых это непрактично и callback-функции необходимы.

Не загромождайте выход


Традиционно функции в ходе выполнения принимают на вход аргументы и возвращают значение, используя выражение return (в идеальном случае единственное выражение return в конце функции: одна точка входа и одна точка выхода). Это имеет смысл. Функции — это, в сущности, маршруты между вводом и выводом.

Javascript даёт возможность делать всё немного по-другому. Вместо того чтобы дожидаться, пока функция закончит выполняться и вернёт значение, мы можем использовать callback-функции, чтобы получить его асинхронно. Это полезно для случаев, когда требуется много времени для завершения, например, при AJAX-запросах, ведь мы не можем приостановить браузер. Мы можем продолжить заниматься другими делами в ожидании вызова колбека. Фактически, очень часто от нас требуется (или, точнее, нам настоятельно рекомендуется) делать всё асинхронно в Javascript'е.

Вот более детальный пример, в котором используется AJAX для загрузки XML-файла и используется функция call() для вызова callback-функции в контексте запрошенного объекта (это значит, что когда мы укажем ключевое слово this внутри callback-функции, оно будет ссылаться на запрошенный объект):

function some_function2(url, callback) {
    var httpRequest; // создаём наш XMLHttpRequest-объект
    if (window.XMLHttpRequest) {
        httpRequest = new XMLHttpRequest();
    } else if (window.ActiveXObject) {
        // для дурацкого Internet Explorer'а
        httpRequest = new
        ActiveXObject("Microsoft.XMLHTTP");
    }
    httpRequest.onreadystatechange = function () {
        // встраиваем функцию проверки статуса нашего запроса
        // это вызывается при каждом изменении статуса
        if (httpRequest.readyState === 4 && httpRequest.status === 200) {
            callback.call(httpRequest.responseXML); // вызываем колбек
        }
    };
    httpRequest.open('GET', url);
    httpRequest.send();
}
// вызываем функцию
some_function2("text.xml", function () {
    console.log(this);
});
console.log("это выполнится до вышеуказанного колбека");


В этом примере мы создаём объект httpRequest и загружаем файл XML. Типичная парадигма возвращения значения в конце функции тут больше не работает. Наш запрос обрабатывается асинхронно, а это означает, что мы начинаем запрос и говорим ему вызвать нашу функцию, как только он закончится.

Мы используем здесь две анонимных функции. Важно помнить, что нам бы не составило труда использовать и именованные функции, но во имя лаконичности мы сделали их вложенными. Первая анонимная функция выполняется всякий раз при изменении статуса в нашем объекте httpRequest. Мы игнорируем это до тех пор, пока состояние не будет равно 4 (т.е. запрос выполнен) и статус будет равен 200 (т.е. запрос выполнен успешно). В реальном мире вам бы захотелось проверить, не провален ли запрос, но мы предполагаем, что файл существует и может быть загружен браузером. Эта анонимная функция связана с httpRequest.onreadystatechange, так что она выполняется не сразу, а вызывается каждый раз при изменении состояния в нашем запросе.

Когда мы наконец завершаем наш AJAX-запрос, мы не просто запускаем callback-функцию, мы используем для этого функцию call(). Это ещё один способ вызова callback-функции. Метод, использованный нами до этого — простой запуск функции здесь сработал бы хорошо, но я подумал, что стоит продемонстрировать и использование функции call(). Как вариант, можно использовать функцию apply() (обсуждение разницы между ней и call() выходят за рамки этой статьи, скажу лишь, что это затрагивает способ передачи аргументов функции).

В использовании call() замечательно то, что мы сами устанавливаем контекст, в котором выполняется функция. Это означает, что когда мы используем ключевое слово this внутри нашей callback-функции, оно ссылается на то, что мы передаём первым аргументом в call(). В данном примере, когда мы ссылались на this внутри нашей анонимной функции, мы ссылались на responseXML, полученный в результате AJAX-запроса.

Наконец, второе выражение console.log выполнится первым, потому что callback-функция не выполняется до тех пор, пока не закончен запрос, и пока это произойдёт, последующие части кода продолжают спокойно выполняться.

Обёртывай это


Надеюсь, теперь вы стали понимать callback-функции достаточно хорошо, чтобы начать их использовать в своём собственном коде. Мне всё ещё трудно структурировать код, который зиждется на callback-функциях (в конце концов он становится похож на спагетти… мой разум слишком привык к обычному структурному программированию), но они — очень мощный инструмент и одна из интереснейших частей языка Javascript.
Share post

Comments 15

    +2
    Я думаю, вам стоит углубиться в детали, поскольку статья практически не содержит особой премудрости.
      0
      Не рекомендовал бы такой способ создания функции (bad practice)
      var func_multiply = new Function("arg1", "arg2", "return arg1 * arg2;");

      Если хотите создать анонимную функцию, то так:
      var my_func = function(){};

      Если именованую, то:
      function my_func(){}

      А в целом статья напоминает мысли вслух человека, который только начал изучать JS.
      Почитай вдумчиво для начала javascript.ru/Function и javascript.ru/AJAX
      Удачи.
        0
        А точно, это же перевод. Тогда вообще не понимаю на кой переводить статьи такого уровня.
          +1
          Ну да, статья для новичков. На Хабре такое не приветствуется?
            +1
            Приветствуется, когда в примерах кода показаны «best practice», а не new Function, итд.
              +1
              Пример с new Function тут приведён для наглядности, для иллюстрации тезиса «функции суть объекты».
          0
          Странная у вас классификация функций.
          var foo = function bar() {};
          Это анонимная или именованная?
            0
            Анонимная, переменная bar ссылающаяся на функцию доступна только внутри тела самой функции.
              0
              Но имя же у функции есть, в свойстве name зафиксировано.

              А если так, именованная или анонимная?
              !function myFunc() {}

              Плохая это классификация, лучше по спецификации: function expression и function declaration. Поведение функций всё-таки зависит от способа объявления, а не от наличия имени.
          0
          Вам стоит почитать про функциональное программирование, лямбда-исчисление и тому подобное.

          Само по себе ваше понимание слова callback — уже дилетантское и неправильное, и именно поэтому вы не нашли никаких статей по теме. А искать на самом деле недалеко — lurkmore.to/SICP
            0
            Я просто оставлю это здесь JS async monad.

            Но SICP в частности и Scheme вообще тоже хороши, поскольку
            The key design principles within JavaScript are taken from the Self and Scheme programming languages.

              0
              По ссылке все-таки не совсем тот уровень, с которого надо начинать ;)
              Хотя интересно, конечно.
            +1
            Некоторое время назад был уверен, что ясно себе представляю как правильно передавать функции в параметрах, как обрабатывать параметры и т.п… Я писал довольно много на JS — и всё хорошо работало.
            Но когда начал писать под Ноду — то понял, как сильно я заблуждался; я понял, насколько я был раньше не прав… =(
            До сих пор регулярно натыкаюсь на случаи, когда понимаю, что мои представления о правильности вызовов функций и передачи в них параметров и возврат значения наверх по цепочке вызовов, да и сам принцип построения этих цепочек, немного неверны.
            С одной стороны, безусловно, Нода и обычный браузерный JS — вещи разные. И в последнем многие косяки «прощаются» системой (в силу наличия строгой и ясной последовательности выполнения) и остаются незамеченными. С другой — всё не так просто как кажется =(
            Меня сильно привлекла эта статья — но я разочарован — она ни о чём =(( Простите
              0
              А какие имено важные детали в этом вопросе остались за кадром?
              0
              Я больше склоняюсь что калбеки в некоторм роде зло, пораждающее макаронные изделия, и по возможности лучше использовать событийный (events) механизм (благо такой есть наверное в любой библиотеке или фреймворке).
              Например нужно передавать callback нескольким функциям внутрь:

              function doSubAction (callback) {
                  // your code there
                  callback();
              }
              
              function doAction (callback) {
                  // your code there
                  doSubAction(callback);
              }
              
              function do () {
                  // your code there
                  doAction(function () {
                      console.log("I'm work");
                  }
              }
              

              И это не учитывая того, что callback порой просто не имеет осмысленного имени где нужно, а еще в данную функцию иногда передаются переменные, с которыми тоже могут возникать вопросы.

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

              Only users with full accounts can post comments. Log in, please.