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.
Автор: testov