Hopp til innholdet

cjohansen.no

Prototyper og JavaScripts innebygde typer

Ved hjelp av JavaScripts prototype-kjeder kan vi bygge funksjonalitet inn i objekter vi ikke selv har laget - så også JavaScripts innebygde objekter. Vi ser på hvordan dette gjøres og noen nyttige applikasjoner.

Prototyper

Som forklart både i et tidligere innlegg om objektorientering i JavaScript, og tidligere i julekalenderen har alle JavaScript-objekter en prototype - et objekt de er basert på. For eksempel er alle strenger basert på String.prototype. Et objekts prototype er på en måte tilsvarende en klasse - men husk at en prototype er et objekt, ikke en klasse (jada, klasser kan være objekter, men JavaScript har ikke klasser).

trim() igjen

Tidlig i kalenderen laget vi en trim-funksjon som trimmet innledende og avsluttende whitespace i en streng. Idag gjør vi den objektorientert, sånn at det blir " min streng ".trim(); i stedet for trim(" min streng ");. La oss oppdatere testcaset fra sist:

new Test.Unit.Runner({
    testTrimTrailingSpace: function() { with(this) {
        assertEqual("Streng", "Streng ".trim(),
                    "Trailing space should be removed");
    }},

    testTrimLeadingSpace: function() { with(this) {
        assertEqual("Streng", " Streng".trim(),
                    "Leading space should be removed");
        assertEqual("Streng", "Streng   ".trim(),
                    "All trailing spaces should be removed");
    }},

    testNoTrimInnerSpace: function() { with(this) {
        var sentence = "A complete sentence with     lots of whitespace.";
        assertEqual(sentence, sentence.trim(),
                    "Inner space should not be removed");
    }},

    testTrimTrailingAndLeadingSpace: function() { with(this) {
        assertEqual("Streng", " Streng ".trim(),
                    "Both leading and trailing space should be removed");
        assertEqual("Streng", "   Streng   ".trim(),
                    "All leading and trailing spaces should be removed");
    }},

    testTrimAnyWhiteSpace: function() { with(this) {
        assertEqual("Streng", " \n\t Streng".trim(),
                    "Any kind of whitespace should be trimmed");
        assertEqual("Streng", "   Streng\n".trim(),
                    "Trailing newlines should be trimmed");
    }}
}, { testLog: "testlog" });

Kjør testen, og observer at den feiler.

Hvordan få fatt i prototypen?

Det er ingen måte å få tak i et objekts prototype direkte (foruten __proto__, som er en Firebug-feature). Derfor må vi vite hvordan prototypen settes for et objekt. Et objekt (kan) instansieres fra en konstruktør, og en konstruktør har en prototype-egenskap som definerer prototypen for nye objekter som instansieres fra denne konstruktøren. Puh!

Strenger kommer fra String-konstruktøren. La oss gjøre et eksperiment: hente en metode på prototypen og kalle den:

console.log(typeof String); // "function"
console.log(typeof String.prototype); // "object"
console.log(typeof String.prototype.indexOf); // "function"
console.log(String.prototype.indexOf("script")); // -1

Ikke overraskende returnerte vårt forsøk på å finne første plassering av substrengen script -1. Metodene på prototypen forventer at this er et strengeobjekt. Heldigvis kan vi sette this til hva som helst når vi bruker call og apply, så for å søke etter en substreng i en annen streng på en utradisjonell måte kan vi gjøre:

console.log(String.prototype.indexOf.call("JavaScript", "script")); // 4

Voila!

Flytte trim()

Ok, da har vi klart å lese ut - og kjøre - noe fra prototypen til strenger. Siden alt er åpent i JavaScript kan vi også skrive til prototypen:

String.prototype.trim = function(str) {
    return str.replace(/^\s+|\s+$/g, '');
};

Dette feiler fullstendig. Hvorfor? Vel, når vi nå operer som en metode på strengens prototype fremfor en frittstående funksjon er det andre grep som må til for å få tak i strengen som skal vaskes. Som jeg forsøkte å forklare over, vil this inne i metodene på prototypen alltid tilsvare et objekt av den aktuelle typen (i vårt tilfelle en streng). Altså må vi ta bort innparameteren og heller jobbe direkte på this:

String.prototype.trim = function() {
    return this.replace(/^\s+|\s+$/g, '');
};

Testene kjører, og vi kan trygt nyte en kaffe.

Bruksområder

Med denne kunnskapen i verktøykassa er det bare fantasien som stopper deg. Om du vil kan du legge inn all funksjonaliteten fra ditt favorittspråk i JavaScript, sånn at det ligner mer. Det er i praksis langt på vei dette prototype.js har gjort - de har dratt JavaScript nærmere Ruby.

MooTools legger også til funksjoner i de innebygde typene, se bare deres "Native"-dokumentasjon.

Et svært interessant bruksområde for dette er å legge til eller fikse (standardisert) funksjonalitet. Mozilla byr i sin dokumentasjon av JavaScript 1.5 på sin egen implementasjon av Array.forEach() slik at man kan sørge for at nettlesere som ikke støtter denne får den definert:

if (!Array.prototype.forEach) {
    Array.prototype.forEach = function(fun /*, thisp*/) {
        var len = this.length;

        if (typeof fun != "function") {
            throw new TypeError();
        }

        var thisp = arguments[1];

        for (var i = 0; i < len; i++) {
            if (i in this) {
                fun.call(thisp, this[i], i, this);
            }
        }
    };
}

Dette gir alle nettlesere muligheten til å loope arrays som dette:

[9, 8, 7, 6, 5, 4, 3, 2, 1].forEach(function(element, index, array) {
    console.log(element);
});

Vel møtt i morgen, når vi tar i bruk denne kunnskapen i en diskusjon om navnerom i JavaScript.

Muligens relatert

2006 - 2010 Christian Johansen Creative Commons Lisens