blog / scopes-closures-praxis

Scopes und Closures: Der Game-Changer in meinem JavaScript-Code

Der Moment, in dem es Klick gemacht hat

Vor zwei Jahren hat mir ein Senior-Developer gesagt: “Wenn du Closures verstehst, verstehst du JavaScript.”

Ich habe nur Bahnhof verstanden. Closures? Klingt kompliziert.

Heute sage ich: Er hatte Recht. Scopes und Closures zu verstehen hat meinen Code fundamental verbessert. Lass mich dir zeigen, warum.

Was ist Scope überhaupt?

Scope bestimmt, wo Variablen sichtbar sind.

// Global Scope - überall sichtbar
const globalName = 'Max';

function greet() {
  // Function Scope - nur in dieser Funktion
  const localName = 'Anna';
  console.log(globalName); // OK - greift auf global zu
  console.log(localName);  // OK - lokal verfügbar
}

greet();
console.log(globalName); // OK
console.log(localName);  // ReferenceError: localName is not defined

Die drei Scope-Typen

// 1. Global Scope
const global = 'Überall sichtbar';

function demo() {
  // 2. Function Scope
  const functionScoped = 'Nur in dieser Funktion';

  if (true) {
    // 3. Block Scope (let/const)
    const blockScoped = 'Nur in diesem Block';
    var functionScoped2 = 'Function-scoped trotz Block!';

    console.log(blockScoped); // OK
  }

  console.log(blockScoped);      // ReferenceError
  console.log(functionScoped2);  // OK (var ignoriert Block!)
}

Key Takeaway: Nutze const/let (block-scoped), niemals var (function-scoped).

Mehr Details: Check mein Tutorial zu Variablen und Scopes.

Scope Chain: Wie JavaScript Variablen findet

const level1 = 'Global';

function outer() {
  const level2 = 'Outer';

  function inner() {
    const level3 = 'Inner';

    // JavaScript sucht in dieser Reihenfolge:
    console.log(level3); // 1. Findet es in inner() ✓
    console.log(level2); // 2. Findet es in outer() ✓
    console.log(level1); // 3. Findet es in global ✓
    console.log(level4); // 4. Nicht gefunden → ReferenceError
  }

  inner();
}

outer();

Shadowing: Wenn Namen kollidieren

const name = 'Global Max';

function greet() {
  const name = 'Local Max'; // "Überschattet" die globale Variable
  console.log(name); // "Local Max"
}

greet();
console.log(name); // "Global Max"

Closures: Die Superkraft

Definition: Eine Closure ist eine Funktion, die Zugriff auf Variablen aus ihrem äußeren Scope hat, auch nachdem die äußere Funktion beendet wurde.

Klingt kompliziert? Hier ist ein Beispiel:

function createCounter() {
  let count = 0; // Private Variable

  return function() {
    count++; // Greift auf 'count' aus äußerem Scope zu
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// count ist NICHT direkt zugänglich
console.log(count); // ReferenceError

Was passiert hier?

  1. createCounter() wird aufgerufen und erstellt count = 0
  2. Eine Funktion wird zurückgegeben (die innere Funktion)
  3. createCounter() ist beendet, aber count lebt weiter
  4. Die innere Funktion “erinnert” sich an count (Closure!)

Praktische Closure-Patterns

1. Private Variables (Data Encapsulation)

// ❌ Ohne Closure: Alles public
const user = {
  password: '12345', // Jeder kann es lesen/ändern!
  checkPassword: function(input) {
    return input === this.password;
  }
};

console.log(user.password); // "12345" (nicht gut!)
user.password = 'hacked';   // Kann überschrieben werden!

// ✅ Mit Closure: Private Daten
function createUser(initialPassword) {
  // password ist privat - keine direkte Zugriff möglich
  let password = initialPassword;

  return {
    checkPassword: function(input) {
      return input === password;
    },
    changePassword: function(oldPass, newPass) {
      if (oldPass === password) {
        password = newPass;
        return true;
      }
      return false;
    }
  };
}

const user2 = createUser('12345');
console.log(user2.password);        // undefined (nicht zugänglich!)
user2.password = 'hacked';          // Hat keine Wirkung
console.log(user2.checkPassword('12345')); // true

2. Factory Functions

// Erstelle konfigurierbare Funktionen
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
const times10 = createMultiplier(10);

console.log(double(5));   // 10
console.log(triple(5));   // 15
console.log(times10(5));  // 50

// Jede Funktion "erinnert" sich an ihren eigenen 'factor'!

3. Event Handlers mit Closure

// Problem: Loop mit var
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i); // 4, 4, 4 (nicht 1, 2, 3!)
  }, i * 1000);
}

// Warum? Alle Callbacks teilen sich die GLEICHE Variable 'i'
// Wenn setTimeout ausgeführt wird, ist i bereits 4

// ✅ Lösung 1: let statt var (Block Scope)
for (let i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i); // 1, 2, 3 (Jede Iteration hat eigenes 'i')
  }, i * 1000);
}

// ✅ Lösung 2: IIFE (Immediately Invoked Function Expression)
for (var i = 1; i <= 3; i++) {
  (function(capturedI) {
    setTimeout(function() {
      console.log(capturedI); // 1, 2, 3
    }, capturedI * 1000);
  })(i);
}

4. Module Pattern

// Klassisches Module Pattern mit Closure
const ShoppingCart = (function() {
  // Private Variablen
  let items = [];
  let total = 0;

  // Private Funktion
  function calculateTotal() {
    total = items.reduce((sum, item) => sum + item.price, 0);
  }

  // Public API
  return {
    addItem: function(item) {
      items.push(item);
      calculateTotal();
    },

    removeItem: function(itemName) {
      items = items.filter(item => item.name !== itemName);
      calculateTotal();
    },

    getTotal: function() {
      return total;
    },

    getItems: function() {
      // Return copy, nicht Original (Encapsulation)
      return [...items];
    }
  };
})();

// Usage
ShoppingCart.addItem({ name: 'Book', price: 20 });
ShoppingCart.addItem({ name: 'Pen', price: 5 });
console.log(ShoppingCart.getTotal()); // 25

// items und total sind NICHT zugänglich
console.log(ShoppingCart.items);  // undefined
console.log(ShoppingCart.total);  // undefined

5. Memoization (Caching)

// Teure Berechnung cachen mit Closure
function createMemoizedFunction(fn) {
  const cache = {}; // Private Cache

  return function(arg) {
    if (cache[arg] !== undefined) {
      console.log('From cache:', arg);
      return cache[arg];
    }

    console.log('Computing:', arg);
    const result = fn(arg);
    cache[arg] = result;
    return result;
  };
}

// Beispiel: Fibonacci (langsam ohne Cache)
const fibonacci = createMemoizedFunction(function(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10)); // Computing: 10, 9, 8, ... (erste Berechnung)
console.log(fibonacci(10)); // From cache: 10 (sofort!)

6. Curry Functions

// Currying: Funktion, die Funktion zurückgibt
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }

    return function(...nextArgs) {
      return curried.apply(this, args.concat(nextArgs));
    };
  };
}

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

// Curried Version
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3));     // 6
console.log(curriedAdd(1, 2)(3));     // 6
console.log(curriedAdd(1)(2, 3));     // 6

// Praktisch für Partial Application
const add10 = curriedAdd(10);
console.log(add10(5, 3));  // 18
console.log(add10(2, 8));  // 20

React Hooks: Closures in Action

Wenn du React nutzt, nutzt du Closures ständig:

function Counter() {
  const [count, setCount] = useState(0);

  // Diese Funktion hat Zugriff auf 'count' via Closure
  const increment = () => {
    setCount(count + 1);
  };

  // setTimeout hat auch Closure über 'count'
  useEffect(() => {
    const timer = setTimeout(() => {
      console.log('Count after 1 second:', count);
    }, 1000);

    return () => clearTimeout(timer);
  }, [count]);

  return <button onClick={increment}>{count}</button>;
}

Stale Closure Problem in React

function Counter() {
  const [count, setCount] = useState(0);

  // ❌ Problem: Closure über altes 'count'
  const incrementBroken = () => {
    setTimeout(() => {
      setCount(count + 1); // Nutzt 'count' aus dem Render wo Button geklickt wurde!
    }, 1000);
  };

  // ✅ Lösung: Functional Update
  const incrementFixed = () => {
    setTimeout(() => {
      setCount(prevCount => prevCount + 1); // Immer aktueller Wert
    }, 1000);
  };

  return <button onClick={incrementFixed}>{count}</button>;
}

Memory Leaks durch Closures vermeiden

Problem: Event Listeners

// ❌ Memory Leak
function attachEventWithLeak() {
  const largeData = new Array(1000000).fill('data');

  document.getElementById('button').addEventListener('click', function() {
    console.log('Button clicked');
    // Closure hält Referenz auf largeData, auch wenn nicht genutzt!
  });
}

// ✅ Fix: Cleanup
function attachEventProperly() {
  const largeData = new Array(1000000).fill('data');

  function handleClick() {
    console.log('Button clicked');
    // largeData nicht referenziert = kann garbage collected werden
  }

  const button = document.getElementById('button');
  button.addEventListener('click', handleClick);

  // Cleanup
  return function cleanup() {
    button.removeEventListener('click', handleClick);
  };
}

Regel: Nur referenzieren was du brauchst

function createProcessor(data) {
  // ❌ Closure hält ALLE data
  return function() {
    return data.filter(item => item.active);
  };

  // ✅ Closure hält nur was nötig ist
  const activeIds = data.filter(item => item.active).map(item => item.id);
  return function() {
    return activeIds;
  };
}

Debugging von Closures

Chrome DevTools

function outer() {
  const secret = 'Hidden Value';

  function inner() {
    debugger; // Breakpoint setzen
    console.log(secret);
  }

  return inner;
}

const fn = outer();
fn();

// In Chrome DevTools:
// 1. Execution pausiert bei 'debugger'
// 2. Scope Panel zeigt:
//    - Local (inner function scope)
//    - Closure (outer function scope mit 'secret')
//    - Global

Console Tricks

function createCounter() {
  let count = 0;

  return {
    increment: function() {
      count++;
      return count;
    },
    debug: function() {
      console.log('Current count:', count);
      console.log('Closure variables:', { count });
    }
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
counter.debug(); // Zeigt interne State

Best Practices

1. Benenne Funktionen für besseres Debugging

// ❌ Anonyme Funktion
const counter = (function() {
  let count = 0;
  return function() { return ++count; };
})();

// ✅ Benannte Funktionen
const counter2 = (function createCounter() {
  let count = 0;
  return function increment() {
    return ++count;
  };
})();

// In Stack Trace siehst du "increment" statt "anonymous"

2. Vermeide Closures in Loops (wenn nicht nötig)

// ❌ Erstellt unnötige Closures
const buttons = Array.from(document.querySelectorAll('button'));
buttons.forEach((button, index) => {
  button.addEventListener('click', function() {
    console.log('Button', index, 'clicked');
  });
});

// ✅ Besser: data-attribute
buttons.forEach((button, index) => {
  button.dataset.index = index;
  button.addEventListener('click', handleButtonClick);
});

function handleButtonClick() {
  console.log('Button', this.dataset.index, 'clicked');
}

3. Dokumentiere komplexe Closures

/**
 * Erstellt einen Rate Limiter mit Closure-based State
 * @param {Function} fn - Die zu limitierende Funktion
 * @param {number} limit - Max Aufrufe pro Zeitfenster
 * @param {number} window - Zeitfenster in ms
 */
function createRateLimiter(fn, limit, window) {
  let calls = []; // Closure: Speichert Timestamps

  return function(...args) {
    const now = Date.now();
    calls = calls.filter(time => now - time < window);

    if (calls.length < limit) {
      calls.push(now);
      return fn.apply(this, args);
    }

    throw new Error('Rate limit exceeded');
  };
}

Fazit

Scopes und Closures zu verstehen ist fundamental für JavaScript:

Scopes

  1. Block Scope mit let/const (bevorzugt)
  2. Function Scope mit var (vermeiden)
  3. Global Scope (sparsam nutzen)
  4. Scope Chain bestimmt Variable Lookup

Closures

  1. Funktion + Outer Scope = Closure
  2. Private Variables durch Closures
  3. Factory Functions für Konfiguration
  4. Module Pattern für Encapsulation
  5. React Hooks nutzen Closures intensiv

Vorteile

  • Data Encapsulation
  • Private State
  • Memoization/Caching
  • Flexible APIs
  • Functional Programming

Mein Code wurde durch Closures:

  • 50% lesbarer (klare Abstraktion)
  • 30% weniger Bugs (private State)
  • Wartbarer (gekapselte Logik)

Weiterführende Ressourcen


Wie nutzt du Closures in deinem Code? Teile deine Use Cases!