Kapitel 02: First-Class Funktionen
Eine kurze Wiederholung
Wenn wir sagen Funktionen sind "First-Class", meinen wir, dass sie wie jede andere Objektklasse behandelt werden. Genauer gesagt behandeln wir Funktionen als normalen Datentyp - sie können in Arrays gespeichert, als Funktionsparameter übergeben, Variablen zugewiesen und vieles mehr werden, ohne besondere Einschränkungen.
Dies ist JavaScript-Grundlagenwissen, dennoch erwähnenswert, weil eine GitHub-Suche zeigt, dass dieses Konzept oft ignoriert oder missverstanden wird. Betrachten wir ein konstruiertes Beispiel:
const hi = name => `Hi ${name}`;
const greeting = name => hi(name);Der Funktionswrapper um hi in greeting ist hier völlig redundant. Warum? Weil JavaScript-Funktionen aufrufbar sind. Mit () wird hi ausgeführt und gibt einen Wert zurück. Ohne Klammern gibt die Variable die Funktion selbst zurück. Zur Veranschaulichung:
hi; // name => `Hi ${name}`
hi("jonas"); // "Hi jonas"Da greeting nur hi mit demselben Argument aufruft, können wir direkt schreiben:
const greeting = hi;
greeting("times"); // "Hi times"Weshalb sollte man eine Funktion in eine Hülle packen, die lediglich dieselbe Funktion mit demselben verdammten Parameter aufruft? Das ergibt keinen verflixten Sinn. Ähnlich wie im Hochsommer einen dicken Parka anzuziehen, um dann die Klimaanlage einzuschalten.
Diese Praxis ist nicht nur umständlich, sondern auch schlechte Programmierpraxis (insbesondere wegen nachfolgender Wartungsprobleme), wenn man Funktionen zwecks verzögerter Ausführung verpackt.
Bevor wir fortfahren, analysieren wir exemplarische Codeausschnitte aus npm-Paketen.
// ignorant
const getServerStuff = callback => ajaxCall(json => callback(json));
// enlightened
const getServerStuff = ajaxCall;AJAX-Code (Asynchroner JavaScript-und-XML-Code) dieser Art findet sich überall. Ihre Äquivalenz beweist:
// this line
ajaxCall(json => callback(json));
// is the same as this line
ajaxCall(callback);
// so refactor getServerStuff
const getServerStuff = callback => ajaxCall(callback);
// ...which is equivalent to this
const getServerStuff = ajaxCall; // <-- look mum, no ()'sSo sieht korrekte Implementierung aus. Lassen Sie mich die zentralen Gründe verdeutlichen:
const BlogController = {
index(posts) { return Views.index(posts); },
show(post) { return Views.show(post); },
create(attrs) { return Db.create(attrs); },
update(post, attrs) { return Db.update(post, attrs); },
destroy(post) { return Db.destroy(post); },
};Dieser Controller besteht zu 99% aus Redundanz. Alternativlösungen:
const BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy,
};...oder Komplettentfernung, da er nur Views und Db verkettet ohne Mehrwert.
Warum First-Class Funktionen bevorzugen?
Wie die Beispiele getServerStuff und BlogController zeigen, führen sinnlose Indirektionsebenen zu wartungsintensivem Redundanzcode.
Zudem: Änderungen an gewrappten Funktionen erzwingen Modifikationen an allen Wrappern.
httpGet('/post/2', json => renderPost(json));Wenn httpGet zukünftig Error-Handling benötigt, muss sämtlicher Adaptercode angepasst werden.
// go back to every httpGet call in the application and explicitly pass err along.
httpGet('/post/2', (json, err) => renderPost(json, err));Mit First-Class-Ansatz reduziert sich der Änderungsaufwand deutlich:
// renderPost is called from within httpGet with however many arguments it wants
httpGet('/post/2', renderPost);Neben der Reduktion überflüssiger Funktionen gilt es, Parameternamen gezielt zu wählen. Inkonsistente Benennung insbesondere bei wachsenden Codebasen wird schnell problematisch.
Synonyme für denselben Begriff sind eine häufige Fehlerquelle. Vergleichen Sie diese beiden funktional identischen Implementierungen - die zweite ist deutlich generischer:
// specific to our current blog
const validArticles = articles =>
articles.filter(article => article !== null && article !== undefined),
// vastly more relevant for future projects
const compact = xs => xs.filter(x => x !== null && x !== undefined);Spezifische Benennungen wie „articles“ binden Code an konkrete Datenstrukturen und behindern Wiederverwendung.
Warnung: Bei this-Verwendung in zugrundeliegenden Funktionen kann es durch First-Class-Aufrufe zu unerwarteten Kontextproblemen kommen.
const fs = require('fs');
// scary
fs.readFile('freaky_friday.txt', Db.save);
// less so
fs.readFile('freaky_friday.txt', Db.save.bind(Db));Durch this-Bindung erhält Db Zugriff auf prototypischen Müllcode. Bei funktionaler Programmierung ist this verzichtbar. Für Fremdbibliotheken kann dennoch Anpassung nötig sein.
Manche argumentieren, this sei für Performance-Optimierungen nötig. Micro-Optimierern sei geraten: Dieses Buch hilft Ihnen nicht weiter.
Damit sind wir bereit für das nächste Kapitel.