Funktionale Programmierung (FP) ist ein Programmierparadigma, das die Anwendung von Funktionen betont und dabei versucht, Seiteneffekte zu vermeiden und veränderliche Daten zu minimieren. JavaScript unterstützt funktionale Programmierkonzepte und bietet vielseitige Möglichkeiten, diese Prinzipien in der Praxis anzuwenden. In diesem Abschnitt werden wir die Grundlagen der funktionalen Programmierung in JavaScript, ihre Konzepte und praktischen Anwendungen untersuchen.
Die funktionale Programmierung basiert auf verschiedenen Schlüsselkonzepten:
Eine pure Funktion ist eine Funktion, die: - Bei gleichen Eingaben immer die gleichen Ausgaben liefert - Keine Seiteneffekte hat (keine Änderung von externen Zuständen) - Nicht auf externe Zustände angewiesen ist
// Impure Funktion (mit Seiteneffekt)
let counter = 0;
function incrementCounter() {
counter++;
return counter;
}
// Pure Funktion
function add(a, b) {
return a + b;
}In der funktionalen Programmierung werden Daten als unveränderlich betrachtet. Statt Daten zu ändern, erzeugen wir neue Daten mit den gewünschten Änderungen.
// Veränderlicher Ansatz (nicht funktional)
const zahlen = [1, 2, 3, 4, 5];
zahlen.push(6); // Verändert das Original-Array
// Unveränderlicher Ansatz (funktional)
const originalZahlen = [1, 2, 3, 4, 5];
const neueZahlen = [...originalZahlen, 6]; // Erstellt ein neues ArrayFunktionen höherer Ordnung sind Funktionen, die: - Andere Funktionen als Argumente annehmen können - Funktionen als Ergebnis zurückgeben können
// Funktion, die eine Funktion als Argument nimmt
function mapNumbers(numbers, transformFn) {
return numbers.map(transformFn);
}
// Verwendung
const double = n => n * 2;
const doubled = mapNumbers([1, 2, 3], double); // [2, 4, 6]
// Funktion, die eine Funktion zurückgibt
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const triple = multiplier(3);
const tripled = triple(4); // 12Funktionale Programmierung bevorzugt einen deklarativen Stil (beschreibt, “was” erreicht werden soll) gegenüber einem imperativen Stil (beschreibt, “wie” etwas erreicht werden soll).
// Imperativ
const zahlen = [1, 2, 3, 4, 5];
const geradeZahlen = [];
for (let i = 0; i < zahlen.length; i++) {
if (zahlen[i] % 2 === 0) {
geradeZahlen.push(zahlen[i]);
}
}
// Deklarativ
const zahlen = [1, 2, 3, 4, 5];
const geradeZahlen = zahlen.filter(zahl => zahl % 2 === 0);In JavaScript sind Funktionen “First-Class Citizens”, was bedeutet, dass sie wie jeder andere Datentyp behandelt werden können:
// Funktionen können Variablen zugewiesen werden
const greet = function(name) {
return `Hallo, ${name}!`;
};
// Funktionen können in Datenstrukturen gespeichert werden
const actions = [
function add(a, b) { return a + b; },
function subtract(a, b) { return a - b; }
];
// Funktionen können an andere Funktionen übergeben werden
function executeOperation(operation, a, b) {
return operation(a, b);
}
const result = executeOperation((a, b) => a * b, 5, 3); // 15Closures sind ein wesentlicher Bestandteil der funktionalen Programmierung in JavaScript. Eine Closure entsteht, wenn eine Funktion auf Variablen aus ihrem äußeren Lexikalischen Scope zugreifen kann, auch nachdem dieser äußere Scope nicht mehr existiert.
function createCounter(initialValue = 0) {
// Die Variable count ist nur innerhalb von createCounter sichtbar
let count = initialValue;
// Diese zurückgegebene Funktion hat Zugriff auf count, auch nachdem createCounter beendet wurde
return function() {
return count++;
};
}
const counter = createCounter(10);
console.log(counter()); // 10
console.log(counter()); // 11
console.log(counter()); // 12Closures ermöglichen: - Kapselung von Daten - Erzeugung von Funktionsfabriken - Implementation von privaten Variablen
Die Funktionskomposition ist eine Technik, um komplexe Funktionen aus einfacheren Funktionen zu erstellen.
// Einfache Funktionen
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const subtract5 = x => x - 5;
// Manuelle Komposition
const transformValue = x => subtract5(multiply2(add10(x)));
console.log(transformValue(5)); // ((5 + 10) * 2) - 5 = 25
// Hilfsfunktion zur Funktionskomposition
function compose(...functions) {
return function(initialValue) {
return functions.reduceRight((value, fn) => fn(value), initialValue);
};
}
const transform = compose(subtract5, multiply2, add10);
console.log(transform(5)); // ((5 + 10) * 2) - 5 = 25Pipe ist ähnlich wie Compose, aber die Funktionen werden in der Reihenfolge ihrer Lesbarkeit (von links nach rechts) angewendet:
function pipe(...functions) {
return function(initialValue) {
return functions.reduce((value, fn) => fn(value), initialValue);
};
}
const transform = pipe(add10, multiply2, subtract5);
console.log(transform(5)); // ((5 + 10) * 2) - 5 = 25Currying ist die Technik, eine Funktion mit mehreren Argumenten in eine Sequenz von Funktionen umzuwandeln, die jeweils nur ein Argument annehmen.
// Normale Funktion mit mehreren Parametern
function add(a, b, c) {
return a + b + c;
}
// Gecurryte Version
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
// Verwendung
console.log(add(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6
// Mit Arrow Functions wird es kompakter
const curriedAddArrow = a => b => c => a + b + c;
console.log(curriedAddArrow(1)(2)(3)); // 6
// Hilfsfunktion zum Currying
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
const curriedAddFlex = curry(add);
console.log(curriedAddFlex(1)(2)(3)); // 6
console.log(curriedAddFlex(1, 2)(3)); // 6
console.log(curriedAddFlex(1)(2, 3)); // 6Currying bietet Vorteile wie: - Partielle Anwendung von Funktionen - Erzeugung spezialisierter Funktionen - Verbesserte Wiederverwendbarkeit
Partielle Anwendung ist das Vorab-Binden einiger Argumente einer Funktion, um eine neue Funktion zu erzeugen:
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = partial(greet, "Hallo");
console.log(sayHello("Max")); // "Hallo, Max!"JavaScript bietet verschiedene integrierte Methoden, die funktionale Programmierung mit Arrays und Objekte erleichtern.
const zahlen = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map: Transformiert jedes Element
const verdoppelt = zahlen.map(n => n * 2);
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// filter: Selektiert Elemente basierend auf einer Bedingung
const geradeZahlen = zahlen.filter(n => n % 2 === 0);
// [2, 4, 6, 8, 10]
// reduce: Reduziert das Array auf einen einzelnen Wert
const summe = zahlen.reduce((acc, n) => acc + n, 0);
// 55
// find: Findet das erste Element, das eine Bedingung erfüllt
const erstesGerade = zahlen.find(n => n % 2 === 0);
// 2
// every: Prüft, ob alle Elemente eine Bedingung erfüllen
const allePositiv = zahlen.every(n => n > 0);
// true
// some: Prüft, ob mindestens ein Element eine Bedingung erfüllt
const mindestensEinGerade = zahlen.some(n => n % 2 === 0);
// true
// Verkettung von Methoden
const summeGeradeVerdoppelt = zahlen
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0);
// 60const benutzer = {
name: "Max Mustermann",
alter: 30,
email: "max@beispiel.de",
rolle: "Admin"
};
// Object.keys, Object.values, Object.entries
const eigenschaften = Object.keys(benutzer);
// ["name", "alter", "email", "rolle"]
const werte = Object.values(benutzer);
// ["Max Mustermann", 30, "max@beispiel.de", "Admin"]
const eintraege = Object.entries(benutzer);
// [["name", "Max Mustermann"], ["alter", 30], ...]
// Funktionaler Ansatz zur Transformation eines Objekts
const benutzerMitVolljährigkeit = Object.entries(benutzer)
.reduce((obj, [key, value]) => {
obj[key] = value;
if (key === "alter") {
obj.istVolljaehrig = value >= 18;
}
return obj;
}, {});
// { name: "Max Mustermann", alter: 30, ... istVolljaehrig: true }Um Seiteneffekte zu vermeiden, ist es wichtig, mit unveränderlichen Datenstrukturen zu arbeiten.
const originalArray = [1, 2, 3, 4, 5];
// Hinzufügen eines Elements
const neuesArray1 = [...originalArray, 6];
// Oder
const neuesArray2 = originalArray.concat(6);
// Entfernen eines Elements
const ohneElement = originalArray.filter(item => item !== 3);
// Ersetzen eines Elements
const ersetzt = originalArray.map(item => item === 3 ? 30 : item);
// Aktualisieren eines Elements an einem bestimmten Index
const aktualisiert = [
...originalArray.slice(0, 2),
30, // Neuer Wert für Index 2
...originalArray.slice(3)
];const originalObjekt = {
name: "Max",
alter: 30,
adresse: {
strasse: "Hauptstraße",
hausnummer: 42,
stadt: "Berlin"
}
};
// Flaches Update
const aktualisiertesObjekt = {
...originalObjekt,
alter: 31,
beruf: "Entwickler"
};
// Verschachteltes Update
const mitNeuerAdresse = {
...originalObjekt,
adresse: {
...originalObjekt.adresse,
hausnummer: 43,
plz: "10115"
}
};Generatoren ermöglichen die faule Auswertung (Lazy Evaluation), bei der Werte nur bei Bedarf berechnet werden:
function* unendlicheFibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fibGenerator = unendlicheFibonacci();
for (let i = 0; i < 10; i++) {
console.log(fibGenerator.next().value);
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34Monaden sind ein fortgeschrittenes Konzept aus der funktionalen Programmierung, das Berechnungen in einem Kontext kapselt:
// Eine einfache Maybe-Monade
class Maybe {
constructor(value) {
this._value = value;
}
static of(value) {
return new Maybe(value);
}
isNothing() {
return this._value === null || this._value === undefined;
}
map(fn) {
if (this.isNothing()) {
return this;
}
return Maybe.of(fn(this._value));
}
getOrElse(defaultValue) {
return this.isNothing() ? defaultValue : this._value;
}
}
// Verwendung
const result = Maybe.of(5)
.map(x => x * 2)
.map(x => x + 10)
.getOrElse(0);
console.log(result); // 20
const safeResult = Maybe.of(null)
.map(x => x * 2) // Wird übersprungen
.map(x => x + 10) // Wird übersprungen
.getOrElse(0);
console.log(safeResult); // 0Das Either-Pattern ist ein funktionales Muster zur Fehlerbehandlung:
class Either {
static left(value) {
return {
isLeft: true,
isRight: false,
value,
map: _ => Either.left(value),
flatMap: _ => Either.left(value),
getOrElse: defaultValue => defaultValue,
fold: (leftFn, _) => leftFn(value)
};
}
static right(value) {
return {
isLeft: false,
isRight: true,
value,
map: fn => Either.right(fn(value)),
flatMap: fn => fn(value),
getOrElse: _ => value,
fold: (_, rightFn) => rightFn(value)
};
}
}
// Verwendung
function divide(a, b) {
if (b === 0) {
return Either.left("Division durch Null");
}
return Either.right(a / b);
}
const erfolg = divide(10, 2)
.map(result => result * 2)
.fold(
error => `Fehler: ${error}`,
value => `Ergebnis: ${value}`
);
console.log(erfolg); // "Ergebnis: 10"
const fehler = divide(10, 0)
.map(result => result * 2)
.fold(
error => `Fehler: ${error}`,
value => `Ergebnis: ${value}`
);
console.log(fehler); // "Fehler: Division durch Null"// Angenommen, wir haben eine Liste von Produkten
const produkte = [
{ id: 1, name: "Laptop", preis: 1200, kategorie: "Elektronik" },
{ id: 2, name: "Schreibtisch", preis: 350, kategorie: "Möbel" },
{ id: 3, name: "Maus", preis: 25, kategorie: "Elektronik" },
{ id: 4, name: "Tastatur", preis: 80, kategorie: "Elektronik" },
{ id: 5, name: "Stuhl", preis: 150, kategorie: "Möbel" },
{ id: 6, name: "Monitor", preis: 300, kategorie: "Elektronik" }
];
// Funktionale Verarbeitung:
// 1. Filtere nach Elektronikprodukten
// 2. Berechne den Preis mit Mehrwertsteuer
// 3. Sortiere nach Preis (aufsteigend)
// 4. Projiziere auf ein einfacheres Format
const ergebnis = produkte
.filter(produkt => produkt.kategorie === "Elektronik")
.map(produkt => ({
...produkt,
preisInklMwSt: Math.round(produkt.preis * 1.19 * 100) / 100
}))
.sort((a, b) => a.preisInklMwSt - b.preisInklMwSt)
.map(produkt => ({
name: produkt.name,
preis: produkt.preisInklMwSt
}));
console.log(ergebnis);
/*
[
{ name: "Maus", preis: 29.75 },
{ name: "Tastatur", preis: 95.2 },
{ name: "Monitor", preis: 357 },
{ name: "Laptop", preis: 1428 }
]
*/// Fictive API-Funktionen
const fetchUser = id => fetch(`/api/users/${id}`).then(r => r.json());
const fetchPosts = userId => fetch(`/api/users/${userId}/posts`).then(r => r.json());
const fetchComments = postId => fetch(`/api/posts/${postId}/comments`).then(r => r.json());
// Funktionale Hilfsfunktionen
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const prop = key => obj => obj[key];
const map = fn => array => array.map(fn);
const flatMap = fn => array => array.flatMap(fn);
const first = array => array[0];
// Asynchrone Pipeline mit Promises
async function getUserFirstPostComments(userId) {
return pipe(
fetchUser,
user => fetchPosts(user.id),
posts => first(posts),
post => fetchComments(post.id)
)(userId);
}
// Verwendung
getUserFirstPostComments(123)
.then(comments => console.log("Kommentare:", comments))
.catch(error => console.error("Fehler:", error));// Funktionen zur Ereignisverarbeitung
const preventDefault = event => {
event.preventDefault();
return event;
};
const extractFormData = event => {
const form = event.target;
const formData = new FormData(form);
return {
event,
formData: Object.fromEntries(formData.entries())
};
};
const validateForm = ({ event, formData }) => {
const errors = {};
if (!formData.name || formData.name.length < 3) {
errors.name = "Name muss mindestens 3 Zeichen lang sein";
}
if (!formData.email || !formData.email.includes('@')) {
errors.email = "Ungültige E-Mail-Adresse";
}
return {
event,
formData,
errors,
isValid: Object.keys(errors).length === 0
};
};
const submitForm = ({ event, formData, errors, isValid }) => {
if (!isValid) {
return { success: false, errors };
}
// API-Aufruf würde hier stattfinden
console.log("Formular wird gesendet:", formData);
return { success: true, data: formData };
};
// Zusammengesetzte Funktion
const handleSubmit = pipe(
preventDefault,
extractFormData,
validateForm,
submitForm
);
// Verbindung mit dem DOM
document.getElementById('userForm').addEventListener('submit', event => {
const result = handleSubmit(event);
if (result.success) {
showSuccessMessage("Formular erfolgreich gesendet!");
} else {
showErrors(result.errors);
}
});Diese modernen JavaScript-Features ergänzen funktionale Programmierung durch sicherere Zugriffe auf Objekte:
// Optional Chaining (?.)
const benutzer = {
profil: {
adresse: {
stadt: "Berlin"
}
}
};
// Traditioneller defensiver Ansatz
const stadt1 = benutzer &&
benutzer.profil &&
benutzer.profil.adresse &&
benutzer.profil.adresse.stadt ||
"Unbekannt";
// Mit Optional Chaining
const stadt2 = benutzer?.profil?.adresse?.stadt ?? "Unbekannt";Diese Features erleichtern das Arbeiten mit unveränderlichen Datenstrukturen:
// Destructuring für leichteren Zugriff auf Objekteigenschaften
function verarbeiteBenutzer({ name, alter, email }) {
return `${name} (${alter}) - ${email}`;
}
// Spread für unveränderliche Updates
function aktualisiereBenutzer(benutzer, updates) {
return { ...benutzer, ...updates };
}
const benutzer = { name: "Max", alter: 30, email: "max@example.com" };
const aktualisiert = aktualisiereBenutzer(benutzer, { alter: 31 });Es gibt verschiedene Bibliotheken, die funktionale Programmierung in JavaScript unterstützen:
Beispiel mit Ramda:
const R = require('ramda');
const benutzer = [
{ name: "Max", alter: 30, aktiv: true },
{ name: "Anna", alter: 25, aktiv: false },
{ name: "Tom", alter: 40, aktiv: true }
];
// Finde aktive Benutzer und extrahiere ihre Namen
const aktiveBenutzernamen = R.pipe(
R.filter(R.prop('aktiv')),
R.map(R.prop('name')),
R.join(', ')
)(benutzer);
console.log(aktiveBenutzernamen); // "Max, Tom"