Wie wir bereits wissen, ist eine Funktion in JavaScript ein Wert.
Jeder Wert in JavaScript hat einen Typ. Welcher Typ ist eine Funktion?
In JavaScript sind Funktionen Objekte.
Eine gute Möglichkeit, sich Funktionen vorzustellen, sind aufrufbare „Aktionsobjekte“. Wir können sie nicht nur aufrufen, sondern auch als Objekte behandeln: Eigenschaften hinzufügen/entfernen, als Referenz übergeben usw.
Funktionsobjekte enthalten einige verwendbare Eigenschaften.
Auf den Namen einer Funktion kann beispielsweise über die Eigenschaft „name“ zugegriffen werden:
Funktion sayHi() { alarm("Hallo"); } alarm(sayHi.name); // sag Hallo
Das Witzige daran ist, dass die Namensvergabelogik intelligent ist. Es weist einer Funktion auch dann den richtigen Namen zu, wenn sie ohne erstellt und dann sofort zugewiesen wird:
let sayHi = function() { alarm("Hallo"); }; alarm(sayHi.name); // sayHi (da ist ein Name!)
Es funktioniert auch, wenn die Zuweisung über einen Standardwert erfolgt:
Funktion f(sayHi = function() {}) { alarm(sayHi.name); // sayHi (funktioniert!) } F();
In der Spezifikation wird diese Funktion als „Kontextname“ bezeichnet. Wenn die Funktion keines bereitstellt, wird es in einer Zuweisung aus dem Kontext herausgefunden.
Objektmethoden haben auch Namen:
let user = { sayHi() { // ... }, sayBye: function() { // ... } } alarm(user.sayHi.name); // sag Hallo alarm(user.sayBye.name); // sag Tschüss
Es gibt jedoch keine Magie. Es gibt Fälle, in denen es keine Möglichkeit gibt, den richtigen Namen herauszufinden. In diesem Fall ist die Namenseigenschaft leer, wie hier:
// Funktion im Array erstellt let arr = [function() {}]; alarm( arr[0].name ); // <leerer String> // Die Engine hat keine Möglichkeit, den richtigen Namen einzurichten, also gibt es keinen
In der Praxis haben die meisten Funktionen jedoch einen Namen.
Es gibt eine weitere integrierte Eigenschaft „length“, die die Anzahl der Funktionsparameter zurückgibt, zum Beispiel:
Funktion f1(a) {} Funktion f2(a, b) {} Funktion Many(a, B, ...more) {} alarm(f1.length); // 1 alarm(f2.length); // 2 alarm(many.length); // 2
Hier sehen wir, dass Restparameter nicht gezählt werden.
Die length
wird manchmal zur Selbstbeobachtung in Funktionen verwendet, die mit anderen Funktionen arbeiten.
Im folgenden Code akzeptiert die ask
-Funktion beispielsweise das Stellen einer question
und den Aufruf einer beliebigen Anzahl von handler
.
Sobald ein Benutzer seine Antwort bereitstellt, ruft die Funktion die Handler auf. Wir können zwei Arten von Handlern übergeben:
Eine Funktion ohne Argumente, die nur aufgerufen wird, wenn der Benutzer eine positive Antwort gibt.
Eine Funktion mit Argumenten, die in beiden Fällen aufgerufen wird und eine Antwort zurückgibt.
Um handler
richtig aufzurufen, untersuchen wir die Eigenschaft handler.length
.
Die Idee ist, dass wir eine einfache Handler-Syntax ohne Argumente für positive Fälle haben (häufigste Variante), aber auch universelle Handler unterstützen können:
Funktion ask(question, ...handlers) { let isYes = bestätigen(Frage); for(let handler of handlers) { if (handler.length == 0) { if (isYes) handler(); } anders { handler(isYes); } } } // bei positiver Antwort werden beide Handler aufgerufen // für eine negative Antwort, nur die zweite ask("Frage?", () => alarm('Du hast ja gesagt'), result => warning(result));
Dies ist ein besonderer Fall des sogenannten Polymorphismus – die Behandlung von Argumenten je nach Typ oder, in unserem Fall, je nach length
unterschiedlich. Die Idee kann in JavaScript-Bibliotheken verwendet werden.
Wir können auch eigene Eigenschaften hinzufügen.
Hier fügen wir die counter
hinzu, um die Gesamtanzahl der Anrufe zu verfolgen:
Funktion sayHi() { alarm("Hallo"); // Zählen wir, wie oft wir laufen sayHi.counter++; } sayHi.counter = 0; // Anfangswert sayHi(); // Hallo sayHi(); // Hallo alarm( `${sayHi.counter} mal aufgerufen` ); // 2 Mal angerufen
Eine Eigenschaft ist keine Variable
Eine einer Funktion zugewiesene Eigenschaft wie sayHi.counter = 0
definiert keinen darin enthaltenen lokalen counter
. Mit anderen Worten, ein counter
und ein Variablen let counter
sind zwei Dinge, die nichts miteinander zu tun haben.
Wir können eine Funktion als Objekt behandeln und Eigenschaften darin speichern, aber das hat keinen Einfluss auf ihre Ausführung. Variablen sind keine Funktionseigenschaften und umgekehrt. Das sind nur Parallelwelten.
Funktionseigenschaften können manchmal Abschlüsse ersetzen. Beispielsweise können wir das Beispiel einer Zählerfunktion aus dem Kapitel „Variablenumfang, Abschluss“ umschreiben, um eine Funktionseigenschaft zu verwenden:
Funktion makeCounter() { // anstatt: // let count = 0 Funktion counter() { return counter.count++; }; counter.count = 0; Rückgabezähler; } let counter = makeCounter(); alarm( counter() ); // 0 alarm( counter() ); // 1
Die count
wird jetzt direkt in der Funktion gespeichert, nicht in ihrer äußeren lexikalischen Umgebung.
Ist es besser oder schlechter als die Verwendung eines Verschlusses?
Der Hauptunterschied besteht darin, dass externer Code nicht darauf zugreifen kann, wenn sich der Wert von count
in einer äußeren Variablen befindet. Nur verschachtelte Funktionen dürfen es ändern. Und wenn es an eine Funktion gebunden ist, dann ist so etwas möglich:
Funktion makeCounter() { Funktion counter() { return counter.count++; }; counter.count = 0; Rückgabezähler; } let counter = makeCounter(); counter.count = 10; alarm( counter() ); // 10
Die Wahl der Umsetzung hängt also von unseren Zielen ab.
Named Function Expression oder NFE ist ein Begriff für Funktionsausdrücke, die einen Namen haben.
Nehmen wir zum Beispiel einen gewöhnlichen Funktionsausdruck:
let sayHi = function(who) { Alert(`Hallo, ${who}`); };
Und fügen Sie einen Namen hinzu:
let sayHi = function func(who) { Alert(`Hallo, ${who}`); };
Haben wir hier etwas erreicht? Was ist der Zweck dieses zusätzlichen "func"
-Namens?
Beachten wir zunächst, dass wir noch einen Funktionsausdruck haben. Das Hinzufügen des Namens "func"
nach function
machte es nicht zu einer Funktionsdeklaration, da es immer noch als Teil eines Zuweisungsausdrucks erstellt wird.
Das Hinzufügen eines solchen Namens hat auch nichts kaputt gemacht.
Die Funktion ist weiterhin als sayHi()
verfügbar:
let sayHi = function func(who) { alarm(`Hallo, ${who}`); }; sayHi("John"); // Hallo, John
Der Name func
hat zwei Besonderheiten, die dafür verantwortlich sind:
Dadurch kann die Funktion intern auf sich selbst verweisen.
Es ist außerhalb der Funktion nicht sichtbar.
Beispielsweise ruft sich die folgende Funktion sayHi
erneut mit "Guest"
auf, wenn kein „ who
“ angegeben ist:
let sayHi = function func(who) { wenn (wer) { alarm(`Hallo, ${who}`); } anders { func("Gast"); // func verwenden, um sich selbst erneut aufzurufen } }; sayHi(); // Hallo, Gast // Aber das wird nicht funktionieren: func(); // Fehler, Funktion ist nicht definiert (außerhalb der Funktion nicht sichtbar)
Warum verwenden wir func
? Vielleicht einfach sayHi
für den verschachtelten Aufruf verwenden?
Tatsächlich können wir in den meisten Fällen:
let sayHi = function(who) { wenn (wer) { Alert(`Hallo, ${who}`); } anders { sayHi("Gast"); } };
Das Problem mit diesem Code besteht darin, dass sich sayHi
im äußeren Code ändern kann. Wenn die Funktion stattdessen einer anderen Variablen zugewiesen wird, beginnt der Code mit der Ausgabe von Fehlern:
let sayHi = function(who) { wenn (wer) { Alert(`Hallo, ${who}`); } anders { sayHi("Gast"); // Fehler: sayHi ist keine Funktion } }; let Welcome = sayHi; sayHi = null; Willkommen(); // Fehler, der verschachtelte sayHi-Aufruf funktioniert nicht mehr!
Das liegt daran, dass die Funktion sayHi
aus ihrer äußeren lexikalischen Umgebung übernimmt. Da es kein lokales sayHi
gibt, wird die äußere Variable verwendet. Und im Moment des Anrufs ist das äußere sayHi
null
.
Der optionale Name, den wir in den Funktionsausdruck einfügen können, soll genau solche Probleme lösen.
Verwenden wir es, um unseren Code zu reparieren:
let sayHi = function func(who) { wenn (wer) { Alert(`Hallo, ${who}`); } anders { func("Gast"); // Jetzt ist alles gut } }; let Welcome = sayHi; sayHi = null; Willkommen(); // Hallo, Gast (verschachtelter Aufruf funktioniert)
Jetzt funktioniert es, denn der Name "func"
ist funktionslokal. Es ist nicht von außen aufgenommen (und dort nicht sichtbar). Die Spezifikation garantiert, dass immer auf die aktuelle Funktion verwiesen wird.
Der äußere Code hat immer noch seine Variable sayHi
oder welcome
. Und func
ist ein „interner Funktionsname“, mit dem sich die Funktion zuverlässig selbst aufrufen kann.
Für die Funktionsdeklaration gibt es so etwas nicht
Die hier beschriebene Funktion „interner Name“ ist nur für Funktionsausdrücke verfügbar, nicht für Funktionsdeklarationen. Für Funktionsdeklarationen gibt es keine Syntax zum Hinzufügen eines „internen“ Namens.
Manchmal, wenn wir einen zuverlässigen internen Namen benötigen, ist dies der Grund, eine Funktionsdeklaration in die Form eines benannten Funktionsausdrucks umzuschreiben.
Funktionen sind Objekte.
Hier haben wir ihre Eigenschaften behandelt:
name
– der Funktionsname. Wird normalerweise aus der Funktionsdefinition entnommen, aber wenn keine vorhanden ist, versucht JavaScript, sie aus dem Kontext (z. B. einer Zuweisung) zu erraten.
length
– die Anzahl der Argumente in der Funktionsdefinition. Restparameter werden nicht gezählt.
Wenn die Funktion als Funktionsausdruck deklariert ist (nicht im Hauptcodefluss) und den Namen trägt, wird sie als benannter Funktionsausdruck bezeichnet. Der Name kann innerhalb verwendet werden, um sich selbst zu referenzieren, für rekursive Aufrufe oder ähnliches.
Außerdem können Funktionen zusätzliche Eigenschaften enthalten. Viele bekannte JavaScript-Bibliotheken nutzen diese Funktion in großem Umfang.
Sie erstellen eine „Haupt“-Funktion und fügen ihr viele weitere „Hilfs“-Funktionen hinzu. Beispielsweise erstellt die jQuery-Bibliothek eine Funktion namens $
. Die lodash-Bibliothek erstellt eine Funktion _
und fügt ihr dann _.clone
, _.keyBy
und andere Eigenschaften hinzu (sehen Sie sich die Dokumente an, wenn Sie mehr darüber erfahren möchten). Tatsächlich tun sie dies, um die Verschmutzung des globalen Raums zu verringern, sodass eine einzelne Bibliothek nur eine globale Variable bereitstellt. Dadurch verringert sich die Möglichkeit von Namenskonflikten.
Eine Funktion kann also selbst eine nützliche Aufgabe erfüllen und auch eine Reihe anderer Funktionen in Eigenschaften enthalten.
Wichtigkeit: 5
Ändern Sie den Code von makeCounter()
so, dass der Zähler auch die Zahl verringern und festlegen kann:
counter()
sollte die nächste Zahl zurückgeben (wie zuvor).
counter.set(value)
sollte den Zähler auf value
setzen.
counter.decrease()
sollte den Zähler um 1 verringern.
Das vollständige Anwendungsbeispiel finden Sie im Sandbox-Code.
PS: Sie können entweder einen Abschluss oder die Funktionseigenschaft verwenden, um die aktuelle Anzahl beizubehalten. Oder schreiben Sie beide Varianten.
Öffnen Sie eine Sandbox mit Tests.
Die Lösung verwendet count
in der lokalen Variablen, aber Additionsmethoden werden direkt in counter
geschrieben. Sie teilen sich die gleiche äußere lexikalische Umgebung und können auch auf die aktuelle count
zugreifen.
Funktion makeCounter() { lass zählen = 0; Funktion counter() { return count++; } counter.set = value => count = value; counter.decrease = () => count--; Rückgabezähler; }
Öffnen Sie die Lösung mit Tests in einer Sandbox.
Wichtigkeit: 2
Schreiben Sie eine sum
, die so funktionieren würde:
Summe(1)(2) == 3; // 1 + 2 Summe(1)(2)(3) == 6; // 1 + 2 + 3 Summe(5)(-1)(2) == 6 Summe(6)(-1)(-2)(-3) == 0 Summe(0)(1)(2)(3)(4)(5) == 15
PS-Hinweis: Möglicherweise müssen Sie für Ihre Funktion eine benutzerdefinierte Objekt-zu-Primitiv-Konvertierung einrichten.
Öffnen Sie eine Sandbox mit Tests.
Damit das Ganze trotzdem funktioniert, muss das Ergebnis von sum
eine Funktion sein.
Diese Funktion muss den aktuellen Wert zwischen Aufrufen im Speicher behalten.
Je nach Aufgabenstellung muss die Funktion bei Verwendung in ==
zur Zahl werden. Funktionen sind Objekte, daher erfolgt die Konvertierung wie im Kapitel Konvertierung von Objekten in Grundelemente beschrieben, und wir können unsere eigene Methode bereitstellen, die die Zahl zurückgibt.
Nun der Code:
Funktion sum(a) { let currentSum = a; Funktion f(b) { aktuelleSumme += b; Rückkehr f; } f.toString = function() { return currentSum; }; Rückkehr f; } alarm( sum(1)(2) ); // 3 alarm( sum(5)(-1)(2) ); // 6 alarm( sum(6)(-1)(-2)(-3) ); // 0 alarm( sum(0)(1)(2)(3)(4)(5) ); // 15
Bitte beachten Sie, dass die sum
tatsächlich nur einmal funktioniert. Es gibt die Funktion f
zurück.
Dann fügt f
bei jedem weiteren Aufruf seinen Parameter zur Summe currentSum
hinzu und gibt sich selbst zurück.
In der letzten Zeile von f
gibt es keine Rekursion.
So sieht eine Rekursion aus:
Funktion f(b) { aktuelleSumme += b; return f(); // <-- rekursiver Aufruf }
Und in unserem Fall geben wir die Funktion einfach zurück, ohne sie aufzurufen:
Funktion f(b) { aktuelleSumme += b; Rückkehr f; // <-- ruft sich nicht selbst auf, sondern gibt sich selbst zurück }
Dieses f
wird beim nächsten Aufruf verwendet und gibt sich selbst wieder zurück, so oft wie nötig. Bei Verwendung als Zahl oder Zeichenfolge gibt toString
dann die currentSum
zurück. Wir könnten hier auch Symbol.toPrimitive
oder valueOf
für die Konvertierung verwenden.
Öffnen Sie die Lösung mit Tests in einer Sandbox.