JavaScript/Funktion
Eine Funktion bezeichnet im Quellcode ein Stück Javascript, das an einer Stelle gespeichert ist und von anderen Stellen aus beliebig oft aufgerufen werden kann. Funktionen verfügen über Parameter, darunter versteht man eine Liste von Variablennamen, die während der Ausführung wie lokale Variablen zur Verfügung stehen und deren Werte – Argumente genannt – vom Aufrufer bereitgestellt werden. So lässt sich eine Funktion leicht für unterschiedliche Zwecke aufrufen. Zum Abschluss kann eine Funktion einen einzelnen Wert an den Aufrufer zurückgeben.
Dieses Konzept ist Ihnen aus anderen Sprachen möglicherweise unter den Namen Subroutine, Prozedur oder Unterprogramm bekannt.
Zur Laufzeit erstellt JavaScript für jede Funktion ein Objekt. Man liest häufig, dass Funktionen in JavaScript "first-class objects" seien - damit ist gemeint, dass ein Funktionsobjekt ein Objekt wie alle anderen Objekte auch ist. Funktionen, die als Objekte dargestellt werden, gibt es in anderen Sprachen auch, aber diese Objekte sind häufig in ihrer Verwendbarkeit eingeschränkt.
Einem Funktionsobjekt in JavaScript kann man neue Eigenschaften geben, man kann es an Variablen zuweisen, an andere Funktionen als Argument übergeben oder als Rückgabewert einer weiteren Funktion erhalten.
Funktionsobjekte (nicht zu verwechseln mit dem Function-Objekt!) können auch Eigenschaft eines anderen Objektes sein, dann spricht man von Methoden. Die Besonderheiten von Methoden sind nicht Thema dieses Artikels.
Javascript stellt Ihnen im globalen Scope etliche Funktionen fertig zur Verfügung. Darüber hinaus können Sie beliebig eigene Funktionen erzeugen.
Inhaltsverzeichnis
Funktionsparameter und Wertrückgabe
Um eine Funktion mit Daten zu versorgen, kann sie man ihr beim Aufrufen Werte mitgeben. Diese Werte stehen dann innerhalb der Funktion als Parameter zur Verfügung. Der Begriff Parameter bezeichnet dabei die Variablen, mit denen JavaScript in der Funktion die übergebenen Werte bereitstellt. Die Werte selbst, die vom Aufrufer geliefert werden, nennt man Argumente.
Das Stück Programm, das sich innerhalb einer Funktion befindet, kann einen Wert berechnen und dem Aufrufer zur Verfügung stellen. Dies ist der Rückgabewert der Funktion.
Das Thema Parameter ist relativ umfangreich und benötigt noch eine Vertiefung. Aber zunächst soll es darum gehen, was Sie mit Funktionen grundsätzlich machen können und wie Sie sie selbst schreiben.
Vordefinierte globale Funktionen
Die vordefinierten Funktionen sind Teil des Sprachumfangs von JavaScript oder werden von der JavaScript-Laufzeitumgebung, also zum Beispiel der Browser, bereitgestellt. Sie müssen diese Funktionen nicht mehr selbst definieren und können sie jederzeit aufrufen.
Diese Funktionen sind technisch als Eigenschaften (d.h. Methoden) des globalen Objekts aufzufassen. In einem Script des angezeigten Dokuments können Sie sie deshalb als Methode von window
aufrufen (z. B. window.alert("Hallo Fenster");
) oder - sofern sie nicht durch lokal deklarierte Variablen oder Funktionen überdeckt werden, einfach als globale Funktion (z. B. alert("Hallo Welt");
).
Die ECMAScript-Spezifikation listet diese 9 globalen Funktionen auf[1].
-
decodeURI()
(kodierten URI dekodieren) -
decodeURIComponent()
(kodierten URI dekodieren – II) -
encodeURI()
(URI kodieren) -
encodeURIComponent()
(URI kodieren – II) -
eval()
(Ausdruck interpretieren) -
isFinite()
(auf numerischen Wertebereich prüfen) -
isNaN()
(auf numerischen Wert prüfen) -
parseFloat()
(in Kommazahl umwandeln) -
parseInt()
(in Ganzzahl umwandeln)
Funktionen aufrufen
Um eine Funktion aufzurufen, benötigen Sie das Objekt, das JavaScript für diese Funktion gebildet hat. Das klingt umständlicher, als es zumeist ist, denn zumeist steht Ihnen dieses Objekt über seinen Namen im Scope des Programmbereichs, in dem Sie die Funktion aufrufen möchten, direkt zur Verfügung. Es kann sich aber auch in einer Variablen oder in einer Objekteigenschaft (dann ist es eine Methode) befinden oder sogar der Rückgabewert einer anderen Funktion sein.
Hinter diesem Objekt notieren Sie eine linke Klammer (
, dann ein oder mehrere durch Komma getrennte Argumente und zum Abschluss eine rechte Klammer )
. Je nach Funktion sind die Argumente erforderlich oder optional. Übergeben Sie mehr Argumente als die Funktion erwartet, werden sie ignoriert.
Syntax:
funcObjekt ( argumente )
// Aufruf als globaler Name
let wert1 = parseFloat("17.4");
// Aufruf als Methode des globalen Objekts (in window)
let wert2 = window.parseFloat("8.15");
Die vordefinierte Funktion parseFloat wandelt eine Zeichenkette in eine Zahl um. Sie wird einmal über ihren globalen Namen aufgerufen, und einmal als Methode auf dem globalen Objekt window
. Das Ergebnis ist das gleiche. Solange Sie keine eigene parseFloat
-Funktion schreiben, die die globale parseFloat
verdecken würde, ist der Aufruf als Methode unnötig.
Bei einem Funktionsaufruf handelt es sich um einen Ausdruck. Da sich Ausdrücke überall verwenden lassen, wo auch Werte zulässig sind, können Sie das Argument für einen Funktionsaufruf auch durch den Aufruf einer anderen Funktion ermitteln lassen. Nehmen wir einmal an, wir hätten in der Variablen clipBoard
ein Objekt mit einer Methode readText()
, die auf irgendeine Weise den aktuellen Text in der Zwischenablage Ihres Computers ermittelt, und wir möchten diesen Text dann in eine Fließkommazahl umwandeln. Man könnte zunächst den Rückgabewert von readText()
in einer Variablen parken und dann an parseFloat()
übergeben. Aber es geht auch direkt:
let wert = parseFloat(clipBoard.readText());
Besondere Funktionen
Es gibt zwei Typen von Funktionen, die ein spezielles Verhalten aufweisen und auf die in eigenen Artikeln eingegangen wird:
Selbst definierte Funktionen erzeugen
Sie können Ihren eigenen Javascript-Code mit Hilfe von Funktionen gliedern. Nur der Code, der beim Laden der Seite sofort ausgeführt werden soll, muss außerhalb einer Funktion stehen und sollte dann nicht viel mehr tun, als globale Variablen zu initialisieren und den Rest der Arbeit an eine Funktion zu delegieren.
Es gibt unterschiedliche Möglichkeiten, in JavaScript eine Funktion zu definieren. Je nach Verfahren spricht man von einer benannten oder einer anonymen Funktion.
Funktionsdeklaration als Statement
Eine Funktionsdeklaration als Statement (function declaration statement) beginnt mit dem Schlüsselwort function
und dem Funktionsnamen, gefolgt von einem Paar runder Klammern und wird gefolgt von der Funktionsdefinition in Form eines Anweisungsblocks. In den Klammern können, durch Komma getrennt, Parameter für die Funktion spezifiziert werden. Ein Semikolon hinter dem Anweisungsblock ist nicht erforderlich.
Syntax:
function name([parameter1[, parameter2[, …]]]) { Anweisungen }
- Wenn Sie eine Funktion schreiben, die keine Parameter benötigt, sind die runden Klammern dennoch erforderlich.
- Wenn Sie eine Funktion schreiben, deren Anweisungsblock nur ein einziges Statement enthält, sind die geschweiften Klammern dennoch erforderlich.
Der Name einer mit dem function
Statement deklarierten Funktion verhält sich, als handelte es sich dabei um eine schreibgeschützte, mit var deklarierte Variable. Das bedeutet, dass dieser Name der Hebung (hoisting) unterliegt und die Funktion bereits vor ihrer Deklaration bekannt ist. Anders als bei Variablen wird aber auch die Definition der Funktion gehoben, sie kann also auch von Programmstellen aus aufgerufen werden, die vor ihrer Deklaration liegen.
Parameter dienen als Ablageort für Werte, die beim Aufruf der Funktion mitgegeben werden können. Innerhalb der Funktionsdefinition verhalten sie sich wie lokale Variablen der Funktion.
function celsiusInFahrenheit(celsius) {
return celsius * 1.8 + 32;
}
console.log("20 Grad Celsius sind " + celsiusInFahrenheit(20) + " Grad Fahrenheit");
Die Anweisung return
beendet die Funktionsausführung und setzt das Programm an der Stelle fort, von der aus die Funktion aufgerufen wurde. Der Wert des hinter return notierten Ausdrucks wird dem Aufrufer als Ergebnis des Funktionsaufrufs zur Verfügung gestellt.
return
und dem ersten Zeichen des Ausdrucks, der den Rückgabewert liefern soll, darf kein Zeilenumbruch verwendet werden.Bitte vermeiden Sie es, das Funktions-Statement in Anweisungsblöcken zu verwenden. Je nach Browserhersteller und -version und abhängig davon, ob Ihr Code im strengen Modus läuft, kann sich JavaScript dabei unterschiedlich verhalten.
let a = 9;
if (a > 4) { // Untergeordneter Anweisungsblock beginnt
function berechne(q) {
return q*4;
}
} else {
function berechne(q) {
return q*7;
}
}
console.log(berechne(5)); // Ergibt je nach Browser 20 oder 35
Wenn Sie unbedingt eine Funktion abhängig von einer bestimmten Bedingung definieren müssen, nutzen Sie Funktionsausdrücke. Dazu mehr im folgenden Abschnitt.
Funktionsausdruck
Ein Funktionsausdruck (function expression), auch Funktionsliteral genannt, ist fast genauso aufgebaut wie ein Funktionsstatement. Er erlaubt es, die Deklaration des Speicherortes für das Funktionsobjekt von der Definition - und damit vom Erzeugen des Funktionsobjekts - zu trennen. Ein Funktionsausdruck darf allerdings nicht am Beginn eines Statements stehen, sonst wird er von JavaScript mit einem Funktionsdeklarations-Statement verwechselt. Man muss ihn einer Variablen zuweisen, oder zumindest in Klammern setzen.
In einem Funktionsausdruck wird hinter function
normalerweise kein Name notiert. Das liegt daran, dass das Funktionsobjekt, das von diesem Ausdruck erzeugt wird, an sich namenlos ist. Über einen Namen erreichbar wird sie erst durch Zuweisung an eine Variable. Man spricht deshalb auch von anonymen Funktionen.
const fahrenheitInCelsius = function(fahrenheit) {
return (fahrenheit - 32) / 1.8;
};
Das Semikolon zum Abschluss der Zuweisung ist erforderlich. Funktionsausdrücke lassen sich in andere Ausdrücke hineinschachteln, deshalb kann JavaScript das Statement nicht einfach am Ende des Anweisungsblocks beenden.
Hinter function
einen Namen zu notieren ist nicht erforderlich, aber es ist erlaubt. Der so deklarierte Name gilt aber nur innerhalb dieser Funktion, nicht außerhalb. Man kann ihn für zwei Dinge nutzen:
- Die anonyme Funktion kann sich über diesen Namen selbst aufrufen (Rekursion)
- Die anomyme Funktion kann sich selbst als Argument an eine andere Funktion übergeben, ohne wissen zu müssen, in welcher Variablen sie gespeichert wurde, z.B. in einem requestAnimationFrame-Callback, der sich für den jeweils nächsten Animationsrahmen neu registrieren muss.
Da Funktionsausdrücke die speichernde Variable von der Funktion separieren, kann man mit ihnen auch Funktionen in Anweisungsblöcken erzeugen:
let berechne;
if (a > 4) {
berechne = function (q) { return q*4; }
} else {
berechne = function (q) { return q*7; }
}
Der wichtigste Unterschied zwischen der Statement- und Ausdruck-Form einer Funktionsdefinition ist, dass das Statement vom ECMAScript-Interpreter vollständig zum Beginn des Scopes gehoben wird, in dem es steht. Wird ein Funktionsausdruck an eine Variable zugewiesen, wird nur die Variablendeklaration gehoben. Das wirkt sich aus, wenn eine Funktion aufgerufen wird, bevor sie definiert wurde:
let machwas;
tuwas(3); // Funktioniert
machwas(5); // Error: undefined ist keine Funktion
function tuwas(q) {
return q*3;
}
machwas = function(q) {
return q*5;
}
machwas(17); // Hier funktioniert es.
Anonyme Funktionen als Callbacks
Das Funktionsobjekt, das von einem Funktionsausdruck erzeugt wird, muss nicht an eine Variable zugewiesen werden. Man kann es genauso gut direkt einer anderen Funktion als Argument übergeben, als Wert in ein Array legen oder es als Wert einer Funktion zurückgeben lassen. An dieser Stelle zeigt sich die Natur von Javascript als funktionale Programmiersprache: Funktionen sind ganz normale Objekte und können wie diese beliebig herumgereicht werden.
Anonyme Funktionen werden gerne genutzt, um Callbacks für Programmierschnittstellen wie setTimeout oder Handler für DOM Ereignisse bereitzustellen:
setTimeout(function() { alert("Zeit vorbei!"); }, 2000);
// Aus dem Blog von Todd Motto
document.querySelector('.menu').addEventListener('click', function (event) {
// Klick wurde ausgeführt
this.classList.toggle('active');
event.preventDefault();
});
Als nachteilig kann sich hier erweisen, dass man auf diese Weise viele kleine Codeschnipsel erhält, denen der Debugger bei der Fehlersuche keinen Namen mehr zuordnen kann. Landet man in einer tiefen Aufrufhierarchie an einem Unterbrechungspunkt (breakpoint) und findet im Aufrufstack 50% anonyme Funktionen, ist kaum erkennbar, auf welchem Weg der Code dorthin gelangt ist.
Hinzu kommt, dass eine benannte Funktion – bei guter Namenswahl – sich selbst dokumentieren kann, während ein anonymer Codeschnipsel automatisch erklärungsbedürftig ist. Z. B. bedarf der Click-Handler im letzten Beispiel einem Moment des Nachdenkens, bevor man versteht, was er tut. Darüber hinaus hat man so das Funktionsobjekt nicht zur Verfügung, was nötig wäre, um den Event-Listener wieder zu entfernen oder für andere Elemente mitzuverwenden. Das Folgende dagegen liest sich gleich besser, weil der Programmierer, der den addEventListener Aufruf liest, nicht die registrierte Funktion verstehen muss, sondern über ihren Namen sofort erfährt, was hier beabsichtigt ist.
// Aus dem Blog von Todd Motto
const toggleMenu = function(event) {
this.classList.toggle('active');
event.preventDefault();
};
document.querySelector('.menu').addEventListener('click', toggleMenu);
Lambda-Ausdruck (oder Pfeilfunktion)
Die Verwendung von Funktionen als Callbacks (siehe oben) kommt häufig vor. Es gibt mehrere Gründe, die für diesen Zweck das Übergeben einer vollwertigen Funktion unpraktisch machen. Der einfachste ist, dass solche Callbacks oft nur eine kompakte Berechnung mit ihren Parametern anstellen und der Schreibaufwand für den Rahmen einer anonymen Funktion größer ist als der Inhalt der Funktion. Ein weiterer Grund ist aber auch, dass der Aufruf einer Funktion einen neuen Wert für die Systemvariable this festlegt, nämlich null
im Strengen Modus oder das globale Objekt. Das ist unangenehm, wenn man eine Funktion mit Callback aus der Methode eines Objekts heraus aufruft. Innerhalb des Callbacks verweist this
dann nicht mehr auf das Objekt, zu dem die Methode gehörte.
Pfeilfunktionen dienen beiden Zwecken. Sie verkürzen den zu schreibenden Code, und sie binden this
nicht neu. Darüber hinaus unterscheiden sie sich noch in den folgenden Details von normalen Funktionen:
- Außer einem eigenen
this
gibt es auch keine eigenen Werte für super, arguments und new.target - Die Folgerung ist, dass man Pfeilfunktionen nicht als Methoden verwenden soll und nicht als Konstruktor verwenden kann.
- Die Helfer
call
,apply
undbind
von Function.prototype sind zwar für Pfeilfunktionen verwendbar, ein Wert für this kann aber nicht gesetzt werden. - Pfeilfunktionen als Generator sind nicht möglich, deshalb ist
yield
in einer Pfeilfunktion unzulässig.
Syntax 1:
parameter => ausdruck ([parameter1[, parameter2[, ...]]]) => ausdruck
=>
darf kein Zeilenumbruch stehen!let werte = [ 1, 2, 3 ]; // Ein Array mit den Einträgen 1, 2 und 3
let quadrate1 = werte.map(function(v) { return v*v;});
let quadrate2 = werte.map(v => v*v);
console.log(quadrate);
// Ausgabe: [ 1, 4, 9 ]
Das Beispiel zeigt, wie man die Quadrate aller Elemente eines Arrays mit Hilfe der Array.map-Methode bestimmen kann. In der ersten Variante wird als Vergleich eine anonyme Funktion in klassischer Schreibweise verwendet, die zweite Variante zeigt die verkürzte Version mit Hilfe einer Pfeilfunktion. Beide Varianten tun das gleiche.
Die oben gezeigte Schreibweise von Pfeilfunktionen kommt zum Einsatz, wenn eine anonyme Funktion benötigt wird, die nur aus einem return
-Statement besteht. Es gibt auch eine erweiterte Syntax mit geschweiften Klammern, die auch umfangreiche Funktionskörper zulässt:
Syntax 2:
parameter => anweisungsblock ([parameter1[, parameter2[, ...]]]) => anweisungsblock
Das folgende Beispiel zeigt einen click-Eventhandler, der beim Anklicken eines Buttons sein aria-expanded-Attribut zwischen true und false umschaltet.
document.documentElement.addEventListener('click', event => {
let button = event.target;
if (button.tagName == 'BUTTON' && button.hasAttribute('aria-expanded'))
{
button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') != 'true');
}
});
Die verwendete Syntax hat keinen Einfluss darauf, ob ein Funktions-Scope erzeugt wird. In beiden Fällen werden this
und auch arguments
nicht neu gebunden, sie verwenden die Werte der Funktion, in der die Pfeilfunktion definiert wurde.
Durch die Verwendung eines Anweisungsblocks entsteht aber ein Block-Scope, so dass Sie lokale Variablen deklarieren können.
class ArrayMath {
constructor(a) {
this.offset = a;
}
addToArray(arr) {
// this bezieht sich im Callback immer noch auf das this von addToArray
return arr.map(val => val + this.offset);
}
}
let mathWorker = new ArrayMath(7);
let meinArray = [ 1, 3, 5 ];
let newArray = mathWorker.addToArray(meinArray);
console.log(newArray); // Ausgabe: 8, 10, 12
Dieses – zugegebenermaßen recht praxisfremde – Beispiel erzeugt eine Klasse ArrayMath
mit der Methode addToArray
. In dieser Methode wird auf jeden Wert in einem übergebenen Array die Zahl aufaddiert, die vom Konstruktor in this.offset
abgelegt wurde. Hier spielt die Pfeilfunktion ihre Stärke aus: man kann dort einfach this.offset
verwenden und greift dabei auf die offset
-Eigenschaft des Objekts zu, in dessen Methode die Pfeilfunktion definiert wurde. Ohne Pfeilfunktionen würde man eine temporäre Variable benötigen, in der man this
zwischenspeichert. Solche Konstrukte findet man häufig, diese temporäre Variable wird gerne that
oder self
. Pfeilfunktionen machen sie überflüssig.
Funktionskonstruktor
Eine eher selten verwendete Vorgehensweise definiert Funktionen über den Funktions-Konstruktor:
let someFunc = new Function(['Parameter1' [, 'Parameter2' [, …]]],
'Anweisungsblock');
eval
gilt auch hier: nur verwenden, wenn es gar nicht anders geht. Berücksichtigen Sie vor allem den möglichen Schaden, der entsteht, wenn auf diese Weise ein potenziell bösartiges JavaScript-Programm von einem Nutzer Ihrer Webseite anderen Benutzern zugänglich gemacht werden kann.
Besonderheiten bei Parametern
Parameterbindung
Wie schon im eingangs verlinkten Artikel über Parameter beschrieben, gibt es unterschiedliche Techniken, wie eine Programmiersprache Argumente und Parameter miteinander verbindet.
In JavaScript ist es so, dass grundsätzlich ein Call-By-Value durchgeführt wird. Das heißt: ganz gleich, welche Werte eine Funktion ihren Parameter-Variablen zuweist, es hat auf die Argumente des Aufrufers keinen Effekt. Die Funktion arbeitet immer nur mit Kopien der Argumente.
Hierbei ist aber zu beachten, was die Argumente eigentlich sind. JavaScript unterscheidet zwischen primitiven Werten und Objekten.
- Primitive Werte sind solche, die eine Einheit bilden und keine veränderbaren Eigenschaften haben. Sie können beispielsweise nicht bei einer Zahl die dritte Stelle vor dem Komma durch eine andere ersetzen. Sie können in einer Zeichenkette keine Zeichen austauschen. Sie können einem String keine neuen Eigenschaften geben. Es gibt natürlich Wege, solche Dinge zu bewerkstelligen, aber dann haben Sie entweder den primitiven Wert in ein Objekt verpackt (Wrapper-Objekt), oder Sie haben einen neuen Wert, und der alte Wert ist unverändert geblieben.
- Objekte sind hingegen Container mit Eigenschaften, die Sie einzeln ändern können. In diesem Sinne zählen auch die Arrays zu den Objekten. Diese Container sind eigenständige Speichereinheiten und werden nicht in Variablen gespeichert. Wenn Sie einer Variablen ein Objekt zuweisen, dann wird in Wahrheit nur gespeichert, wo das Objekt im Speicher zu finden ist. Übergeben Sie ein Array oder Objekt an eine Funktion, wird der Funktion nur mitgeteilt, wo sich der betreffende Speicherbereich befindet.
Das bedeutet: Wenn Sie einer Funktion ein Array oder ein Objekt übergeben, dann kann die Funktion das Array oder das Objekt selbst nicht ersetzen, es hat aber Zugriff auf dessen Speicherbereich und kann Eigenschaften darin verändern, hinzufügen oder löschen. Sie müssen Arrays und Objekte bei der Übergabe an eine Funktion so behandeln, als würden sie mittels Call-By-Reference übergeben.
Das C++ Feature, bei der Deklaration einer Referenz mitzuteilen, dass das referenzierte Datenelement nicht verändert werden darf (Zeiger auf const
), gibt es in JavaScript nicht.
Optionale Parameter
Viele Funktionen sind so geschrieben, dass sie nicht alle Parameter benötigen, die man mitgeben kann. Statt dessen verwenden sie für den weggelassenen Parameter einen Ersatzwert. Da dieser Ersatzwert dafür sorgt, dass es nicht zu einem Programmfehler (fault) kommt, nennt man solche Ersatzwerte auch einen Defaultwert oder kurz Default.
Es gibt unterschiedliche Möglichkeiten, für einen Parameter einen Defaultwert vorzusehen. Zum einen können Sie das arguments
-Array verwenden (wird im Anschluss genauer erklärt). Mit seiner length
-Eigenschaft können Sie prüfen, wie viele Parameter übergeben wurden und wenn einer fehlt, setzen Sie einen Ersatzwert ein. Eine weitere Möglichkeit ist, den Inhalt der Parameter abzufragen. Enthalten sie den Wert undefined
, dann wurde vermutlich kein Wert übergeben. Es könnte aber auch sein, dass der Aufrufer tatsächlich undefined
übergeben hat.
function paramTest(a, b) {
console.log(arguments.length, '/', arguments);
console.log('a =', a, 'und b =', b);
}
paramTest(7);
// 1 / Arguments [7, callee: ...]
// a = 7 und b = undefined
paramTest(7, undefined);
// 2 / Arguments [7, undefined, callee: ...]
// a = 7 und b = undefined
Die Funktion paramTest gibt den Inhalt des arguments
-Arrays aus sowie die Inhalte der Parametervariablen. Wenn Sie die Ausgaben der beiden Aufrufe vergleichen, dann sehen Sie, dass beim ersten Aufruf arguments.length
den Wert 1 hat, also für b
nichts übergeben wurde. Beim zweiten Aufruf wurde undefined
übergeben, deswegen ist arguments.length
gleich 2, aber der Variablen b
können Sie das nicht ansehen.
Sie können nun abfragen, ob arguments.length
kleiner als 2 ist oder ob b
den Wert undefined
enthält. Welche Variante Sie wählen, hängt davon ab, ob undefined
ein zulässiger Wert für b
ist.
function paramTest(a, b) {
if (arguments.length < 2)
b = 8;
console.log('a =', a, 'und b =', b);
}
paramTest(7);
// a = 7 und b = 8
Um das Bereitstellen von Defaultwerten zu erleichtern, besitzt JavaScript eine eigene Syntax. Sie fügen an den Parameter einfach ein =
und den Wert an, der als Default verwendet werden soll.
function paramTest(a = 7, b = 9) {
console.log('a =', a, 'und b =', b);
}
paramTest();
// a = 7 und b = 9
paramTest(2);
// a = 2 und b = 9
paramTest(2, 5);
// a = 2 und b = 5
paramTest(undefined, 1); // Achtung!
// a = 7 und b = 1
Tatsächlich ist es so, dass JavaScript abfragt, ob ein Parameter zu Beginn der Funktion den Wert undefined
hat. Wenn ja, wird der notierte Defaultwert zugewiesen.
Variadische Funktionen
Außer Funktionen mit optionalen Parametern gibt es auch solche, deren Parameteranzahl nicht festgelegt ist und die eine variable Anzahl von Parametern verarbeiten können. Solche Funktionen heißen variadische Funktionen. Eine solche Funktion kann - muss aber nicht - einige feste Parametern haben. Im Anschluss an die festen Parameter kann der Aufrufer eine beliebige Menge weiterer Parameter übergeben.
Sie haben zwei Möglichkeiten, eine solche Parametermenge zu verarbeiten: das arguments
-Array und einen Restparameter.
arguments
arguments
ist innerhalb von Funktionen - nicht in Pfeilfunktionen! - eine Systemvariable, die sich ähnlich wie ein Array verhält und alle Argumente enthält, die der Funktion übergeben wurden.
Sie hat eine Eigenschaft length
, der die Anzahl der übergebenen Argumente entnommen werden kann. Zugriff auf die Werte der einzelnen Argumente erhält man über arguments[0]
bis arguments[arguments.length - 1]
, dies sind die vom Aufrufer übergebenen Werte in genau dieser Reihenfolge.
arguments
hat gewisse Eigenschaften eines Arrays, aber es ist kein Array. Sie können die length
-Eigenschaft nutzen und mit den eckigen Klammern einen Elementzugriff durchführen. Es gibt sogar einen Iterator, so dass Sie die for...of-Schleife nutzen können. Die Methoden, die zu Array.prototype
gehören, finden Sie in einem arguments
Objekt aber nicht. Da die meisten Methoden von Array.prototype
gutmütig sind und auf arrayähnlichen Objekten genauso wie auf Arrays funktionieren, können Sie sich mittels .call()
bzw. .apply()
behelfen, wie das nachfolgende Beispiel zeigt.
// addiereXZu verwendet die Arraymethode splice, um die Parameter ab Position 2 in
// das Array 'summanden' zu übertragen
function addiereXZu(summand) {
// Variadische Parameter (ab Position 2) in eigenes Array übertragen.
let werte = Array.prototype.splice.call(arguments, 1);
// Auf jeden dieser Werte den ersten Parameter aufaddieren und Ergebnisarray zurückgeben
return werte.map(wert => summand + wert);
}
console.log(addiereXZu(5, 2, 3, 4)); // Gibt das Array [7, 8, 9] aus.
addiereXZu
addiert den ersten Parameter zu allen folgenden Parametern und
gibt die Summen als neues Array zurück. Die Funktion übernimmt die splice-Methode aus Array.prototype, um die Einträge ab Position 2 (Index 1) von arguments
in ein neues Array zu übertragen. Aus diesem Array wird dann mittels der map()-Methode von Arrays das Array mit den Summen erzeugt.
arguments
-Array mit den Parametervariablen. Wenn Sie in der paramTest
-Funktion aus dem Abschnitt über optionale Parameter an a
einen Wert zuweisen, ändert sich arguments[0]
mit. Wenn Sie an arguments[1]
einen Wert zuweisen, ändert sich b
mit.Achtung!
arguments
, bei der arguments wie eine Eigenschaft der Funktion verwendet wird (beispielsweise könnte man innerhalb einer Funktion namens addiere addiere.arguments
verwenden. Es ist damit möglich, bei geschachtelten Funktionen auf das arguments-Array äußerer Funktionen zuzugreifen. Zum einen ist dieses Feature im strengen Modus nicht verfügbar, zum anderen war und ist es kein Teil eines Webstandards. Sie sollten es daher nicht verwenden. Wenn Sie unbedingt in einer geschachtelten Funktion auf variadische Parameter der äußeren Funktion zugreifen müssen, verwenden Sie besser Restparameter oder weisen in der äußeren Funktion arguments
an eine Variable zu. arguments.callee
, die das Funktionsobjekt liefert, zu dem arguments gehört. Sie wurde genutzt, um auf das Funktionsobjekt anonymer Funktionen zugreifen zu können (beispielsweise für rekursive Aufrufe). Diese Eigenschaft ist im strengen Modus nicht verfügbar, und ihre Verwendung war schon in ECMAScript 3 (1999) nicht mehr erforderlich, weil seitdem auch Funktionen in Funktionsausdrücken benannt werden können und über ihren Namen ansprechbar sind.Restparameter
Der Sprachstandard ECMAScript 2015 führt den Rest-Operator ein, der eine Werteliste zu einem Array zusammenfassen kann. Damit können Sie angeben, dass alle Parameter einer bestimmten Position als ein Array bereitgestellt werden. Die Funktion addiereXZu
aus dem Abschnitt über arguments
lässt sich damit so schreiben:
function addiereXZu(summand, ...werte) {
// Auf jeden der Werte den ersten Parameter aufaddieren und Ergebnisarray zurückgeben
return werte.map(wert => summand + wert);
}
console.log(addiereXZu(5, 2, 3, 4)); // Gibt das Array [7, 8, 9] aus.
Der Restoperator steht an der zweiten Parameterposition. Dadurch werden ab dem zweiten übergebenen Argument alle Werte zu einem Array zusammengefasst und stehen in werte
zur Verfügung.
Der Vorteil des Restparameters ist, dass es sich hier um ein echtes Array handelt, und dass Sie keine magische Zahl in Ihrem Code haben, die die Anzahl der nichtvariadischen Parameter angibt.
Scopes und Funktionen
Der Begriff Scope bezeichnet den Kontext, in dem eine JavaScript-Anweisung ausgeführt wird. Jede Variable oder Funktion in JavaScript ist einem solchen Kontext zugeordnet.
Grundsätzlich hat jede JavaScript-Ausführungsumgebung einen globalen Scope. In einem HTML Dokument wird er beim Laden des Dokuments erzeugt und bleibt vorhanden, bis der Browser ein anderes Dokument lädt. Durch das Starten von Workern können weitere, unabhängige globale Scopes erzeugt werden, die so lange existieren, wie es den Worker gibt.
Alle Variablen, die außerhalb einer Funktion definiert wurden, sind dem globalen Scope zugeordnet. Funktionen - sofern sie nicht in andere Funktionen geschachtelt sind - befinden sich ebenfalls im globalen Scope.
Definiert man eine Funktion, gilt innerhalb dieser Funktion ein neuer Scope. Die Parameter der Funktion und alle Variablen, die innerhalb der Funktion definiert werden sind, nur in diesem Scope sichtbar. Der Scope, in dem die Funktion definiert wurde, gilt diesem neuen Scope als übergeordnet.
In einem Scope können beliebige neue Variablen angelegt werden, auch solche, deren Name in einem übergeordneten Scope bereits genutzt wurde. Sie werden dann von der Variablen im untergeordneten Scope verdeckt. Wird eine Variable genutzt, die im inneren Scope nicht definiert wurde, wird auf die Variable im äußeren Scope zugegriffen.
let bar = 1;
let baz = 17;
function foo() {
let baz = 2; // Erzeugt eine neues baz im inneren Scope!
let zap = 9; // Noch eine neue Variable
return bar + baz + zap; // Rechnet 1 + 2 + 9
}
foo(); // Gibt 12 zurück
let xy = bar + zap; // Erzeugt einen Ausführungsfehler, da zap nicht definiert wurde.
Es ist auch möglich, Funktionen innerhalb anderer Funktionen zu definieren. Eine solche innere Funktion ist dann nicht Teil des globalen Scopes, sondern Teil des Scopes der Funktion, in der sie definiert wird.
Auch innere Funktionen bekommen ihren eigenen Scope, der dem Scope der äußeren Funktion untergeordnet wird. Deswegen kann eine innere Funktion auf ihren eigenen Scope, auf den Scope der äußeren Funktion und auf den globalen Scope zugreifen. Natürlich kann man diese Schachtelung beliebig vertiefen, es entstehen dann entsprechend lange Scope-Ketten.
function berechne(c) { // Äußere Funktion, c liegt im Scope von berechne
function helper(a, b) { // Innere Funktion, kann auf c zugreifen
return a*(b+c); // Die Parameter a und b liegen im Scope von helper
}
let ergebnis = helper(2, 7); // Erzeugt eine neue Variable im Scope von berechne
return ergebnis;
};
berechne(17); // Gibt 48 zurück
helper(1,2); // Fehler, helper ist hier nicht definiert
Außer dem globalen Scope und dem Funktionsscope gibt es in JavaScript noch zwei weitere Scope-Typen:
- Wenn Sie ein ECMAScript-Modul erstellen, so wird für die in diesem Modul auf oberster Ebene definierten Funktionen und Variablen ein eigener Modul-Scope gebildet. Der globale Scope ist Elternscope des Modul-Scopes.
- Sofern Sie innerhalb eines Anweisungsblocks Variablen mit
let
oderconst
definieren, gelten diese Variablen nur im Block-Scope dieses Anweisungsblocks.
Lebensdauer von Scopes
Die Lebensdauer des globalen Scopes wurde bereits beschrieben - er existiert so lange wie das Dokument angezeigt wird oder so lange, wie ein Worker läuft.
Der Scope einer Funktion wird erzeugt, sobald die Funktion aufgerufen wird. Das geschieht bei jedem Aufruf einer Funktion von neuem, der Scope des zweiten Aufrufs einer Funktion weiß nichts über den Scope des ersten Aufrufs. Und im Normalfall ist es so, dass der Scope einer Funktion nicht mehr benötigt wird, wenn die Funktion endet, und gelöscht wird. Die darin befindlichen Variablen werden ebenfalls gelöscht. Soweit diese Variablen Objekte enthielten, verlieren diese Objekte damit einen Bezugspunkt, der sie am Leben erhält, und wenn das der letzte Bezugspunkt war, werden sie bei nächster Gelegenheit aus dem Arbeitsspeicher entfernt (garbage collection).
Die Normalität gerät an ihre Grenze, wenn innere Funktionen verwendet werden. Wie beschrieben, sind Funktionen echte Objekte, und eine innere Funktion kennt den Scope ihrer äußeren Funktion. Oder anders gesagt: Das Objekt "innere Funktion" verweist auf das von JavaScript intern gebildete Objekt "Scope der äußeren Funktion". Das ist wenig bedeutsam, solange das Objekt "innere Funktion" nur im Objekt "Scope der äußeren Funktion" gespeichert ist. Die beiden verweisen lediglich aufeinander, und sobald die äußere Funktion endet, verliert der äußere Scope seinen Bezugspunkt und wird gelöscht.
Das ändert sich, wenn das Objekt "innere Funktion" an einer Stelle gespeichert wird, die nicht zum Scope der äußeren Funktion gehört. So etwas kann durch die unterschiedlichsten Dinge ausgelöst werden:
- Registrieren der inneren Funktion als Eventhandler
- Speichern der inneren Funktion in einer Variablen, die zum Elternscope der äußeren Funktion gehört
- Zurückgeben der inneren Funktion an den Aufrufer der äußeren Funktion
Wenn jetzt die äußere Funktion endet, besitzt die innere Funktion noch einen weiteren, eigenen Bezugspunkt im Arbeitsspeicher. Und weil sie auf den Scope der äußeren Funktion verweist, hängt er mit an diesem Bezugspunkt. Damit leben die innere Funktion und der Scope der äußeren Funktion solange weiter, wie dieser Bezugspunkt existiert. Auf diese Weise wurde ein abgeschlossener, privater Speicherbereich geschaffen, eine Closure. Dieser Speicherbereich kann von der inneren Funktion genutzt werden. Wurden im Scope der äußeren Funktion noch weitere Funktionen definiert, gehören sie ebenfalls zur Closure und können ebenfalls genutzt werden.
function erzeugeAddierer(konstante) {
function addiereWert(a) {
return a + konstante;
}
return addiereWert;
};
let add2 = erzeugeAddierer(2),
add7 = erzeugeAddierer(7);
console.log(add2(7)); // gibt 9 zurück!
console.log(add7(9)); // gibt 16 zurück!
Die Funktion erzeugeAddierer
definiert eine innere Funktion addiereWert
. Diese innere Funktion greift auf den Parameter konstante
von erzeugeAddierer
zu. Danach wird addiereWert
als Wert zurückgegeben und vom Aufrufer in einer Variablen gespeichert.
Im Beispiel geschieht das gleich zweimal, mit unterschiedlichen Argumenten. Wie beschrieben, wird bei jedem Aufruf von erzeugeAddierer
ein neuer Scope für diese Funktion gebildet. Für die innere Funktion addiereWert
wird demnach jedesmal ein neues Funktionsobjekt erzeugt, das einen Verweis auf seinen Besitzerscope enthält - die Closure. Dadurch bleibt der unterschiedliche Wert des Parameters konstante
der erzeugeAddierer
-Funktion für die beiden addiereWert
-Funktionsobjekte erhalten und kann genutzt werden, sobald eines dieser Funktionsobjekte für einen Funktionsaufruf genutzt wird. So erklärt sich, weshalb add2
den Wert 2 addiert und add7
den Wert 7.
Zu beachten ist auch, dass der Aufrufer von erzeugeAddierer
nichts darüber wissen muss, welcher Name das Funktionsobjekt, das er zurückbekommt, innerhalb von erzeugeAddierer
gehabt hat. Der Aufrufer sieht ein anonymes Funktionsobjekt, das er in einer Variablen seiner Wahl speichern kann. Danach kann diese Variable wie eine Funktion benutzt werden.
Solange die Variablen add2
und add7
ihren Wert behalten, leben die beiden Funktionsobjekte und ihre zugeordnete Closure fort.
Es sei davor gewarnt, dies zu ausgiebig zu nutzen. Scopes belegen Speicher, die Variablen eines Scopes belegen Speicher und Objekte, die in diesen Variablen gespeichert sind, belegen ebenfalls Speicher. Sie können auf diese Weise recht schnell den Arbeitsspeicherbedarf einer Webseite in die Höhe treiben.
Weitere Feinheiten zu Scopes in JavaScript finden Sie im Tutorial zum Umgang mit Callback-Funktionen und im Tutorial zu Namensräumen.
Hauptartikel: JavaScript/Scope umfangreicher Hintergrundartikel zum tieferen Verständnis
Siehe auch
- Rekursive Funktionen
- Closures
- JavaScript/Tutorials/Namensraum (Anwendung von IIFE)
Quellen
- ↑ ECMA 262: Funktionen des globalen Objekts