Асинхронные функции в Javascript и ключевое слово this

в 10:30, , рубрики: callback, javascript, this, Веб-разработка, ооп, метки: , , ,

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

function Loader()
{
    this.load = function()
    {
        $.ajax({
            url: '/test.php',
            success: function(data, textStatus, jqXHR) {
                // здесь уже никак нельзя обратиться к объекту класса
                console.log(this); // this содержит внутренний объект jQuery,
                                   // вызывающий функцию success
            }
        });
    }
}
(new Loader()).load();

Для решения этой проблемы в яваскрипте есть функция apply, которая позволяет вызвать любую функцию в контексте нашего объекта — т.е. this будет таким, каким мы скажем. Осталось только сделать callback-функцию, которая будет генирировать асинхронную функцию:

function cb(object, fnc)
{
    return function() {
        return fnc.apply(object, arguments);
    }
}

object — объект, который будет подставляться в this
fnc — выполняемая функция
arguments — стандартный объект аргументов функции

Теперь результат будет таким:

function Loader()
{
    this.load = function()
    {
        $.ajax({
            url: '/test.php',
            success: cb(this, this.onLoad)
        })
    }

    this.onLoad = function(data, textStatus, jqXHR)
    {
        console.log(this); // this теперь содержит объект класса Loader
    }
}
(new Loader()).load();

В качестве функции не обязательно передавать функцию объекта, можно создать функцию «на лету»:

cb(this, function(data, textStatus, jqXHR) {
    console.log(this);
});

Сохранение исходного объекта this

В предыдущем примере мы потеряли объект, вызывающий функцию success (внутренний объект jQuery), но часто возникает потребность использовать вызывающий объект. Например, в событиях HTML-элементов:

function Button()
{
    this.render = function()
    {
        var submit = $('<button>').html('click me');
        submit.bind('click', function(event) {
            // здесь this содержит HTML-элемент, вызывающий событие
            console.log(this);
        });
        submit.appendTo($('body'));
    }
}
(new Button()).render();

В данном примере вызывается событие click HTML-элемента button. Для того чтобы сохранить объект вызывающий функцию (в данном случае HTML-элемент button), callback-функция примет следующий вид:

function cb(object, fnc)
{
    return function() {
        var args = [this];
        for (var i in arguments) args.push(arguments[i]);
        return fnc.apply(object, args);
    }
}

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

function Button()
{
    this.render = function()
    {
        var submit = $('<button>').html('click me');
        submit.bind('click', cb(this, this.onClick));
        submit.appendTo($('body'));
    }

    this.onClick = function(target, event)
    {
        console.log(this); // this теперь содержит объект класса Button
        console.log(target); // HTML-элемент, вызывающий событие
    }
}
(new Button()).render();

Автор: BoneFletcher

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js