Zum Inhalt springen
aviral gupta

// B4.5 · ca. 38 Min. · Einstieg

Projekt: eine Bibliothek mit Zählern und Formatierern aus Closures

Nach diesem Projekt schreiben Sie Funktionsfabriken: Zähler mit eigenem privaten Stand, Formatierer, die sich ihre Einstellungen merken, und kleine Funktionen, verbunden mit pipe.

Lektion 5 von 5 in B4 Funktionen

Ende des Moduls

Danach können Sie

  • Eine Fabrik wie makeCounter bauen, deren Funktionen einen privaten Zustand teilen, den jeder Zähler getrennt hält
  • Einstellbare Formatierer mit makeFormatter, toFixed und Template-Literalen bauen
  • Kleine Funktionen mit pipe zu größeren verbinden
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion B4.4: Was gibt dieser Code aus?

    function makeAdder(x) {
      return (y) => x + y;
    }
    const add2 = makeAdder(2);
    console.log(typeof add2, add2(5));
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen: count steht außerhalb der Fabrik. Was gibt dieser Code aus?

    let count = 0;
    function makeCounter() {
      return () => {
        count += 1;
        return count;
      };
    }
    const a = makeCounter();
    const b = makeCounter();
    a();
    a();
    console.log(b());
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie den Startwert, sodass makeCounter(10, 5) bei 10 beginnt und das erste increment 15 ergibt.

    function makeCounter(start = 0, step = 1) {
      let count = ____;
      const increment = () => {
        count += step;
        return count;
      };
      return {increment};
    }
    const c = makeCounter(10, 5);
    console.log(c.increment());
    let count = ;
  4. Üben · Aufgabe 4 von 7

    Ein Formatierer mit einer Nachkommastelle und einem %-Zeichen. Was gibt dieser Code aus?

    function makeFormatter(options = {}) {
      const prefix = options.prefix ?? "";
      const suffix = options.suffix ?? "";
      const decimals = options.decimals ?? 2;
      return (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`;
    }
    
    const percent = makeFormatter({suffix: "%", decimals: 1});
    console.log(percent(12.34));
  5. Üben · Aufgabe 5 von 7

    pipe führt seine zwei Funktionen der Reihe nach aus. Was gibt dieser Code aus?

    const pipe = (first, second) => (value) => second(first(value));
    const addOne = (n) => n + 1;
    const double = (n) => n * 2;
    console.log(pipe(addOne, double)(5), pipe(double, addOne)(5));
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Ein Zähler, der bei 10 beginnt und sich in Fünferschritten bewegt. Was gibt die letzte Zeile aus?

    function makeCounter(start = 0, step = 1) {
      let count = start;
      const increment = () => {
        count += step;
        return count;
      };
      const decrement = () => {
        count -= step;
        return count;
      };
      const value = () => count;
      const reset = () => {
        count = start;
        return count;
      };
      return {increment, decrement, value, reset};
    }
    
    const c = makeCounter(10, 5);
    c.increment();
    c.increment();
    c.decrement();
    console.log(c.value(), c.reset(), c.value());
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie makeIdGenerator(prefix), das eine Pfeilfunktion zurückgibt. Jeder Aufruf dieses Pfeils liefert die nächste Kennung: order-1, order-2 und so weiter. Erzeugen Sie zwei Generatoren mit verschiedenen Präfixen, rufen Sie sie gemischt auf und prüfen Sie, dass jeder für sich zählt. Versuchen Sie dann, next von außerhalb der Fabrik zu lesen.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Die Closure-Bibliothek

makeCounter gibt vier Funktionen zurück, die sich einen privaten Stand teilen; zwei Zähler behalten ihre eigenen Stände. makeFormatter liest seine Einstellungen einmal und gibt einen Formatierer zurück, der sie bei jedem Aufruf verwendet. pipe verbindet withVat und euro zu einem Preisschild. Führen Sie es aus, verschieben Sie dann let count = start; aus makeCounter heraus (als let count = 0; oben in der Datei) und beobachten Sie, wie sich die beiden Zähler plötzlich einen Stand teilen.

main.js

// A small closure library: counters, formatters and pipe.
function makeCounter(start = 0, step = 1) {
  let count = start;
  const increment = () => {
    count += step;
    return count;
  };
  const decrement = () => {
    count -= step;
    return count;
  };
  const value = () => count;
  const reset = () => {
    count = start;
    return count;
  };
  return {increment, decrement, value, reset};
}

function makeFormatter(options = {}) {
  const prefix = options.prefix ?? "";
  const suffix = options.suffix ?? "";
  const decimals = options.decimals ?? 2;
  return (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`;
}

const pipe = (first, second) => (value) => second(first(value));

const visits = makeCounter();
const tickets = makeCounter(100, 10);
visits.increment();
visits.increment();
tickets.increment();
tickets.increment();
tickets.decrement();
console.log("visits:", visits.value(), "tickets:", tickets.value());
console.log("after reset:", visits.reset(), tickets.reset());

const euro = makeFormatter({suffix: " EUR"});
const dollar = makeFormatter({prefix: "$"});
const percent = makeFormatter({suffix: "%", decimals: 1});
console.log(euro(4.5), dollar(19.9), percent(12.34));

const withVat = (net) => net * 1.19;
const priceTag = pipe(withVat, euro);
console.log(priceTag(10), priceTag(25));

Ausführen mit

node main.js

Ausgabe

visits: 2 tickets: 110
after reset: 0 100
4.50 EUR $19.90 12.3%
11.90 EUR 29.75 EUR
  • visits und tickets stammen aus derselben Fabrik, zählen aber getrennt: 2 und 110.
  • reset setzt jeden Zähler auf seinen eigenen Start zurück: 0 und 100.
  • toFixed füllt 4.5 zu 4.50 auf und rundet 12.34 auf 12.3; das Template-Literal fügt Präfix und Suffix hinzu.
  • priceTag(10) führt zuerst withVat aus, 11.9, dann euro, 11.90 EUR.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 3

Zähler mit eigenem Stand

makeCounter(start = 0, step = 1) soll {increment, decrement, value, reset} zurückgeben. Jeder Zähler beginnt bei start, bewegt sich um step, und reset geht auf start zurück und gibt den Wert zurück. Im Startcode teilen sich alle Zähler ein count oben in der Datei, und start und step werden ignoriert. Reparieren Sie das, sodass jeder Zähler seinen eigenen privaten Stand hält.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Der zweite Zähler macht dort weiter, wo der erste aufgehört hat. Wie viele count-Variablen gibt es?

  2. Hinweis 2

    Verschieben Sie die Deklaration von count in makeCounter und lassen Sie es bei start beginnen: let count = start;. Dann erzeugt jeder Aufruf sein eigenes.

  3. Hinweis 3

    Verwenden Sie step in increment und decrement und start in reset.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Each counter must keep its own count, starting at start and moving by step.
export function makeCounter(start = 0, step = 1) {
  let count = start;
  const increment = () => {
    count += step;
    return count;
  };
  const decrement = () => {
    count -= step;
    return count;
  };
  const value = () => count;
  const reset = () => {
    count = start;
    return count;
  };
  return {increment, decrement, value, reset};
}
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Each counter must keep its own count, starting at start and moving by step.
let count = 0;

export function makeCounter(start = 0, step = 1) {
  const increment = () => {
    count += 1;
    return count;
  };
  const decrement = () => {
    count -= 1;
    return count;
  };
  const value = () => count;
  const reset = () => {
    count = 0;
    return count;
  };
  return {increment, decrement, value, reset};
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {makeCounter} from './main.js';

test('zwei Zähler behalten ihren eigenen Stand', () => {
  const a = makeCounter();
  const b = makeCounter();
  a.increment();
  a.increment();
  b.increment();
  assert.equal(a.value(), 2, `nach zwei Erhöhungen ist a.value() ${a.value()}, erwartet 2`);
  assert.equal(b.value(), 1, `nach einer Erhöhung ist b.value() ${b.value()}, erwartet 1`);
});

test('ein Zähler beginnt bei start und bewegt sich um step', () => {
  const c = makeCounter(10, 5);
  assert.equal(c.value(), 10, `makeCounter(10, 5).value() ist ${c.value()}, erwartet 10`);
  const up = c.increment();
  assert.equal(up, 15, `das erste increment hat ${up} zurückgegeben, erwartet 15`);
});

test('decrement geht um step zurück', () => {
  const c = makeCounter(10, 5);
  c.decrement();
  const down = c.decrement();
  assert.equal(down, 0, `zwei decrement-Aufrufe von 10 um 5 haben ${down} zurückgegeben, erwartet 0`);
});

test('reset geht auf start zurück', () => {
  const c = makeCounter(3);
  c.increment();
  c.increment();
  const back = c.reset();
  assert.equal(back, 3, `reset() hat ${back} zurückgegeben, erwartet 3`);
  assert.equal(c.value(), 3, `nach reset() ist value() ${c.value()}, erwartet 3`);
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 2 von 3

Ein einstellbarer Formatierer

makeFormatter(options = {}) liest prefix, suffix und decimals bereits. Lassen Sie den zurückgegebenen Pfeil eine Zahl formatieren: zuerst das Präfix, dann die Zahl mit genau decimals Nachkommastellen, dann das Suffix. So ergibt makeFormatter({suffix: " EUR"})(4.5) den Wert "4.50 EUR" und makeFormatter({prefix: "$"})(19.9) den Wert "$19.90". Die Datei exportiert außerdem euro, erzeugt mit der Fabrik.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Der Formatierer gibt die Zahl unverändert zurück, ohne Präfix, Suffix oder feste Nachkommastellen.

  2. Hinweis 2

    amount.toFixed(decimals) liefert die Zahl als String mit genau so vielen Nachkommastellen.

  3. Hinweis 3

    Setzen Sie die drei Teile der Reihe nach in das Template-Literal: `${prefix}${amount.toFixed(decimals)}${suffix}`.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Return a function that formats a number with prefix, decimals and suffix.
export function makeFormatter(options = {}) {
  const prefix = options.prefix ?? "";
  const suffix = options.suffix ?? "";
  const decimals = options.decimals ?? 2;
  return (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`;
}

export const euro = makeFormatter({suffix: " EUR"});
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Return a function that formats a number with prefix, decimals and suffix.
export function makeFormatter(options = {}) {
  const prefix = options.prefix ?? "";
  const suffix = options.suffix ?? "";
  const decimals = options.decimals ?? 2;
  return (amount) => `${amount}`;
}

export const euro = makeFormatter({suffix: " EUR"});

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {makeFormatter, euro} from './main.js';

test('euro(4.5) ergibt 4.50 EUR', () => {
  assert.equal(euro(4.5), '4.50 EUR', `euro(4.5) hat ${JSON.stringify(euro(4.5))} zurückgegeben, erwartet "4.50 EUR"`);
});

test('ein Präfix steht vor der Zahl', () => {
  const dollar = makeFormatter({prefix: '$'});
  assert.equal(dollar(19.9), '$19.90', `der $-Formatierer hat ${JSON.stringify(dollar(19.9))} zurückgegeben, erwartet "$19.90"`);
});

test('decimals legt die Zahl der Nachkommastellen fest', () => {
  const percent = makeFormatter({suffix: '%', decimals: 1});
  assert.equal(percent(12.34), '12.3%', `percent(12.34) hat ${JSON.stringify(percent(12.34))} zurückgegeben, erwartet "12.3%"`);
});

test('ohne Optionen: zwei Nachkommastellen, nichts drumherum', () => {
  const plain = makeFormatter();
  assert.equal(plain(3), '3.00', `makeFormatter()(3) hat ${JSON.stringify(plain(3))} zurückgegeben, erwartet "3.00"`);
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 3 von 3

Ein Preisschild aus zwei kleinen Funktionen

pipe(first, second) soll eine Funktion zurückgeben, die first auf ihr Argument anwendet und dann second auf dieses Ergebnis. priceTag = pipe(withVat, euro) soll dann 19 % Mehrwertsteuer aufschlagen und formatieren: priceTag(10) ergibt "11.90 EUR". Der Startcode führt die beiden Funktionen in der falschen Reihenfolge aus. Reparieren Sie pipe; lassen Sie withVat, euro und priceTag, wie sie sind.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: pipe(addOne, double)(3) ergibt 7. Welche Funktion lief zuerst?

  2. Hinweis 2

    Der Wert muss in first gehen und das Ergebnis von first in second.

  3. Hinweis 3

    Vertauschen Sie die Aufrufe: (value) => second(first(value)).

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Return a function that runs first, then second on first's result.
export const pipe = (first, second) => (value) => second(first(value));

const withVat = (net) => net * 1.19;
const euro = (amount) => `${amount.toFixed(2)} EUR`;

// A price tag: add VAT, then format.
export const priceTag = pipe(withVat, euro);
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Return a function that runs first, then second on first's result.
export const pipe = (first, second) => (value) => first(second(value));

const withVat = (net) => net * 1.19;
const euro = (amount) => `${amount.toFixed(2)} EUR`;

// A price tag: add VAT, then format.
export const priceTag = pipe(withVat, euro);

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {pipe, priceTag} from './main.js';

const addOne = (n) => n + 1;
const double = (n) => n * 2;
const square = (n) => n * n;

test('pipe führt zuerst first, dann second aus', () => {
  const got = pipe(addOne, double)(3);
  assert.equal(got, 8, `pipe(addOne, double)(3) hat ${got} zurückgegeben, erwartet 8`);
});

test('priceTag(10) ergibt 11.90 EUR', () => {
  assert.equal(priceTag(10), '11.90 EUR', `priceTag(10) hat ${JSON.stringify(priceTag(10))} zurückgegeben, erwartet "11.90 EUR"`);
});

test('eine verbundene Funktion lässt sich erneut verbinden', () => {
  const got = pipe(pipe(addOne, double), square)(1);
  assert.equal(got, 16, `pipe(pipe(addOne, double), square)(1) hat ${got} zurückgegeben, erwartet 16`);
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Häufige Fehler

Die Fabrik nicht aufrufen

function makeCounter(start = 0) {
  let count = start;
  const increment = () => {
    count += 1;
    return count;
  };
  return {increment};
}

const counter = makeCounter;
console.log(counter.increment());

Was Node.js ausgibt

TypeError: counter.increment is not a function

Warum, und die Lösung

Ohne runde Klammern ist counter die Fabrik selbst, kein von ihr erzeugter Zähler. Die Fabrik hat kein eigenes increment, also ist counter.increment undefined, und der Aufruf schlägt fehl. Rufen Sie die Fabrik auf, um einen Zähler zu erzeugen: const counter = makeCounter();.

Den privaten Stand von außen lesen

function makeCounter(start = 0) {
  let count = start;
  const increment = () => {
    count += 1;
    return count;
  };
  return {increment};
}

const counter = makeCounter();
counter.increment();
console.log(count);

Was Node.js ausgibt

ReferenceError: count is not defined

Warum, und die Lösung

count lebt im Scope von makeCounter. Nur die darin erzeugten Funktionen erreichen es, die oberste Ebene der Datei nicht. Genau das hält es privat. Geben Sie dem Zähler eine Funktion, die es zurückgibt, etwa value: () => count, und rufen Sie counter.value() auf.

Einen String statt einer Zahl formatieren

const euro = (amount) => `${amount.toFixed(2)} EUR`;
console.log(euro("4.5"));

Was Node.js ausgibt

TypeError: amount.toFixed is not a function

Warum, und die Lösung

toFixed ist eine Methode von Zahlen. "4.5" ist ein String, der kein toFixed hat, also schlägt der Aufruf fehl. Übergeben Sie eine Zahl, euro(4.5), oder wandeln Sie Text zuerst mit Number("4.5") in eine Zahl um.

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Fabriken und privater Zustand

Eine Funktionsfabrik ist eine Funktion, die andere Funktionen erzeugt und zurückgibt. makeCounter(start = 0, step = 1) deklariert darin let count = start; und erzeugt dann vier Pfeile, increment, decrement, value und reset, die alle dasselbe count in ihrer Closure halten. Sie gibt sie zusammen als {increment, decrement, value, reset} zurück, kurz für {increment: increment, …}; aufgerufen werden sie als counter.increment(). Objekte behandelt Modul B5 ausführlich. Jeder Aufruf von makeCounter führt den Rumpf erneut aus, also bekommt jeder Zähler sein eigenes count. Von außen kann niemand count lesen oder ändern, außer über diese vier Funktionen: Die Closure hält es privat. Steht count außerhalb der Fabrik, teilen sich alle Zähler eines.

Formatierer: Einstellungen einmal festhalten

makeFormatter(options = {}) liest seine Einstellungen einmal: const decimals = options.decimals ?? 2; nimmt 2, wenn keine Nachkommastellen angegeben wurden, und prefix und suffix fallen auf "" zurück. Zurück gibt sie einen Pfeil, der sie bei jedem Aufruf verwendet: (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`. toFixed rundet die Zahl auf so viele Nachkommastellen und füllt mit Nullen auf, aus 4.5 wird "4.50"; das Ergebnis ist ein String. So sind euro = makeFormatter({suffix: " EUR"}) und percent = makeFormatter({suffix: "%", decimals: 1}) zwei Funktionen mit eigenen Einstellungen. toFixed gibt es nur bei Zahlen: euro("4.5") bricht mit TypeError: amount.toFixed is not a function ab.

Kleine Funktionen verbinden

Kleine Funktionen, die je eine Sache tun, lassen sich leicht testen und wiederverwenden. pipe verbindet zwei davon: const pipe = (first, second) => (value) => second(first(value));. Sie gibt eine neue Funktion zurück, die zuerst first ausführt und dessen Ergebnis an second weitergibt. Die Reihenfolge zählt: pipe(withVat, euro) schlägt die Mehrwertsteuer auf und formatiert dann, pipe(euro, withVat) würde dagegen versuchen, einen String zu multiplizieren. priceTag = pipe(withVat, euro) ist selbst eine Funktion und lässt sich erneut verbinden: pipe(pipe(addOne, double), square) führt drei Schritte aus. Auch pipe ist eine Closure: Der zurückgegebene Pfeil merkt sich first und second.

Quellen

Zuletzt geprüft am 3. Oktober 2026