Javascript: Reference na mateřský objekt v event handlerech [příspěvek v archivu]

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) {
     /* this.Data = 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).

Petr Staníček, 7. 3. 2008 v 12.17 • Rubrika: IT, WebdesignKomentáře: 8

8 komentářů k článku »Javascript: Reference na mateřský objekt v event handlerech«

[1] Vložil(a): David Grudl 7. 3. 2008 v 14.38

WOW!!!

Škoda, že jsi tento článek nenapsal už před dvěma lety. Pixy, hodně toho svým čtenářům ještě dlužíš :-))

[2] Vložil(a): ehmo 7. 3. 2008 v 15.08

velmi pekny clanok.
ono javascript je divny uz vo svojej podstate
alert(typeof Boolean[-6]);
clovek obcas zasne ak featury ma v sebe a co vsetko dokaze „vygrcat“

[3] Vložil(a): David Grudl 7. 3. 2008 v 15.38

Abych vysvětlil své nadšení, s tímto problémem jsem se taky hodně trápil a nakonec jsem našel řešení, které používám třeba v Míchátku:

Celá magie se skrývá v této nenápadné funkci:

function createHandler(obj, handler)
{
    return function(e) { obj[handler](e); }
}

// a pak přiřazujeme handlery tímto způsobem:
var obj = new myObject(data);
someElement.onclick = createHandler(obj, 'clickHandler');
// místo obj.clickHandler;

[4] Vložil(a): karf 7. 3. 2008 v 17.19

Já obvykle používám takovejhle vzor s anonymní funkcí:

this.load = function(){
    var that = this;
    $.get( this.URL, this.Params, function(data) { return that.doSomething(data); } );
}

[5] Vložil(a): Pixy 7. 3. 2008 v 17.31

[3] Je tam nějaký jiný rozdíl, než že místo thisObj se to jmenuje that? Z hlediska principu je jedno, jestli je tam anonymní funkce nebo je mezi tím více volání. Nicméně díky za doplnění.

[6] Vložil(a): karf 7. 3. 2008 v 19.14

[4] Je tam ten malý rozdíl, že v mém případě se vytváří closure pro anonymní funkci až v metodě load.

[7] Vložil(a): Pixy 7. 3. 2008 v 19.40

[5] Jasně. Já se jen bál, jestli jsem tam nepřehlídl nějaký zásadnější rozdíl. :-) Dík.

[8] Vložil(a): filer 19. 3. 2008 v 10.11

Akurát včera som narazil ešte na (formálne) trochu iné riešenie.

Function.prototype.bind = function(obj)
{
   var method = this;
   temp = function()
   {
      return method.apply(obj, arguments);
   }
}

function myObject() {
// ...
this.load = function() {
   $.ajax({
      url: this.URL,
      data: this.Params,
      success: this.onloadHandler.bind(this),
      error: this.onerrorHandler.bind(this)
      });
   }
this.onloadHandler = function(Data) {
   // ...
   }
this.onerrorHandler = function(XHR,ErrorString,Exception) {
   // ...
   }
}

Váš komentář

Upozornění: Pokud vás téma tohoto příspěvku nezajímá, nebaví, dotýká se vás či vás dokonce uráží, tak prosím odejděte a pokud možno se nadále ve vlastním zájmu dalším podobným vyhýbejte. Hlavně se to prosím nesnažte autorovi sdělovat v komentářích, takové příspěvky nikoho nezajímají a budou nejspíš vymazány. Totéž platí pro vaše názory na osobu autora, na jiné přispěvatele, mluvení z cesty, ze spaní či pod vlivem omamných látek a další podobné výlevy nesouvisející s tématem článku. Jinými slovy, toto je prostor soukromého blogu určený pro komentování příspěvku publikovaného výše, nikoli k chatování a volné diskusi. Děkuji za pochopení.

Abyste mohli komentovat, musíte se přihlásit.