JavaScript voor beginners: Functies
In de vorige aflevering hebben we het gehad over variabelen, herhalingslussen, en logische tests. We zullen in deze aflevering zien hoe we deze drie elementen kunnen gebruiken bij het schrijven van functies. Ook zullen we het hebben over het begrip "recursie", en laten zien hoe we zelf in JavaScript een recursieve functie kunnen creëren. Daarna zullen het hebben over de ingebouwde functies van JavaScript. Tenslotte zullen we ingaan op het verschil tussen functies en "methods".Voordelen
In principe zouden we onze Javascript-opdrachten steeds in de HTML-pagina kunnen opnemen op die plaatsen waar ze "nodig zijn". Het komt echter regelmatig voor dat (bijna) identieke stukjes script op meerdere plaatsen gebruikt worden. In dergelijke gevallen is het vaak handig om er een functie van te maken. We hoeven dan minder in te tikken en onze pagina wordt kleiner. Bovendien neemt de leesbaarheid van de pagina vaak toe, en hoeven we eventuele wijzigingen maar op één plaats aan te brengen, zodat we onze code makkelijker kunnen onderhouden.Om gebruik te kunnen maken van een functie moeten we twee dingen doen: we moeten de functie eerst definiëren, en vervolgens moeten we de functie aanroepen. Het definiëren van een functie gaat als volgt:
function test()
{
document.write("Hello, world")
} We gebruiken dus het gereserveerde woord "function", gevolgd door de naam van de functie (in dit geval: "test") en de twee haakjes "(" en ")". Zo meteen zullen we zien dat er tussen die haakjes een of meer " parameters" kunnen staan, maar functies zonder parameters zijn ook toegestaan. Om het kerngedeelte van de functie, waarin staat wat er moet gebeuren als de functie wordt aangeroepen, staan de twee haken "{" en "}". Deze haken gebruiken we ook om twee of meer opdrachten samen te voegen tot een "blok" dat bijvoorbeeld meerdere malen moet worden uitgevoerd in een herhalingslus. Bij het definiëren van functies zijn de haken echter altijd verplicht, ook als de functie slechts één enkele opdracht bevat.
Nu we de functie hebben gedefinieerd, moeten we haar nog aanroepen. Dat gaat als volgt:
test() Bij het aanroepen van een functie gebruiken we dus de naam ervan, gevolgd door de twee haakjes, maar zonder het woord "function".
De functie uit dit voorbeeld is niet zo heel erg interessant: er wordt alleen maar een tekst op het scherm getoond, maar er wordt geen waarde teruggegeven. (In een taal als Pascal zouden we dit een procedure noemen in plaats van een functie.) Een "typische" functie daarentegen geeft wel een waarde terug; de aanroep van zo'n functie is dan:
tst = test() We kennen hier dus aan de variabele tst de waarde toe die als resultaat wordt teruggegeven door de functie test. Het probleem is alleen nog, dat onze voorbeeldfunctie helemaal geen waarde teruggeeft! Na bovenstaande aanroep zal variabele tst dan ook de waarde "undefined" hebben. Om hier iets aan te doen, moeten we nog een opdracht aan de functie toevoegen waarmee een waarde wordt geretourneerd, bijvoorbeeld:
return 123 Ook "return" is een gereserveerd woord binnen JavaScript; het mag alleen gebruikt worden binnen een functie. Al met al ziet ons voorbeeld er nu als volgt uit:
function test()
{
document.write("test")
return 123
}
tst = test()
document.write(tst) Het resultaat zal zijn dat variabele tst de waarde 123 krijgt en dat "test123" naar het scherm wordt geschreven. Indien we de waarde van tst niet hoeven te onthouden voor gebruik verderop in het programma, mogen we de laatste twee opdrachten overigens ook samenvoegen tot:
document.write(test())
Parameters
We hebben aan de hand van een heel eenvoudig voorbeeld de basisbeginselen van het gebruik van functies besproken. Nu zullen we zien hoe we diezelfde beginselen toepassen bij het schrijven van een wat ingewikkelder functie. Als voorbeeld zullen we de functie "faculteit" kiezen, een functie die bijvoorbeeld bij statistische berekeningen regelmatig gebruikt wordt. De wiskundige notatie is: n! (een n met een uitroepteken, uitgesproken als "n faculteit", waarbij n een geheel getal groter dan of gelijk aan een is). Deze notatie kunnen we echter niet gebruiken volgens de JavaScript-regels; in plaats daarvan gebruiken we: faculteit(n). Dit betekent dat sprake is van een functie met een "parameter", zeg maar: een waarde (n) die aan de functie wordt aangeboden als invoer. We kunnen ook functies hebben met meerdere parameters; in dat geval worden de parameters van elkaar gescheiden door middel van komma's, bijvoorbeeld: mijnfunctie(a,b,c).Om onze functie daadwerkelijk te kunnen schrijven, moeten we ons herinneren hoe "faculteit" ook al weer berekend wordt. Dat gaat als volgt: 1! is 1; 2! is 1*2=2; 3! is 1*2*3=6; enzovoort, dus n! is 1*2*3*…*n. We kunnen de functie nu schrijven met behulp van een herhalingslus:
{ var tmp = 1 for (i=1;i<=n;i++) tmp = tmp * i return tmp } Als we de functie nu bijvoorbeeld aanroepen met de opdracht: document.write(faculteit(5)), dan wordt de waarde 120 naar het scherm geschreven. We maken binnen de functie dus gebruik van een tijdelijke variabele tmp die de beginwaarde 1 krijgt. Vervolgens wordt de lus "n" maal uitgevoerd, waarbij "n" de parameter is waarmee we de functie aanroepen; in ons voorbeeld dus vijf.
Bij de eerste iteratie wordt aan tmp een nieuwe waarde toegekend, te weten: de oude waarde van tmp (de door ons toegekende beginwaarde 1) maal de waarde die de lusteller op dat moment heeft (eveneens 1), dus de "nieuwe" waarde is ook 1. Bij de tweede iteratie wordt tmp gelijk aan 1 * 2, dus 2; bij de derde iteratie wordt tmp gelijk aan 2 * 3, dus 6; bij de vierde iteratie wordt tmp 6 * 4, dus 24; en bij de vijfde iteratie tenslotte krijgt tmp de waarde 24 * 5, dus 120. Dat eindresultaat wordt teruggegeven als resultaat van de functie door middel van de opdracht "return tmp".
We kunnen hier twee dingen opmerken. Allereerst is het van belang dat we aan tmp een beginwaarde geven (d.m.v. "tmp = 1"). Zouden we dat niet doen, dan zou tmp bij de eerste iteratie nog "undefined" zijn, en de opdracht tmp = tmp * 1 leidt dan tot de foutmelding "tmp is not a number".
Daarnaast zouden we kunnen opmerken dat we in dit geval aan lusteller i ook de beginwaarde 2 zouden kunnen geven; roepen we onze functie namelijk aan met "faculteit(1)", dan heeft de parameter n immers de waarde 1 en is de voorwaarde voor uitvoering van de lus meteen al onwaar (want 2 is niet kleiner dan of gelijk aan 1). De functie geeft meteen tmp terug als resultaat, en tmp heeft nog de beginwaarde 1; dat klopt dan mooi, want 1! is gedefinieerd als 1. In dit voorbeeld maakt het weinig uit (de functie zal er niet merkbaar sneller door worden), maar in het algemeen moeten we goed opletten welke beginwaarden en voorwaarden voor uitvoering we in onze herhalingslussen gebruiken!
Recursie
We hebben de functie faculteit nu geschreven met behulp van een herhalingslus, en dat ligt ook tamelijk voor de hand. Er is echter een alternatieve wijze, die gebruik maakt van het feit dat functies in JavaScript zelf ook functies kunnen aanroepen. Dat kunnen andere functies zijn, maar functies kunnen eventueel ook zichzelf aanroepen. In dat laatste geval spreken we van "recursie".Als we nog eens kijken naar de betekenis van "faculteit", dan zien we bijvoorbeeld dat 4! = 1 * 2 * 3 * 4, en 5! = 1 * 2 * 3 * 4 * 5. Met andere woorden, 5! is gelijk aan 4! maal 5, en in het algemeen geldt dat n! gelijk is aan (n-1)! maal n. Van deze eigenschap maken we gebruik bij onze recursieve definitie, die er als volgt uit ziet:
function faculteit(n)
{
if (n == 1)
return 1
else
return faculteit(n-1) * n
} We weten dat voor het "speciale geval" 1! geldt dat het resultaat gelijk is aan 1, terwijl voor het "algemene geval" n! geldt dat het resultaat gelijk is aan (n-1)! maal n, zoals hierboven beschreven. Deze regels zien we heel duidelijk terug in onze recursieve definitie.
Indien we deze functie aanroepen met "faculteit(5)", dan is "n" in eerste instantie gelijk aan 5, zodat het "else"-gedeelte van de if-opdracht wordt uitgevoerd. De waarde van n wordt tijdelijk opgeslagen (op de zogenaamde "stack"), met de bedoeling om die t.z.t. te vermenigvuldigen met het resultaat van "faculteit(n-1)", oftewel: "faculteit(4)", zodra dat bekend is. Bij het berekenen van "faculteit(4)" wordt opnieuw het else-gedeelte uitgevoerd: ook de waarde 4 wordt op de stack gezet, terwijl nu "faculteit(3)" berekend moet worden.
Dit gaat zo door totdat de stack de getallen 5, 4, 3 en 2 bevat, en "faculteit(1)" moet worden berekend. Nu geldt eindelijk dat n gelijk is aan 1, dus deze functieaanroep geeft de waarde 1 terug. Vervolgens wordt deze waarde vermenigvuldigd met de laatste waarde die op de stack was gezet (2), waarna het resultaat van "faculteit(2)" bekend is; tegelijk wordt die laatste waarde 2 van de stack verwijderd. Nu wordt het resultaat van "faculteit(2)" vermenigvuldigd met wat vervolgens de laatste waarde op de stack is (3), enzovoort, net zolang totdat het resultaat van "faculteit(4)" met de waarde 5 op de stack is vermenigvuldigd, en dan zijn we klaar.
Zoals wel uit dit voorbeeld blijkt, is het gebruik van recursie relatief ingewikkeld. We moeten er bijvoorbeeld aan denken dat het laatste getal dat we op de stack hebben gezet er als eerste weer vanaf komt; we spreken hier van "Last In First Out", ofwel LIFO. Daarnaast moeten we er voor zorgen dat de recursieve aanroepen een keer stoppen.
Als we de recursieve versie van onze functie bijvoorbeeld aanroepen met "faculteit(0)", dan zal nooit gelden dat n gelijk is aan 1, dus de "return 1" zal ook nooit worden uitgevoerd. In eerste instantie roepen we de functie immers aan met n = 0, vervolgens (in het else-gedeelte) met n = -1, dan met n = -2, enzovoort.
In theorie zou dit oneindig zo door kunnen gaan, dus onze functie zou nooit een waarde teruggeven en ons programma zou "hangen". In de praktijk zou JavaScript merken dat er te veel getallen op de stack komen te staan, en het programma zou afgebroken worden met een foutmelding als "stack overflow". Ditzelfde gebeurt trouwens als we een zeer grote waarde voor n kiezen, bijvoorbeeld als we de functie aanroepen met "faculteit(100000)".
Bovendien is de recursieve variant aanzienlijk trager dan de versie met de herhalingslus. Gezien deze nadelen zullen we niet zo vaak gebruik maken van recursie, maar er zijn gevallen waarin recursie een heel elegante definitie van een functie mogelijk maakt, bijvoorbeeld bij het sorteren van gegevens.
Ingebouwde functies
JavaScript kent ook enkele ingebouwde functies die we kunnen aanroepen zonder dat we ze eerst zelf hoeven te definiëren. Een voorbeeld daarvan is de functie "parseInt". We geven een string als parameter mee aan de functie, en krijgen hetzij een geheel getal, hetzij de waarde "NaN" (not a number) terug. Bijvoorbeeld: parseInt("3") geeft als resultaat het getal 3, net als parseInt("3Q").Zodra de functie een teken tegenkomt dat niet kan worden geïnterpreteerd als deel van een geheel getal, wordt alles vanaf dat teken tot aan het einde van de string genegeerd. Zo geeft parseInt("3.14") het resultaat 3, want een "decimal point" (onze komma) kan geen deel uitmaken van een geheel getal, en parseInt("-3.14") geeft het resultaat -3. Kan het eerste teken van de string direct al niet deel uitmaken van een geheel getal, dan krijgen we het resultaat "NaN" terug.
Een vergelijkbare ingebouwde functie is parseFloat, alleen kunnen we daar ook een getal mèt een decimal point terugkrijgen. Zo geeft parseFloat("-3.14") de waarde -3.14 terug, en parseFloat("3q") de waarde 3.
JavaScript bevat ook twee functies om strings te kunnen gebruiken waarin tekens anders dan letters of cijfers voorkomen. De functie "escape" zet een niet-alfanumeriek teken om naar de notatie "%xx", waarbij xx de ASCII-waarde van dat teken is. Zo geeft escape(" ") het resultaat "%20". Voor tekens die wèl alfanumeriek zijn, geeft deze functie het teken zelf als resultaat terug, dus escape("a") geeft als resultaat "a". De functie "unescape" doet precies het tegenovergestelde: unescape("a") geeft als resultaat "a", en unescape("%20") geeft als resultaat " ". Deze functies zijn van belang om niet-alfanumerieke tekens van de ene web-pagina aan de andere door te kunnen geven.
Tenslotte kent JavaScript de ingebouwde functie "eval". Deze functie is zeer krachtig: we geven als parameter een string mee waarin JavaScript-expressies en opdrachten mogen voorkomen, en krijgen het bijbehorende resultaat terug. In de expressies mogen ook variabelen voorkomen. We kunnen bijvoorbeeld aan variabele x de waarde 10 toekennen, en vervolgens schrijven: y = eval("x+faculteit(5)+2"), waarbij "faculteit" onze eerder gedefinieerde functie is. Het resultaat zal zijn dat y de waarde 132 krijgt.
Functies en methoden
Laten we eens een functie definiëren waarmee we kunnen bepalen welk van de twee als parameters meegegeven getallen het grootst is. Dat kan bijvoorbeeld als volgt:function max(x,y) { if (x > y) return x else return y } Als we deze functie bijvoorbeeld aanroepen met: m = max(1,2), dan krijgt m de waarde 2; met m = max(-1,-2) krijgt m de waarde -1 (want -1 is groter dan -2).
Nu bestaat er in sommige programmeertalen een ingebouwde functie max die precies hetzelfde doet als de door ons gedefinieerde functie; als JavaScript zo'n functie zou bevatten, zouden we die gewoon aan kunnen roepen zonder de functie eerst te hoeven definiëren. En inderdaad kunnen we in JavaScript schrijven: m = Math.max(-1,-2) met het zelfde resultaat als in ons vorige voorbeeld.
Er is echter een subtiel verschil: in het geval van "Math.max" is sprake van een ingebouwd "object" Math, dat de "methode" max bevat. Methoden horen dus, anders dan ingebouwde functies, bij een specifiek object en niet bij de taal "als geheel". Op objecten en hun methoden (methods), eigenschappen (properties) en gebeurtenissen (events) zullen we uitgebreider ingaan in de afleveringen 6 en 8 van deze cursus. Voor het moment volstaat het om te weten dat veel van de functies die in andere talen zijn ingebouwd, in JavaScript ook voorhanden zijn, maar dan als methoden van ingebouwde objecten als Math, Date en string.
Conclusie
In deze aflevering hebben we gezien hoe we zelf al dan niet recursieve functies kunnen definiëren, en we hebben gezien dat JavaScript ook een aantal ingebouwde functies kent. Tenslotte hebben we functies vergeleken met methoden.In de volgende aflevering zullen we het hebben over de "operators" waarmee allerlei rekenkundige en logische bewerkingen kunnen worden uitgevoerd, en over de voorrangsregels die gelden als we meer dan een operator in een opdracht gebruiken. Tenslotte zullen we kennis maken met enkele bronnen op het Web waar we verdere informatie over JavaScript kunnen vinden.
