9 Funktionen, Scopes und Closures

Funktionen bilden einen zentralen Baustein der JavaScript-Programmierung. Sie ermöglichen die Organisation von Code in wiederverwendbare, modulare Einheiten und unterstützen verschiedene Programmierstile - von prozedural über objektorientiert bis hin zu funktional. Eng mit Funktionen verbunden sind die Konzepte von Scopes und Closures, die maßgeblich beeinflussen, wie Variablen in JavaScript verwaltet werden.

9.1 Funktionsarten und Parameter

JavaScript bietet verschiedene Arten, Funktionen zu definieren, die jeweils eigene charakteristische Eigenschaften und Anwendungsfälle haben.

9.1.1 Funktionsdeklaration

Die klassische Form, eine Funktion zu definieren, ist die Funktionsdeklaration:

function willkommen(name) {
  return `Hallo ${name}!`;
}

Ein Merkmal der Funktionsdeklaration ist das “Hoisting” - die Funktion wird zur Kompilierzeit an den Anfang ihres Scopes “gehoben” und kann bereits vor ihrer Definition im Code aufgerufen werden:

console.log(willkommen("Max")); // Funktioniert trotz Aufruf vor Definition

function willkommen(name) {
  return `Hallo ${name}!`;
}

9.1.2 Funktionsausdruck

Bei einem Funktionsausdruck wird die Funktion einer Variablen zugewiesen:

const quadrieren = function(zahl) {
  return zahl * zahl;
};

Im Gegensatz zu Funktionsdeklarationen werden Funktionsausdrücke nicht gehoisted und können erst nach ihrer Definition aufgerufen werden.

Funktionsausdrücke können benannt werden, was für rekursive Aufrufe oder bessere Debugging-Informationen nützlich ist:

const fakultät = function fak(n) {
  if (n <= 1) return 1;
  return n * fak(n - 1);
};

9.1.3 Pfeilfunktionen (ES6)

Mit ES6 wurden Pfeilfunktionen eingeführt, die eine kompaktere Syntax bieten:

// Grundlegende Syntax
const addieren = (a, b) => {
  return a + b;
};

// Bei einem einzelnen Parameter können die Klammern entfallen
const verdoppeln = x => x * 2;

// Bei einem Einzeiler mit direkter Rückgabe können geschweifte Klammern entfallen
const halbieren = x => x / 2;

Pfeilfunktionen haben einige wichtige Besonderheiten:

Die lexikalische Bindung von this macht Pfeilfunktionen besonders nützlich für Callbacks:

const timer = {
  sekunden: 0,
  start() {
    // Pfeilfunktion behält das 'this' von timer bei
    setInterval(() => {
      this.sekunden++;
      console.log(this.sekunden);
    }, 1000);
  }
};

9.1.4 Weitere Funktionsformen

JavaScript bietet noch weitere spezialisierte Funktionsarten:

Diese spezialisierten Funktionstypen werden in den Kapiteln “Generatoren und Iteratoren” und “Asynchrones JavaScript” detaillierter behandelt.

9.2 Parameter und Argumente

JavaScript bietet flexible Möglichkeiten, mit Funktionsparametern umzugehen.

9.2.1 Grundlegende Parameter

In ihrer einfachsten Form nehmen Funktionen eine feste Anzahl benannter Parameter entgegen:

function grüßen(vorname, nachname) {
  return `Hallo ${vorname} ${nachname}!`;
}

JavaScript erzwingt keine strenge Übereinstimmung zwischen deklarierten Parametern und übergebenen Argumenten:

9.2.2 Standardparameter (ES6)

Mit ES6 können Standardwerte für Parameter definiert werden:

function konfigurieren(optionen = {}, debugModus = false) {
  // optionen ist ein leeres Objekt, wenn kein Wert übergeben wird
  // debugModus ist false, wenn kein Wert übergeben wird
}

Standardparameter werden nur aktiviert, wenn das Argument undefined ist oder fehlt:

function log(nachricht, level = "info") {
  console.log(`[${level}]: ${nachricht}`);
}

log("Test");             // [info]: Test
log("Fehler", "error");  // [error]: Fehler
log("Seltsam", null);    // [null]: Seltsam - null ist ein gültiger Wert, überschreibt den Standard

9.2.3 Rest-Parameter (ES6)

Rest-Parameter ermöglichen die Verarbeitung einer beliebigen Anzahl von Argumenten als Array:

function summe(...zahlen) {
  return zahlen.reduce((summe, zahl) => summe + zahl, 0);
}

summe(1, 2, 3, 4, 5);  // 15

Rest-Parameter müssen der letzte Parameter in der Funktionsdefinition sein und bieten eine modernere Alternative zum arguments-Objekt.

9.2.4 Destrukturierung in Parametern

Mit Objektdestrukturierung können Parameter übersichtlicher gestaltet werden:

function renderBenutzer({ name, email, rolle = "Benutzer" }) {
  // Parameter werden direkt aus dem übergebenen Objekt extrahiert
  // rolle erhält einen Standardwert, falls nicht im Objekt vorhanden
}

// Aufruf
renderBenutzer({ name: "Max Mustermann", email: "max@beispiel.de" });

9.3 Lexikalisches Scoping

Der “Scope” definiert die Sichtbarkeit und Lebensdauer von Variablen. JavaScript verwendet lexikalisches Scoping, bei dem der Scope einer Variablen durch ihre Position im Quellcode zur Schreibzeit bestimmt wird.

9.3.1 Arten von Scopes

JavaScript kennt verschiedene Arten von Scopes:

  1. Globaler Scope: Variablen, die außerhalb jeder Funktion oder Blocks deklariert werden
  2. Funktions-Scope: Variablen, die innerhalb einer Funktion deklariert werden
  3. Block-Scope: Variablen, die innerhalb eines Blocks ({}) mit let oder const deklariert werden

9.3.2 Function-Scope vs. Block-Scope

Die Art der Variablendeklaration bestimmt den Scope-Typ:

function scopeDemo() {
  var functionScoped = "Ich bin in der ganzen Funktion sichtbar";
  
  if (true) {
    var auchFunctionScoped = "Auch ich bin überall in der Funktion sichtbar";
    let blockScoped = "Ich bin nur in diesem if-Block sichtbar";
    
    console.log(functionScoped);     // Funktioniert
    console.log(blockScoped);        // Funktioniert
  }
  
  console.log(auchFunctionScoped);   // Funktioniert
  // console.log(blockScoped);       // ReferenceError
}

9.3.3 Scope-Hierarchie

Scopes sind hierarchisch verschachtelt. Innere Scopes haben Zugriff auf Variablen aus äußeren Scopes, aber nicht umgekehrt:

const global = "Ich bin global";

function äußereFunktion() {
  const äußer = "Ich bin in der äußeren Funktion";
  
  function innereFunktion() {
    const inner = "Ich bin in der inneren Funktion";
    console.log(global);  // Zugriff auf globale Variable möglich
    console.log(äußer);   // Zugriff auf Variable aus äußerem Scope möglich
  }
  
  innereFunktion();
  // console.log(inner); // ReferenceError - kein Zugriff auf inneren Scope
}

9.3.4 Variable Hoisting

“Hoisting” beschreibt das Verhalten, bei dem Deklarationen (nicht Initialisierungen) von Variablen und Funktionen an den Anfang ihres Scopes gehoben werden:

console.log(gehoisteteFunktion()); // "Ich wurde gehoistet" - funktioniert

function gehoisteteFunktion() {
  return "Ich wurde gehoistet";
}

console.log(varVariable); // undefined - deklariert, aber nicht initialisiert
var varVariable = "Ich bin eine var-Variable";

// console.log(letVariable); // ReferenceError - temporal dead zone
let letVariable = "Ich bin eine let-Variable";

9.4 Closures

Eine Closure entsteht, wenn eine Funktion auf Variablen aus ihrem äußeren lexikalischen Scope zugreift, selbst wenn sie außerhalb dieses Scopes ausgeführt wird. Dies ist eines der mächtigsten Konzepte in JavaScript.

9.4.1 Grundkonzept

function äußereFunktion() {
  const message = "Hallo von außen!";
  
  function innereFunktion() {
    console.log(message); // Greift auf Variable aus äußerem Scope zu
  }
  
  return innereFunktion;
}

const meineClosure = äußereFunktion();
meineClosure(); // "Hallo von außen!" - auch nach Abschluss von äußereFunktion

Die zurückgegebene innereFunktion “erinnert” sich an ihre Umgebung - sie behält Zugriff auf alle Variablen, die zum Zeitpunkt ihrer Definition im Scope waren, selbst wenn die äußereFunktion bereits abgeschlossen ist.

9.4.2 Praktische Anwendungen

Closures finden in verschiedenen Szenarien Anwendung:

1. Datenprivatsheit und Kapselung

function createCounter() {
  let count = 0; // Private Variable
  
  return {
    increment() { return ++count; },
    decrement() { return --count; },
    getValue() { return count; }
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
// Der direkte Zugriff auf count ist nicht möglich

2. Funktionsfabriken

function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = multiplier(2);
const triple = multiplier(3);

double(5); // 10
triple(5); // 15

3. Ereignishandler mit Zustandsspeicherung

function createButtonHandler(buttonName) {
  let clickCount = 0;
  
  return function() {
    clickCount++;
    console.log(`${buttonName} wurde ${clickCount} mal geklickt`);
  };
}

const handleButtonA = createButtonHandler("Button A");

9.4.3 Häufige Fehler mit Closures

Ein klassisches Problem tritt auf, wenn Closures in Schleifen erstellt werden:

// Problematisch
function createFunctions() {
  var functions = [];
  
  for (var i = 0; i < 3; i++) {
    functions.push(function() {
      console.log(i);
    });
  }
  
  return functions;
}

var funcs = createFunctions();
funcs[0](); // 3 (nicht 0 wie erwartet)
funcs[1](); // 3
funcs[2](); // 3

Alle Funktionen greifen auf dieselbe i-Variable zu, die nach Schleifenende den Wert 3 hat.

Mit ES6 löst let dieses Problem elegant:

function createFunctions() {
  const functions = [];
  
  for (let i = 0; i < 3; i++) {
    // let erzeugt für jeden Schleifendurchlauf einen neuen Block-Scope
    functions.push(function() {
      console.log(i);
    });
  }
  
  return functions;
}

const funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2

9.5 Das this-Keyword

Das this-Keyword in JavaScript verweist auf das Ausführungskontext-Objekt und verhält sich anders als in vielen anderen Sprachen. Der Wert von this wird dynamisch zur Laufzeit bestimmt und hängt davon ab, wie eine Funktion aufgerufen wird.

9.5.1 Grundregeln für this

  1. Globaler Kontext: Im globalen Scope verweist this auf das globale Objekt (z.B. window im Browser)

  2. Funktionsaufruf: Bei einem einfachen Funktionsaufruf ist this im strikten Modus undefined, sonst das globale Objekt

  3. Methodenaufruf: Bei einem Methodenaufruf verweist this auf das Objekt, auf dem die Methode aufgerufen wird

  4. Konstruktoraufruf: Mit new verweist this auf die neu erstellte Instanz

  5. Explizite Bindung: Mit .call(), .apply() oder .bind() kann this explizit gesetzt werden

// Als Methode
const person = {
  name: "Max",
  greet() {
    return `Hallo, ich bin ${this.name}`;
  }
};
person.greet(); // "Hallo, ich bin Max"

// Als einfache Funktion
const greetFunc = person.greet;
greetFunc(); // "Hallo, ich bin undefined" (oder Fehler im strikten Modus)

// Explizite Bindung
const anna = { name: "Anna" };
person.greet.call(anna); // "Hallo, ich bin Anna"

9.5.2 Lexikalisches this mit Pfeilfunktionen

Pfeilfunktionen haben kein eigenes this, sondern übernehmen es aus dem umgebenden lexikalischen Kontext:

const objekt = {
  daten: [1, 2, 3],
  
  // Problematisch mit regulärer Funktion
  prozessiereTraditional: function() {
    console.log(this.daten); // [1, 2, 3]
    
    this.daten.forEach(function(item) {
      // this ist hier nicht mehr objekt
      console.log(this.daten, item); // undefined, 1/2/3
    });
  },
  
  // Besser mit Pfeilfunktion
  prozessiereMitPfeil: function() {
    console.log(this.daten); // [1, 2, 3]
    
    this.daten.forEach(item => {
      // this bleibt objekt dank lexikalischer Bindung
      console.log(this.daten, item); // [1, 2, 3], 1/2/3
    });
  }
};

9.6 Funktionale Programmierung mit Funktionen höherer Ordnung

Da Funktionen in JavaScript First-Class-Objekte sind, können sie:

Diese Eigenschaften ermöglichen funktionale Programmiertechniken:

9.6.1 Funktionen höherer Ordnung

Funktionen, die andere Funktionen als Parameter akzeptieren oder zurückgeben:

// Nimmt eine Funktion als Parameter
function mapArray(array, transformFn) {
  const result = [];
  for (let i = 0; i < array.length; i++) {
    result.push(transformFn(array[i], i));
  }
  return result;
}

// Gibt eine Funktion zurück
function createValidator(regex) {
  return function(value) {
    return regex.test(value);
  };
}

const isEmail = createValidator(/^[^@]+@[^@]+\.[^@]+$/);
isEmail("test@example.com"); // true

9.6.2 Reine Funktionen (Pure Functions)

Reine Funktionen haben keine Seiteneffekte und liefern bei gleichen Eingaben immer gleiche Ausgaben:

// Reine Funktion
function add(a, b) {
  return a + b;
}

// Nicht reine Funktion (mit Seiteneffekt)
let total = 0;
function addToTotal(value) {
  total += value; // Seiteneffekt: Ändert Zustand außerhalb
  return total;
}

9.6.3 Funktionskomposition

Das Kombinieren mehrerer Funktionen zu einer neuen Funktion:

function compose(f, g) {
  return function(x) {
    return f(g(x));
  };
}

const double = x => x * 2;
const increment = x => x + 1;

const doubleAndIncrement = compose(increment, double);
doubleAndIncrement(3); // 7 (= (3*2) + 1)