Hopp til innholdet

cjohansen.no

Javascript-funksjoner, objekter og this

Har du kodet objektorientert Javascript og koblet det bare såvidt med event-handlere og/eller Ajax har du sannsynligvis støtt på problemer med this. I motsetning til i mange andre språk så er nøkkelordet this svært dynamisk i Javascript.

Utgangspunktet

For å illustrere problemet kjører vi på med et eksempel. Følgende er et Javascript-objekt som opprettes med en HTML-lenke og en alternativ tekst. Deretter vil objektet modifisere lenkens oppførsel slik at klikk ikke åpner siden den peker til, men i stedet bare bytter tekst i lenken.

Ønsket funksjonalitet kan testes her.

HTML:

<p>
  <a href="/" id="target">Denne lenken</a> bytter
  tekst når man klikker på den.
</p>

Javascript:

var LinkTextSwapper = function(link, text) {
    this.link = link;
    this.text = text;
    this.link.onclick = this.swapText;
};

LinkTextSwapper.prototype = {
    swapText: function(e) {
        var currentText = this.link.innerHTML;
        this.link.innerHTML = this.text;
        this.text = currentText;

        return false;
    }
};

Dette er et enkelt objekt. Den første funksjonen er konstruktøren, og setter tre egenskaper: lenken, som er en HTML-lenke ( a-element), en tekst som skal overta som lenketekst på klikk, og til sist setter den event-handleren på lenken.

Den andre funksjonen settes på prototypen til det nye objektet vårt. Denne metoden tar eventen som parameter og bytter teksten på lenken. (Teksten byttes ved å skrive direkte til innerHTML, noe som ikke er tillatt i XHTML. Dette er bare et simpelt eksempel.)

For å teste dette kjører vi funksjonen når siden lastes:

window.onload = function(e) {
    var link = document.getElementById('target');
    new LinkTextSwapper(link, 'Linken herane');
};

Voila. Problemet er bare at dette kræsjer når du trykker på lenken. Meldingen du får er this.link has no properties. Hvorfor?

Problemet: vekslende kontekst

Grunnen til at lenken ikke fungerer som planlagt er kontekst. Når swapText kjøres er ikke konteksten lengre vårt opprinnelige LinkTextSwapper-objekt. Konteksten er nå i stedet lenken, så this inneholder istedet en referanse til lenken.

Lagre konteksten

Denne dynamiske behandlingen av this vil overraske mange som er vant til mindre dynamiske språk. Det er flere måter å komme rundt dette på. La oss se på den mest åpenbare først. Det er kun tilegningen av event-handleren som gjør utslag her, resten av koden er lik. Jeg viser derfor kun konsktruktøren:

var LinkTextSwapper = function(link, text) {
    this.link = link;
    this.text = text;
    var me = this;
    this.link.onclick = function(e) { me.swapText(e); };
};

Dette fungerer. Hvorfor? Fordi vi "husker" den opprinnelige konteksten representert av this i den lokale variabelen me. Inne i den anonyme funksjonen settes this til å referere lenken, mens me forblir det den var. Dermed kalles riktig funksjon.

Det funker, men det er ikke helt kvasst enda. I en vanlig "web 2.0"-applikasjon vil det være mange event-handlere som skal settes (husk, Ajax-funksjonalitet fungerer også gjennom events). Hvis du ønsker å utvikle koden din objektorientert vil du få mye duplisering.

Bind this til en metode

En mer generisk løsning kan være å binde konteksten/ this til den gitte funksjonen. La oss se en mulig løsning på dette:

function bind(fn, obj) {
    return function() {
        return fn.call(obj, arguments);
    };
}

Denne funksjonen trenger litt forklaring. La oss først se hvordan den kan brukes. Det tidligere eksempelet kan nå skrives som:

var LinkTextSwapper = function(link, text) {
    this.link = link;
    this.text = text;
    this.link.onclick = bind(this.swapText, this);
};

Dette er egentlig det samme vi gjorde i forrige eksempel, bare abstrahert ett nivå høyere opp. bind-funksjonen returnerer funksjonen som kaller metoden med riktig kontekst. Det er viktig å få med seg her at bind returnerer en funksjon som brukes som event-handler. Det er ikke bind selv som håndterer eventen.

Function.call og arguments

I bind brukes det to viktige konsepter for callbacks i Javascript: Det første er call-metoden. Denne metoden kan kalles på et funksjonsobjekt (alt er objekter i Javascript, funksjoner og metoder også). Effekten av å kalle call på en funksjon/metode er at funksjonen blir kjørt, men med spesifisert kontekst.

Det første argumentet til call er konteksten som er tilgjengelig som this i funksjonen. Resterende argumenter sendes videre til funksjonen.

Det andre viktige å notere seg er arguments. Denne inneholder alle argumentene som funksjonen ble kalt med. I koden vår sender vi argumentene bare videre, slik at bind-funksjonen vår bare blir en metode-proxy som fikser konteksten for oss.

En tilsvarende metode på funksjonsobjektet er apply. Den eneste forskjellen mellom denne og call er behandlingen av argumenter. Der call sender ett og ett argument videre, sender apply bare en array med argumenter som er tilgjengelig ett og ett i funksjonen som kjøres.

Sømløs integrasjon

For å gjøre det hele enda litt glattere kan vi implementere bind rett i Function.prototype. Ved å gjøre dette vil alle Javascript-funksjoner ha metoden og syntaksen blir litt smartere.

Function.prototype.bind = function(obj) {
    var fn = this;
    return function() { return fn.call(obj, arguments); };
}

var LinkTextSwapper = function(link, text) {
    this.link = link;
    this.text = text;
    this.link.onclick = this.swapText.bind(this);
};

LinkTextSwapper.prototype = {
   swapText: function(e) {
       var currentText = this.link.innerHTML;
       this.link.innerHTML = this.text;
       this.link.blur();
       this.text = currentText;

       return false;
   }
};

De av dere som bruker Prototype vil kjenne igjen dette.

Minnelekkasjer

Mange andre enn meg har snakket om dette lenge, og en av dem er Laurens van den Oever. Han har implementert en funksjon-objekt-bindingscache (puh!) for å unngå å binde ett objekt til samme funksjon mer enn én gang, og for å unngå minnelekkasjer forbundet med "closures". Å gjøre dette er ikke spesielt vanskelig, og hans artikkel er å anbefale.

Hvem sa at Javascript ikke er noe morsomt å programmere i?

Muligens relatert

2006 - 2012 Christian Johansen Creative Commons Lisens