V Javascriptu se mi často stává, že mám vytvořenou nějakou objektovou
strukturu a nějakou metodu takového objektu chci použít jako ovladač
události (event handler). Ale zde nastává problém v tom, že
klíčové slovo this odpovídá při volání handleru ne
mateřskému objektu, kterému patří metoda, ale objektu, který vyvolal
událost. Příklad:
function myObject(data) {
this.Data = data;
this.clickHandler = function() {
alert(this);
}
}
var obj = new myObject(data);
someElement.onclick = obj.clickHandler;
Pokud nastane událost someElement.onclick, zavolá se sice
„metoda“ objektu obj.clickHandler, ale this bude
v tom okamžiku znamenat objekt someElement, na které událost
vznikla.
Často je v těchto handlerech potřeba pracovat s mateřským
objektem – ostatně většinou takovéto konstrukce píšeme coby nějaké
obecné knihovny či komplexní struktury a chceme využívat hlavní výhody
objektového programování: tedy uzavřenost a kontext, kdy si každý
podobjekt spravuje své věci, nemíchá se do jiných činností a nikdo se
zase nemíchá do té jeho. Tento nedostatek a chybějící zpětná reference
na mateřský objekt se dá různými způsoby obejít. Nejčastěji doplněním
reference do potenciálních event triggerů – tedy objektů, které
můžou vyvolat danou událost a přes které se pak lze na mateřský objekt
dostat, např.:
this.clickHandler = function() {
alert(this.owner);
}
someElement.onclick = obj.clickHandler;
someElement.owner = obj;
V tomto případě bude při vyvolání události v handleru hodnotou
this.owner právě mateřský objekt obj. Jenomže ne
vždycky je možné takové přiřazení udělat – objektů je vytvořených
mnoho a v daném okamžiku nevíme, se kterým se právě pracuje, nebo
event trigger není dostupný, abychom do něj mohli něco doplnit atd.
Typicky se to stává při použití externích knihoven a frameworků. Vezměme
jako příklad třeba jQuery a načítání dat přes
ajax:
function myObject() {
this.load = function() {
$.get( this.URL, this.Params, this.onloadHandler );
}
this.onloadHandler = function(data) {
}
}
var obj = [];
for (var i=0;i<objCnt;i++) obj[i] = new myObject(data);
obj[x].load();
Zde je po úspěšném načtení ajaxových dat opět coby handler
zavolána metoda obj.onloadHandler, ale tentokrát v ní
this odpovídá instanci interního objektu
jQuery.ajax, ke kterému se (slušně) nedostaneme a nedá se do
něj nic přidávat. Instancí MyObject je také mnoho a nemůžeme
zjistit, který to právě je. A konečně volání ajaxu je asynchronní a
může jich probíhat současně několik, takže nějaká globální proměnná
nás taky nezachrání.
Je zde ale jedna věc, která není na první pohled vůbec zřejmá: ačkoli
je dotyčný handler volán cizím objektem zvnějšku a je jakoby
„vytržen“ ze svého kontextu v mateřském objektu, pořád zůstává
jeho „metodou“ a při volání má jeho kontext a jmenný prostor. Jsou zde
tedy dostupné všechny proměnné definované lokálně v rámci mateřského
objektu. A dá se toho využít:
function myObject() {
var thisObj = this;
this.load = function() {
$.get( this.URL, this.Params, this.onloadHandler );
}
this.onloadHandler = function(data) {
thisObj.Data = data;
thisObj.doAnythingElse();
}
}
V lokální proměnné thisObj má objekt uloženu referenci
sám na sebe, a tato reference bude dostupná i event handleru
zavolanému úplně jiným objektem. Bude zde platit, že this je
objekt, který vygeneroval událost (event trigger), a
thisObj je objekt sám.
Doplnění:
Díky Davidovi za skvělý nápad v komentářích! Ještě jsem ho trochu
upravil a vzniklo tak zatím nejlepší a formálně asi nejčistší
řešení:
function dynamicHandler(obj,method) {
return function(){ method.apply(obj,arguments) };
}
function myObject() {
this.load = function() {
$.ajax({
url: this.URL,
data: this.Params,
success: dynamicHandler(this,this.onloadHandler),
error: dynamicHandler(this,this.onerrorHandler)
});
}
this.onloadHandler = function(Data) {
}
this.onerrorHandler = function(XHR,ErrorString,Exception) {
}
}
Jak je asi vidět, takhle to řešení funguje i s libovolným počtem
parametrů, což je docela důležité. Standardní event handlery sice
předávají obvykle parametr jen jeden (event), ale např. jQuery už
vrací parametrů více (třeba callback $.ajax.error vrací
argumenty až tři).