Hopp til innholdet

cjohansen.no

Objektorientering via closures

Som en praktisk applikasjon av closures ser vi på hvordan vi med hjelp av closures kan gi objektene våre en ekstra dimensjon.

Et simpelt objekt

Med konstruktører initierer vi nye objekter med new foran kallet til konstruktøren. Med objektliteraler kan vi lage nye objekter simpelthen via { property: "value" }. Vi kan også lage objekter som lagrer sin tilstand i en closure. Slike objekter lager vi enkelt nok ved å kalle en funksjon.

function person(name, email) {
    var firstName, lastName;

    var personObj =  {
        getName: function() {
            return firstName + (!!lastName ? " " + lastName : "");
        },

        setName: function(newName) {
            if (typeof newName === "undefined" || newName === null) {
                throw new TypeError("Cannot set name without name");
            }

            newName = newName.split(" ");
            firstName = newName.shift();
            lastName = newName.shift();
        },

        getEmail: function() {
            return email;
        }
    };

    personObj.setName(name);

    return personObj;
};

Denne funksjonen returnerer et nytt objekt som har tre metoder: getName(), setName(name) og getEmail(). Det som skiller dette objekter fra "vanlige" objekter er at det hverken i objektliteralen eller andre steder er satt noen name- eller email-properties.

Det som er spesielt interessant med dette er at closuren - altså det lokale skopet til person - ikke er tilgjengelig for kode utenfor dette skopet. Prøv å bruke koden over:

var christian = person("Christian Johansen", "cj@cjohanseeen.no");
console.log(christian.name); // undefined
console.log(christian.getName()); // "Christian Johansen"

Det er i det hele tatt ikke mulig å vite noe om den indre tilstanden til personobjekter. Implementasjonsdetaljer som at navn lagres som fornavn og etternavn skjules for omverdenen ved at medlemmene lagres i closuren som skapes av funksjonen person.

Denne måten å lage objekter på gir oss med andre ord noe JavaScript ikke eksplisitt støtter: tilgangskontroll. Det er to nivåer med tilgang som kan defineres: privat eller offentlig (public). I tillegg er det vanlig å notere seg at man har "privilegerte metoder" - altså metoder som er public, men som aksesserer privat data (følgelig kan man også lage public metoder som ikke har tilgang på privat tilstand).

Følgende gir en oversikt over hvor du kan definere hva:

"mitt.navnerom".namespace() = function() {
    // Private variable
    var prop1, prop2;

    // Private metoder
    function meth1() {
    }

    return {
        // Public medlemmer
        prop3: null,
        prop4: null,

        // Public metoder
        meth2: function() {
        },

        // Priviligerte metoder
        meth3: function() {
            return prop1;
        }
    };
};

What's the catch?

Ved å opprette objekter på denne måten oppnår vi elegant skjuling av implementasjonsdetaljer, men det er ikke gratis. Når vi lager objekter fra konstruktører har vi muligheten til å:

Sistnevnte gir en potensielt stor besparing i minnebruk ettersom objekter kun har en property dersom den overskriver standardverdien. I tillegg er alle metodene lagret kun en gang - på konstruktørens prototype. Dette i kontrast mot "closure-objektene", der metoder er definert på samtlige instanser. Dette siste poenget er viktig.

For å stikke fingeren i været sammenlignet jeg eksempelet over med dette:

function Person(name, email) {
    this.setName(name);
        this.email = email;
    }

    Person.prototype = {
        firstName: "",
        lastName: "",

        getName: function() {
            return this.firstName + (!!this.lastName ? " " + this.lastName : "");
        },

        setName: function(newName) {
            if (typeof newName === "undefined" || newName === null) {
                throw new TypeError("Cannot set name without name");
            }

            newName = newName.split(" ");
            this.firstName = newName.shift();
            this.lastName = newName.shift();
        },

        getEmail: function() {
            return this.email;
        }
};

Måling

Jeg er absolutt ingen ekspert på måling av minneforbruk i JavaScript, og på tidspunktet hadde jeg heller ikke Chrome (og dens tasktray) tilgjengelig. Jeg målte på følgende måte:

  1. Laget en side som opprettet 20.000 objekter via closures-metoden
  2. Åpnet Opera med denne siden
  3. Noterte minneforbruk for Opera
  4. Lukket siden og stengte nettleseren
  5. Gjentok eksperimentet for konstruktør-metoden

Hvorvidt dette er en holdbar måte å måle på vet jeg ikke, men det gir i det minste en slags pekepinn. Stemmer det indikerer testene mine ca 36% forbedring ved å bruke konstruktør og prototype fremfor closures. Dette betyr ikke at du aldri skal bruke closures som skissert her, men at du bør tenke før du gjør det.

Vel møtt i morgen for en diskusjon av memoisering av funksjoner.

Muligens relatert

2006 - 2010 Christian Johansen Creative Commons Lisens