Hopp til innholdet

cjohansen.no

Closures

Et svært nyttig - og kraftig - konsept er såkalte closures. Å forstå hva en closure er og hvordan du kan nyttegjøre deg av dem er essensielt for å ta full glede av JavaScript.

Rask definisjon

In computer science, a closure is a function that is evaluated in an environment containing one or more bound variables. When called, the function can access these variables

En closure er enkelt sagt en funksjon som har tilgang til konteksten den ble definert i. Dette blir svært nyttig når vi begynner å kalle funksjoner i andre kontekster enn den der de er definert.

Den beste måten å få grep om konseptet på er med noen eksempler. Husk at funksjoner i JavaScript er fullverdige objekter, og kan dermed tilordnes variabler, sendes som argumenter til andre funksjoner/metoder og ellers behandles som objekter ellers.

NB! Jeg bruker funksjon for å omtale både funksjoner og metoder. En metode er bare en funksjon som er tilordnet et objekt. Funksjon er nærmere nøkkelordet function, og er derfor min samlebetegnelse.

Eksempel: eventhandlere

Igår så vi flere eksempler på eventhandlere. En eventhandler er et eksempel på en closure:

function addLinkLogger() {
    var element = document.getElementsByTagName("a")[0];

    function clickEventHandler(event) {
        console.log(element.innerHTML);
        return false;
    };

    element.onclick = clickEventHandler;
}

addLinkLogger();
console.log(typeof element); // undefined - en lokal variabel i addLinkLogger

Her opprettes det en funksjon, addLinkLogger, som henter ut dokumentets første lenke og legger på en eventhandler for klikkeventen. Eventhandleren returnerer false slik at å klikke på lenken ikke åpner en ny side. Istedet logger den bare lenketeksten til konsollet.

Det som er interessant her er hvordan eventhandleren ( clickEventHandler) refererer den lokale variabelen element, selvom den ikke kalles i samme kontekst hvor element og clickEventHandler er definert. Funksjonen kalles seinere, når lenken klikkes, i lenkens kontekst. Alikevel har den en referanse til lokale variabler i den konteksten der den ble definert. Dette kalles en closure.

Den indre funksjonen - eventhandleren - blir en closure fordi den er tilgjengelig utenfor sin definerende kontekst. Dermed holdes en referanse til den definerende konteksten (lokale variabler i addLinkLogger) i live selv etter at den ytre funksjonen ( addLinkLogger) er ferdig med å kjøre.

Som dere kanskje gjetter er det ikke gratis å binde opp variabler i closures på denne måten, og man bør være litt var på når man lager en closure, og når man ikke gjør det. I eksempelet over kunne det vært en fordel å heller implementert det på følgende måte:

function clickEventHandler(event) {
    var element = (event || window.event).target;
    console.log(element.innerHTML);
    return false;
};

function addLinkLogger() {
    var element = document.getElementsByTagName("a")[0];
    element.onclick = clickEventHandler;
}

addLinkLogger();

Minneforbruk og closures

Det er en viktig forskjell mellom de to løsningene (rent bortsett fra at ingen av dem er tilrådelig, de er bare forenklede eksempler). Når en funksjon returnerer dannes det et skopobjekt for funksjonen som har en referanse til alle de lokale variablene. I de fleste tilfeller vil disse bli umiddelbart forkasta av garbage collectoren.

I tilfeller der du har indre funksjoner som i det første eksempelet kan ikke skopet skrapes av garbage collectoren fordi den indre funksjonen fortsatt har en referanse til det (closure). Dette betyr at referansen til DOM-elementet element holdes i live. I eksempelet må den jo det fordi den brukes i den indre funksjonen, men faktum er at sirkulære referanser mellom JavaScript-objekter og DOM-elementer er en kilde til minnelekkasje.

Med dette i bakhodet kan vi gjøre en kombinasjon av de to eksemplene som beholder det elegante i det første eksempelet - nemlig at løsningen er selvholdt - og det minneeffektive i den andre løsningen - at det ikke er noen sirkulære referanser mellom objekter og elementer.

function addLinkLogger() {
    var element = document.getElementsByTagName("a")[0];

    element.onclick = function (event) {
        var el = (event || window.event).target;
        console.log(el.innerHTML);
        return false;
    };

    // Her bryter jeg referansen
    element = null;
}

addLinkLogger();

I denne løsningen så vil fortsatt den indre funksjonen ha en referanse til skopet, men skopet har ikke lengre en referanse til DOM-elementet fordi dette nulles når vi er ferdig med å bruke det. Dette betyr også at den indre funksjonen heller ikke kan bruke det, og derfor henter vi ut elementet via event.target som i eksempel #2. Kompakt og fritt(?) for minnelekkasjer.

Merk at i IE vil denne typen sirkulære referanser fryse opp minne helt frem til nettleseren lukkes(!) Det er ikke nok at brukeren forlater websiden din. Så pass på!

Closures er et noe tungfordøyd tema, så vi kommer tilbake til det flere ganger i løpet av julekalenderen. For en langt grundigere gjennomgang kan jeg anbefale denne glimrende FAQ-en.

Vel møtt i morgen for en diskusjon rundt JavaScripts mest kryptiske konstruksjon: (function() {})();

Muligens relatert

2006 - 2010 Christian Johansen Creative Commons Lisens