Javascript: Reference na mateřský objekt v event handlerech

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).

8 komentářů u „Javascript: Reference na mateřský objekt v event handlerech“

  1. 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. 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. 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 href=„http://­latrine.dgx.cz/co­lor-mixer-aneb-michatko“ rel=„nofollow ugc“>http://la­trine.dgx.cz/co­lor-mixer-aneb-michatko:

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

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

  8. 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) {
       // ...
       }
    }

Napsat komentář