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.