Pull to refresh

Правильный захват контекста в Javascript

JavaScript *
Довольно часто во многих статьях я вижу, как люди захватывают контекст this для использования в анонимной функции и удивляюсь — то, что уже стало стандартом — просто ужасная практика, которая противоречит всем канонам программирования. Вам знакома такая запись?
var self this;
Может вам тоже стоит переосмыслить этот аспект?

Итак, пример:
var self this;
asyncFunc(function () {
    
self.callMethod();
});


Другой пример (jQuery):
$('input').bind('keydown', function () {
    var 
$this = $(this);
    
$this.css({
        
background $this.val()
    });
});


Что вы про них скажете?

Ненависть


Я считаю, что в названиях переменных self и $this (а также that, _this, t) кроется зло. Зло кроется по двум причинам.
Первая — названия совершенно не несут смысловой нагрузки. Таким же чудом мы могли бы использовать, скажем var killmeplz this;
Вторая — иногда контексты могут множиться и пересекаться и тогда настаёт путаница. Например, когда функции у нас вложены одна в другую:
var self this;
asyncFunc(function () {
    var 
self2 this// wtf ?!!
    
setTimeout(function () {
        
self.callMethod(self2);
    }, 
200);
});


Решение первое — правильно именуем переменные


Всегда надо давать вменяемые имена переменным. Это правило относится не к контексту в JS, а к программированию в целом, но во время захвата контекста все об этом забывают. Например:
$('input').bind('keydown', function () {
    var 
$colorInput = $(this);
    
$colorInput.css({
        
background $colorInput.val()
    });
});


Это решит проблему со вложенными функциями.
$block = $(this);
$(
'button').each(function () {
    var 
$button = $(this);
    $.
each(users, function () {
        var 
user this;
        
$block.append(user.init($button));
    });
});


Но чаще всего такое разнообразие контекстов не требуется. Потому давайте посмотрим на другой способ:

Принудительное задание контекста функции


Эту идею я почерпнул из фреймворка MooTools и считаю её замечательной. Немножко расширим прототип Function
Function.prototype.bind = function (scope) {
    var 
fn this;
    return function () {
        return 
fn.apply(scopearguments);
    };
};


Теперь мы можем не терять контекст в течении всей работы. Если нам нужен только внешний контекст — мы прямо это указываем и код становится значительно прозрачнее:
asyncFunc(function () {
    
this.callMethod();
}.
bind(this));


Другие возможности работы с .bind


Часто бывает, что для того, чтобы работать с методом объекта приходится городить очень некрасивую конструкцию. Например (пример на MooTools):
var Analizer = new Class({
    
initialize : function (name) {
        
this.dataRouter = new DataRouter[name]();
    },
    
start : function () {
        var 
analizer   this;
        
this.dataRouter.get(function (data) {
            
analizer.parse(data);
        });
    },
    
parse : function (data) {
        
// parsing data, using this.privateMethods
    
}
});


мы не можем передать в метод get просто ссылку на метод parse:
dataGetter.get(analizer.parse);


потому что тогда потеряется контекст метода. метод bind поможет нам в этом и мы видим, насколько яснее стал этот код:
var Analizer = new Class({
    
initialize : function (name) {
        
this.dataRouter = new DataRouter[name]();
    },
    
start : function () {
        
this.dataRouter.get(
            
this.parse.bind(this)
        );
    },
    
parse : function (data) {
        
// parsing data, using this.privateMethods
    
}
});


Небольшой кусок кода из карточной игры Bridge на LibCanvas, демонстрирующий использование bind.
Суть ассинхронности в том, что ИИ не должен совершать никаких действий, пока летит карта.
Например, он берет карту из колоды, но может её туда положить только после того, как карта долетит, иначе будет неприятный для игрока эффект.
Bridge.AI = new Class({
    
// ..
    
putCardSmart : function (card) {
        
this.putCardcard,
            
// Этот метод вызовется только когда карта долетит, но он сохранит контекст.
            
this.finishSmart.bind(this)
        );
    },
    
getCardSmart : function () {
        
this.getCard(function (card) {
            
this.canPutCard(card) ?
                
this.putCardSmart(card) :
                
this.finishSmart();
        }.
bind(this)); // Мы захватываем контекст.
    
},
    
finishSmart : function () {
        
this.canFinishMove() ?
            
this.finishMove() :
            
this.movement();
    }
    
// ..
});


Примеры вредных советов


vl.vg/28.01.2010/tooltip-jquery
blog.kron0s.com/javascript-programming-patterns_2
habrahabr.ru/blogs/jquery/52185
Tags:
Hubs:
Total votes 113: ↑87 and ↓26 +61
Views 35K
Comments Comments 147