Коллбэк в JavaScript… Что за зверь?

Original author: Brandon Morelli
  • Translation
Если вы не очень хорошо представляете себе — что такое «коллбэки», и как ими пользоваться в JavaScript, сейчас у вас есть шанс их понять и научиться с ними работать.

image

Перейдём сразу к делу. Коллбэк — это функция, которая должна быть выполнена после того, как другая функция завершит работу. Отсюда и название, которое, в английском написании, может быть представлено как «call back», хотя обычно это — «callback». Среди вариантов перевода этого слова — «обратный вызов». В русскоязычных публикациях, допускающих использование жаргона программистов, весьма распространена калька с оригинального названия: «коллбэк». Если же обойтись без жаргона, то о чём мы говорим, называется «функция обратного вызова».

Углубившись, для объяснения сущности функций обратного вызова, в особенности JavaScript, можно сказать, что функции в JS — это объекты. Поэтому функции могут принимать другие функции в качестве аргументов и возвращать их в качестве результатов. Функции, которые работают подобным образом, называют функциями высшего порядка. Коллбэками же обычно называют функции, передаваемые другим функциям в качестве аргументов.

Зачем нужны функции обратного вызова?


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

Взглянем на простой пример:

function first(){
  console.log(1);
}
function second(){
  console.log(2);
}
first();
second();

Как можно ожидать, функция first() выполняется первой, а функция second() второй. Запуск этого кода приводит к тому, что в консоль будет выведено следующее:

// 1
// 2

Пока, надеемся, всё понятно, но что, если функция first() содержит код, который нельзя выполнить немедленно? Например, там есть обращение к некоему API, причём, сначала нужно отправить запрос, а потом дождаться ответа? Для того, чтобы это сымитировать, воспользуемся функцией setTimeout(), которая применяется в JavaScript для вызова других функций с заданной задержкой. Мы собираемся отложить вызов функции на 500 миллисекунд.

Вот что получилось теперь:

function first(){
  // Имитируем задержку
  setTimeout( function(){
    console.log(1);
  }, 500 );
}
function second(){
  console.log(2);
}
first();
second();

Для наших целей особенности работы setTimeout() сейчас неважны. Главное — обратите внимание на то, что вызов console.log(1) будет выполнен с задержкой.

Вот что произойдёт при запуске этого кода:

// 2
// 1

Несмотря на то, что функция first() была вызвана первой, сначала в лог попало то, что выводит функция second().

Это не значит, что JavaScript вызывает функции не в том порядке, в котором мы расположили их вызовы в коде. Смысл в том, что система переходит к исполнению функции second(), не дожидаясь ответа от функции first().

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

Создаём функцию обратного вызова


Создадим собственную функцию обратного вызова.

Для начала — откройте консоль разработчика Chrome (Ctrl + Shift + J в Windows, или Cmd + Option + J в Mac) и введите следующее:

function doHomework(subject) {
  alert(`Starting my ${subject} homework.`);
}

Тут мы объявили функцию doHomework(). Эта функция принимает одну переменную — название предмета, по которому некто делает домашнюю работу. Вызовите функцию, введя в консоли следующее:

doHomework('math');
// Выводит сообщение: Starting my math homework.

Теперь добавим, в качестве второго аргумента функции doHomework(), параметр callback, который будем использовать для того, чтобы передать doHomework() функцию обратного вызова. Теперь код будет выглядеть так:

function doHomework(subject, callback) {
  alert(`Starting my ${subject} homework.`);
  callback();
}

Вызовем обновлённую функцию следующими образом:

doHomework('math', function() {
  alert('Finished my homework');
});

Сначала будет выведено сообщение с текстом Starting my math homework., потом — с текстом Finished my homework.

Функции обратного вызова совсем необязательно создавать непосредственно при вызове функций, которым они передаются. Такую функцию можно объявить и где-нибудь в коде:

function doHomework(subject, callback) {
  alert(`Starting my ${subject} homework.`);
  callback();
}
function alertFinished(){
  alert('Finished my homework');
}
doHomework('math', alertFinished);

После вызова функции doHomework() всё будет выглядеть точно так же, как в предыдущем примере. Различия заключаются лишь в том, как мы работаем с функцией обратного вызова.

Как вы можете видеть, тут, в качестве аргумента при вызове функции doHomework(), использовано имя функции alertFinished().

Функции обратного вызова в реальных проектах


Для программного взаимодействия с популярной социальной сетью Twitter используется специальное API. Выполняя обращения к этому API, мы вынуждены ждать ответа, и только после его получения можем выполнять с тем, что придёт от Twitter, какие-то действия. Вот материал, где рассмотрена работа с Twitter API в среде Node.js с использованием NPM-пакета twitter.

Рассмотрим фрагмент кода из этого материала. Полагаем, он является отличной демонстрацией практического применения функций обратного вызова.

T.get('search/tweets', params, function(err, data, response) {
  if(!err){
    // Именно здесь можно работать с тем, что вернёт нам Twitter
  } else {
    console.log(err);
  }
})
T.get()

— это функция, которая выполняет get-запрос к Twitter API. У функции три аргумента. Первый — 'search/tweets', представляет собой маршрут запроса. Здесь мы собираемся выполнить поиск по твитам. Второй аргумент — params — это параметры поиска. Третий аргумент — анонимная функция, которая и является функцией обратного вызова.

Функция обратного вызова здесь весьма важна, так как, прежде чем продолжать работу, нужно дождаться ответа от сервера. Неизвестно, будет ли обращение к API успешным, поэтому, после отправки параметров поиска по маршруту search/tweet с помощью get-запроса, приходится ждать. Как только Twitter ответит на запрос, будет выполнена функция обратного вызова. Если что-то пошло не так, в ней мы получим объект ошибок (err). Если запрос обработан нормально, в аргументе err будет значение, эквивалентное false, а значит, во-первых, будет исполнена ветвь if условного оператора, а во-вторых — можно будет рассчитывать на то, что в объекте response окажутся некие полезные данные, с которыми уже можно что-то делать.

Итоги


Надеемся, наш рассказ о функциях обратного вызова в JavaScript оказался полезен тем, кто не очень хорошо в них разбирался. На самом деле, то, о чём мы здесь говорили — это лишь вершина айсберга. Однако теперь, поняв основы, вы можете расширять и углублять свои знания в этой области.

Уважаемые читатели! Если вы из тех, кто, до чтения этого материала, плохо представлял себе, что такое функции обратного вызова в JS, скажите — стало понятнее? А если коллбэки для вас — обычное дело, просим поделиться опытом с новичками.
RUVDS.com
1,138.45
RUVDS – хостинг VDS/VPS серверов
Share post

Comments 35

    +16
    Коллбэки в JavaScript для дошкольников? Подобные статьи появляются с определённой периодичностью. Но кто-то хоть не просто выдаёт материал 15-летней давности, а про промисы и async пишут. Хотя и по ним уже уйма статей есть.
      0

      как преобразовать callback:


      function doHomework(subject, callback) {
        alert(`Starting my ${subject} homework.`);
        callback();
        alert('Stop');
      }
      function alertFinished(){
        alert('Finished my homework');
      }
      doHomework('math', alertFinished);

      к промисам:


      function doHomework(subject) {
        return new Promise(function(resolve) {
          alert(`Starting my ${subject} homework.`);
          resolve();
          alert('Stop');
        });
      }
      doHomework('math').then(alert('Finished my homework'));

      ожидаю выполнение alert('Stop') после alert('Finished my homework')


      почему работает наоборот?

        +1

        Потому что продолжения всегда исполняются асинхронно. Формальное требование для A+/Promises звучит примерно так: "при исполнении продолжения в стеке вызовов не должно быть никаких фреймов кроме инфраструктурных".

          –1

          вот как можно обойти:


          function doHomework(subject) {
              return new Promise(async function (resolve) {
                  alert(`Starting my ${subject} homework.`);
                  await resolve();
                  alert('Stop');
              });
          }
          doHomework('math').then(alert('Finished my homework'));
            +5

            В том, что вы написали — нет смысла. resolve(); ничего не возвращает, а применять к нему await бессмысленно. Аналогично бессмысленно делать функцию, переданную в конструктор Promise, асинхронной — никто не будет проверять что она вернула.


            Возможно, вам показалось что все работает как вы задумывали из-за нескольких случайностей. Одна из которых — неправильно созданное продолжение (вы вызываете alert сразу же вместо того чтобы передавать его как замыкание).

              0

              Да, какую-то фигню сморозил. Идея была в том, чтобы сделать код синхронным.

          +1

          Если вам нужно исполнение alert('Stop'); строго после любых прямых продолжений — можно использовать setTimeout:


          function doHomework(subject) {
            return new Promise(resolve => {
              alert(`Starting my ${subject} homework.`);
              resolve();
            })
            .then(() => new Promise(resolve => setTimeout(resolve, 0)))
            .then(() => alert('Stop'));
          }

          Но на самом деле так лучше не делать, ибо костыль. Если вам требуется дождаться окончания исполнения обратного вызова — значит, обещания для этого не подходят.

            0
            Потому что строкой
            doHomework('math').then(alert('Finished my homework'));
            

            вы заставляете alert выполниться прямо на месте, а в then уходит результат выполнения.
            Необходимо обернуть alert в функцию одним из способов:
            // ES5:
            doHomework('math').then(function() {alert('Finished my homework')});
            или 
            doHomework('math').then(alert.bind(null, 'Finished my homework'));
            
            // ES6
            doHomework('math').then(() => alert('Finished my homework'));
            
              0

              И этого все равно будет недостаточно. Потому что продолжение вызывается асинхронно.

                0

                Всё равно оно неправильно работает (ожидаю выполнение alert('Stop') после alert('Finished my homework'))

              +5

              Как-то припозднились. На дворе уже вовсю промисы и уже разворачивается async/await, а вы про коллбеки.

                –2
                Вот только сами промисы создаются с передачей в конструктор фукнции в качестве параметра. То есть — callback

                element.addEventListener('click', ()=> {...}) 
                

                на промисы или async/await как будете переводить?
                  –2
                    +1
                    при чем тут fetch?
                      0
                      Пардон, ошибся. Тут не при чём, верно.
                +9
                Ждём статью про циклы и анонимные функции. Блин, серъёзно, что делает этот пост на хабре в 2017?
                  0

                  А что, в 2017 году девелоперы рождаются со встроенным пониманием JS callback'ов? Автор дал вполне прозрачный заголовок — кто в теме, может "пролистать" публикацию. Кто заинтересовался — welcome, как говорится. Научиться фильтровать нужную информацию от ненужной по заголовку не сложнее, чем понять callback'и.


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

                  +3
                  весьма распространена калька с оригинального названия: «коллбэк». Если же обойтись без жаргона, то о чём мы говорим, называется «функция обратного вызова».

                  Справедливости ради калька — это как раз функция «обратного вызова», а «коллбэк» — лексическое заимствование.

                    +2
                    >>Коллбэк — это функция, которая должна быть выполнена после того, как другая функция завершит работу.

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

                      –2
                      Или вы не правы или я чего-то не знаю.
                      Callback функция, после выполнения асинхронной операции, будет помещена в ту же event loop очередь функций, где была и функция, зарегистрировавшая callback.
                      Поскольку event loop обрабатывается в один поток, то callback не будет извлечен и обработан из очереди event loop, пока изначальная не закончит работу (и все вызвавшие её выше по стеку). Даже если асинхронная операция завершилась во время работы изначальной функции, коллбек раньше не пройдет.
                      Я даже примитивов синхронизации не знаю: js-функция выполняется всегда одна (даже для веб-воркеров).
                      Простите за косноязычие и поправьте если я не прав.
                        0

                        Как и любая другая функция, Callback функция выполняется когда она была вызвана.

                          –1
                          Да, но нет.
                          Callback-функция окажется в очереди выполнения функций.
                          Описания: Event Loop Explained
                          или с раздела «Очередь» Параллельная модель и цикл событий.
                          Изначальный метод, зарегистрировавший callback, в любом случае дойдет до конца, даже если асинхронная операция (сетевой запрос, таймаут и пр.) уже завершилась. Callback не будет выполняться сразу, а добавится в эту же очередь. А очередь обрабатывается последовательно и синхронно:
                          while(queue.waitForMessage()){
                            queue.processNextMessage();
                          }
                          


                          PS: Минус, конечно, важная вещь… Но хочется убедиться, что я правильно или неправильно понимаю механизм работы js-движков. Пока больше нигде не видел утверждений, что callback вызывается сразу.
                            0

                            Изначальный метод может не регистрировать callback, а просто вызвать его.

                              0

                              Да, спасибо — это тривиально, но мы про асинхронные операции (перечитайте комментарий AlexPu).
                              Я про однопоточный движок выполнения js-методов, ссылки на невозможность срабатывания коллбека посреди папского метода (да и любого другого) привел.
                              Про синхронизацию потоков при моей неправоте спрашивать не буду.

                                0

                                Вообще-то, AlexPu писал про синхронные операции.

                                  0

                                  Действительно :)

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

                          Рузумеетеся те или иные библиотеки или фреймворки могут предоставлять ту или иную функциональность в виде API, подразумевая некое априори заданное поведение… скажем некая функция которая принимает в качестве параметра другую функцию (или например жкзепляр некоего функционального интерфейса, если данный конкретный ЯП не поддерживает идею function as first class citizen), гарантируя, что данная функция будет вызвана в определенный момент времени… скажем по завершении выполнения охватывающей функции… или в начале… или вообще не давать каких либо обещаний на сей счет (что чаще всего и бывает) — и это вообще если говорить о синхронных реалтизациях… В случае асинхронных же реализаций даже Господь не стал бы вам давать гарантий в общем случае (хотя в частных случаях вполне возможно).

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

                            function doHomework(subject, callback) {
                              alert(`Starting my ${subject} homework.`);
                              callback();
                            }
                            


                            можно реализовать так:
                            function doHomework(subject, callback) {
                              callback();
                              alert(`Starting my ${subject} homework.`);  
                            }
                            


                            или так

                            function doHomework(subject, callback) {
                            	
                            	var crayfishes = getAllWorldCrayfishes();
                            	while(crayfish: crayfishes){
                            		if(crayfish.location() === 'mountain' && crayfish.hasWhistled()){
                            			callback();
                            			break;
                            		}
                            	}
                            	
                              alert(`Starting my ${subject} homework.`);
                            }
                            


                            И все три варианта используют callback функции… А то что вы имели в виду какую-то конкретную реализацию, вы просто забыли написать… и не надо ссылаться на асинхронные варианты — порядок вызова асинхронных реализацию той или иной функциональности можно предвидеть только для конкретной реализации этой функциональности… да и то не всегда… скажем почему-бы не реализовать асинхронную функциональность при которой функция обратного вызова будет вызвана через 25 часов, 12 минут и 4 секунды после завершения вызова охватывающей функции и только в високосный год?

                              0
                              Да, выше меня поправили. Я подумал про асинхронный вызов коллбека, по причине что синхронный объяснять слишком очевидно: когда вызовешь — тогда и будет, если вызовешь. Думал дело глубже и хитрее :)
                                –1
                                Дело не в том, что вы подумали, а в некорректном ОПРЕДЕЛЕНИИ, которое вы дали в начале статьи

                                Я-же ничего не писал про «дух статьи» и скажем про то что вы думали или не думали (да и не умею я мыслей читать — такой вот я ущербный). Дело именно в определении — если вы его даете, то стоит трижды (а лучше четырежды) подумать над его корректностью… Особенно если пишете статью рассчитанную на начинающих разработчиков, кои очень хорошо усваивают прежде всего определения, особенно если они даны в форме догмы
                                  0
                                  Это не моя статья. Я просто хотел убедиться, что правильно понимаю работу js-движка. Неправильно поняв Ваш первый комментарий — я засомневался. Сомнения сняты, спасибо.
                                    0
                                    >>Это не моя статья.
                                    А, да — извините — попутал…
                        0

                        Спасибо, благодаря вашей статье узнал что такое промисы, и теперь внедряю их в свой код. Промисы действительно выглядят визуально более понятнее чем использование каллбэков.

                        0

                        Написали были лучше как удалить элемент массива изнутри foreach:)))

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