Iterator Helpers: map(), filter(), take() direkt auf Iteratoren
AI generated
JS
() =>
JavaScript · Sprachfeatures · Performance
Iteratoren transformieren, ohne Zwischenarrays:
die neuen Iterator Helpers

map(), filter(), take(), drop() und reduce() gibt es jetzt direkt auf Iteratoren, mit Lazy Evaluation statt vollstaendiger Materialisierung als Array.

17 Min. Lesezeit ES2025 Lazy Evaluation Generatoren

1. Das Problem: Iterables sind keine Arrays

JavaScript kennt seit ES2015 das Iterator-Protokoll, ueber das Generatoren, Maps, Sets und viele weitere Objekte durchlaufen werden koennen. Doch bis vor Kurzem fehlten den Iteratoren selbst die komfortablen Transformationsmethoden, die Arrays laengst hatten. Wer einen Generator mit map() transformieren wollte, musste ihn zuerst per Array.from() vollstaendig in ein Array umwandeln, erst danach standen map(), filter() und Co. zur Verfuegung.

Dieser Umweg ist mehr als nur unbequem. Er zwingt die Engine, saemtliche Elemente eines Iterators sofort zu erzeugen und im Speicher zu halten, selbst wenn am Ende nur die ersten drei Ergebnisse gebraucht werden oder der Iterator theoretisch unendlich ist. Ein Generator, der Primzahlen ohne Obergrenze erzeugt, laesst sich mit Array.from() schlicht nicht in ein Array verwandeln, das Programm haengt sich in einer Endlosschleife auf. Die neuen Iterator Helpers loesen genau dieses Problem.

2. Grundprinzip: Iterator Helpers direkt auf dem Iterator

Seit der Einfuehrung der Iterator Helpers besitzt jedes Objekt, das dem Iterator-Protokoll folgt und von Iterator als Basisklasse erbt (Generatoren tun das automatisch), Methoden wie .map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), .toArray() und weitere direkt auf sich selbst. Der entscheidende Unterschied zu den Array-Methoden: .map() und .filter() auf einem Iterator geben nicht sofort ein Ergebnis zurueck, sondern selbst wieder einen neuen, lazy Iterator.

Erst wenn dieser Ergebnis-Iterator tatsaechlich konsumiert wird, per for...of-Schleife, per .next()-Aufruf oder per einer terminalen Methode wie .toArray() oder .reduce(), werden die Transformationen tatsaechlich ausgefuehrt, und zwar Element fuer Element statt in separaten Durchlaeufen. Diese Verkettung ohne Zwischenmaterialisierung ist der Kern des Performance- und Speichervorteils gegenueber der klassischen Array-Kette.


function* zahlenGenerator() {
  let n = 1;
  while (true) yield n++;
}

// Lazy Kette: kein Zwischenarray, keine vollstaendige Auswertung
const ergebnis = zahlenGenerator()
  .map(n => n * n)
  .filter(n => n % 2 === 0)
  .take(5)
  .toArray();

console.log(ergebnis); // [4, 16, 36, 64, 100]

3. Lazy Evaluation im Vergleich zu Array.from() plus map/filter

Die klassische Vorgehensweise Array.from(iterator).map(fn).filter(fn2) ist eager, das heisst jeder Schritt der Kette erzeugt ein vollstaendiges neues Zwischenarray, bevor der naechste Schritt beginnt. Bei drei verketteten Operationen auf tausend Elementen bedeutet das drei vollstaendige Durchlaeufe und mindestens zwei komplette Zwischenarrays im Speicher, selbst wenn am Ende nur zehn Elemente tatsaechlich gebraucht werden.

Die Iterator-Helper-Kette dagegen verarbeitet jedes Element einzeln durch die gesamte Pipeline, bevor das naechste Element ueberhaupt angefordert wird. Bei .take(5) am Ende der Kette bedeutet das: sobald fuenf passende Elemente gefunden wurden, stoppt die gesamte Verarbeitung sofort, auch wenn der zugrunde liegende Iterator theoretisch noch Millionen weiterer Werte liefern koennte. Kein Zwischenarray wird angelegt, jedes Element durchlaeuft map und filter genau einmal und wird danach sofort wieder freigegeben.


// Eager: Array.from materialisiert SOFORT das komplette Array
// function* endlos() { let n = 1; while (true) yield n++; }
// Array.from(endlos()) -- haengt sich auf, niemals fertig!

// Lazy: take() begrenzt die Verarbeitung, bevor sie ausufert
function* endlos() {
  let n = 1;
  while (true) yield n++;
}

const ersteFuenfQuadratzahlen = endlos()
  .map(n => n * n)
  .take(5)
  .toArray();

console.log(ersteFuenfQuadratzahlen); // [1, 4, 9, 16, 25]

4. drop(), flatMap() und reduce() als weitere Bausteine

Neben map(), filter() und take() gehoert drop(n) zum Standardrepertoire: es ueberspringt die ersten n Elemente lazy, ohne sie zu materialisieren, ideal fuer Paginierung ueber potenziell grosse Datenstroeme. flatMap() erlaubt es, jedes Element in null, eins oder mehrere neue Elemente aufzufaechern, etwa um verschachtelte Datenstrukturen waehrend des Durchlaufs flach zu klopfen, ohne vorher ein verschachteltes Array zu erzeugen.

reduce() und toArray() sind terminale Operationen, sie beenden die Lazy-Kette und liefern ein konkretes Ergebnis. reduce() verhaelt sich dabei exakt wie sein Array-Pendant, akkumuliert also Werte ueber die gesamte, potenziell endlose Sequenz, weshalb es bei unendlichen Iteratoren nur in Kombination mit einer vorherigen take()-Begrenzung sinnvoll ist. Ohne diese Begrenzung wuerde reduce() auf einem unendlichen Iterator schlicht nie terminieren.


function* seiten(daten) {
  for (const item of daten) yield item;
}

const grosseDatenmenge = seiten(Array.from({ length: 1000 }, (_, i) => i));

// Paginierung: Seite 3 bei Seitengroesse 10, ganz ohne Zwischenarray
const seite3 = grosseDatenmenge.drop(20).take(10).toArray();
console.log(seite3); // [20, 21, ..., 29]

// reduce als terminale Operation, mit vorheriger Begrenzung
const summeErsteZehn = seiten([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
  .filter(n => n % 2 === 0)
  .reduce((acc, n) => acc + n, 0);

console.log(summeErsteZehn); // 30 (2+4+6+8+10)

5. Performance bei grossen und unendlichen Sequenzen

Der Performance-Unterschied wird besonders bei grossen Datenmengen mit fruehem Abbruch sichtbar. Bei einer Million Elementen, von denen nur die ersten zehn nach einer Filterbedingung gebraucht werden, muss die eager Array-Kette theoretisch alle eine Million Elemente durch map() und filter() schleusen, bevor slice(0, 10) ueberhaupt greifen kann. Die lazy Iterator-Kette dagegen verarbeitet im guenstigsten Fall nur so viele Elemente, wie tatsaechlich bis zum Erreichen von zehn Treffern noetig sind, das koennen zwoelf sein oder auch nur zehn.

Bei Streams aus externen Quellen, etwa paginierten API-Antworten, Datei-Zeilen oder WebSocket-Nachrichten, die als Async-Iteratoren modelliert sind, ist der Speichervorteil noch deutlicher: Es muss niemals der komplette Datenstrom im Arbeitsspeicher gehalten werden, jedes Element wird verarbeitet und danach vergessen. Fuer Server-Anwendungen, die grosse Datenmengen streamen, reduziert das den Speicherbedarf oft um Groessenordnungen im Vergleich zum vollstaendigen Puffern in einem Array.

6. Iterator Helpers auf Async-Iteratoren

Analog zu synchronen Iteratoren existiert AsyncIterator.prototype mit denselben Helper-Methoden fuer asynchrone Generatoren und Async-Iterables. Das ist besonders relevant fuer for await...of-Konsumenten, die Daten aus Netzwerkquellen, Datenbank-Cursorn oder Streams verarbeiten. Statt manuell eine Schleife zu schreiben, die jede Antwort einzeln prueft und filtert, laesst sich dieselbe deklarative Kette wie bei synchronen Iteratoren verwenden, nur dass jeder Schritt intern ein Promise zurueckgibt.

Das macht asynchrone Datenverarbeitungspipelines erheblich lesbarer: Eine Kette aus .filter(), .map() und .take() auf einem asynchronen Generator, der Datenbank-Zeilen liefert, liest sich fast wie eine SQL-Query, waehrend die zugrunde liegende Ausfuehrung weiterhin Zeile fuer Zeile, ohne alles auf einmal in den Speicher zu laden, ablaeuft.


async function* zeilenAusApi(url) {
  let seite = 0;
  while (true) {
    const res = await fetch(`${url}?page=${seite++}`);
    const items = await res.json();
    if (items.length === 0) return;
    for (const item of items) yield item;
  }
}

async function main() {
  const ersteAktivenNutzer = await zeilenAusApi("/api/users")
    .filter(user => user.active)
    .map(user => user.name)
    .take(5)
    .toArray();

  console.log(ersteAktivenNutzer);
}

7. Eigene Iterator-Klassen mit Helper-Methoden ausstatten

Wer eine eigene Iterator-Klasse schreibt, kann von der neuen Basisklasse Iterator erben statt nur das rohe Iterator-Protokoll (next()-Methode) zu implementieren. Dadurch erben eigene Klassen automatisch alle Helper-Methoden wie map(), filter() und take(), ohne dass diese manuell nachgebaut werden muessten. Das ist ein deutlicher Fortschritt gegenueber frueher, wo jede Bibliothek mit eigenen Iterator-Typen entweder eine eigene Helper-Implementierung mitbringen oder Nutzer zu Array.from() zwingen musste.

Praktisch bedeutet das: Eine Klasse, die etwa Baumknoten in Tiefensuche durchlaeuft, muss lediglich extends Iterator und eine next()-Methode implementieren, und schon stehen .filter(node => node.hasChildren) oder .take(10) ohne zusaetzlichen Code zur Verfuegung. Bibliotheksautoren sparen sich dadurch erhebliche Mengen an Boilerplate-Code, den sie frueher fuer jede eigene Sequenz-Abstraktion neu schreiben mussten.

8. Browser-Support und Migrationsstrategie

Iterator Helpers sind Teil des ES2025-Standards. Chrome und Edge unterstuetzen sie seit Version 122, Node.js seit Version 22 nativ, Firefox und Safari sind zum Stand Mitte 2026 in aktuellen Versionen ebenfalls vollstaendig auf dem Stand. Fuer Projekte, die noch aeltere Browser bedienen muessen, bietet core-js eine vollstaendige Polyfill-Implementierung, die sowohl die synchrone als auch die asynchrone Variante nachruestet.

Fuer die Migration bestehender Codebasen lohnt sich der Blick auf Stellen, an denen Array.from(generator) direkt gefolgt von einer Kette aus map()/filter()/slice() steht. Diese lassen sich meist verlustfrei durch die direkte Iterator-Helper-Kette ersetzen, wobei slice(0, n) zu take(n) und slice(n) zu drop(n) wird. Wichtig: Nur wenn tatsaechlich ein Iterator oder Generator die Datenquelle ist, lohnt sich der Umstieg, bei bereits vorliegenden, kleinen, endlichen Arrays bringt er keinen messbaren Vorteil.

9. Vergleichstabelle: Array-Kette vs. Iterator-Helper-Kette

Die folgende Tabelle fasst die wichtigsten Unterschiede zwischen der klassischen, eager Array-Verarbeitung und der neuen, lazy Iterator-Helper-Kette zusammen, um bei der Wahl der richtigen Technik fuer den jeweiligen Anwendungsfall zu helfen.

Als Faustregel gilt: Bei bereits vollstaendig vorliegenden, kleinen Arrays bleibt die klassische Array-API die einfachste und gewohnteste Wahl. Sobald aber Generatoren, unendliche Sequenzen, Streams oder ein frueher Abbruch mittels take() im Spiel sind, spielen Iterator Helpers ihre Staerken bei Speicherverbrauch und Performance voll aus.

Eigenschaft Array.from() + map/filter Iterator Helpers Praxis-Auswirkung
Auswertung Eager, sofort vollstaendig Lazy, Element fuer Element Fruehzeitiger Abbruch moeglich
Zwischenspeicher Volles Array pro Schritt Keine Zwischenarrays Geringerer Speicherbedarf
Unendliche Sequenzen Nicht moeglich (Endlosschleife) Problemlos mit take() Neue Anwendungsfaelle
Terminale Operation noetig Nein, direkt ein Array Ja, toArray()/reduce()/for...of Kette bleibt bis zum Ende lazy

Mironsoft

Moderne Browser-APIs, Performance und wartbares JavaScript

JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?

Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.

Code-Review

Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.

Performance-Optimierung

Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.

Modernisierung

Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.

10. Zusammenfassung

Iterator Helpers auf einen Blick

map() / filter()

Geben auf Iteratoren selbst wieder einen lazy Iterator zurueck, keine sofortige Ausfuehrung.

take() / drop()

Begrenzen bzw. ueberspringen Elemente lazy, essenziell fuer unendliche Sequenzen und Paginierung.

toArray() / reduce()

Terminale Operationen, die die Lazy-Kette tatsaechlich ausloesen und ein konkretes Ergebnis liefern.

Async-Variante

AsyncIterator.prototype bietet dieselben Methoden fuer for await...of-Datenquellen wie Streams und APIs.

11. FAQ: Iterator Helpers auf einen Blick

1Muss ich Array.from() jetzt komplett aus meinem Code entfernen?
Nein, fuer bereits vollstaendige, kleine Arrays bleibt Array.from() weiterhin sinnvoll. Iterator Helpers lohnen sich vor allem bei Generatoren, unendlichen Sequenzen und fruehem Abbruch mittels take().
2Was passiert, wenn ich reduce() auf einem unendlichen Iterator ohne vorheriges take() aufrufe?
Das Programm terminiert nie, da reduce() den gesamten Iterator konsumieren muss, um ein Ergebnis zu liefern. Bei unendlichen Sequenzen ist eine vorherige take()-Begrenzung zwingend.
3Sind Iterator Helpers auf normalen Arrays direkt nutzbar?
Nicht direkt, Arrays haben weiterhin ihre eigenen map/filter-Methoden. Man kann aber array.values() aufrufen, um einen Array-Iterator zu erhalten, auf dem dann die Iterator-Helper-Methoden verfuegbar sind.
4Wie unterscheidet sich take() von slice(0, n) bei Arrays?
Funktional aehnlich, aber take() arbeitet lazy auf dem Iterator und stoppt die gesamte vorgeschaltete Pipeline sofort nach n Elementen, waehrend slice() ein bereits vollstaendig materialisiertes Array voraussetzt.
5Kann ich mehrere filter()- und map()-Aufrufe beliebig verketten?
Ja, beliebig viele Aufrufe lassen sich verketten, da jeder Schritt wieder einen Iterator zurueckgibt. Erst die abschliessende terminale Operation loest die tatsaechliche Verarbeitung aus.
6Funktionieren Iterator Helpers auch mit Map- und Set-Objekten?
Ja, Map.prototype.entries()/keys()/values() und die entsprechenden Set-Methoden liefern Iteratoren, auf denen die Helper-Methoden direkt verfuegbar sind.
7Was ist der Unterschied zwischen flatMap() und map() bei Iterator Helpers?
map() erzeugt fuer jedes Eingabeelement genau ein Ausgabeelement, flatMap() kann fuer jedes Eingabeelement null, eins oder mehrere Ausgabeelemente liefern, indem die Callback-Funktion selbst ein Iterable zurueckgibt.
8Muss ich fuer AsyncIterator Helpers ein anderes Polyfill verwenden?
core-js deckt sowohl synchrone Iterator Helpers als auch die AsyncIterator-Variante ab, es ist also kein separates Paket noetig.
9Verlangsamt die Lazy-Auswertung kleine, endliche Ketten spuerbar?
Bei sehr kleinen Datenmengen ist der Unterschied meist vernachlaessigbar, teils sogar minimal langsamer wegen des internen Overheads pro Iterator-Objekt. Der Vorteil zeigt sich vor allem bei grossen oder unendlichen Sequenzen mit fruehem Abbruch.
10Kann ich eine eigene Klasse einfach durch extends Iterator zur Iterator-Helper-faehig machen?
Ja, sobald eine Klasse von Iterator erbt und next() implementiert, stehen alle Helper-Methoden wie map(), filter() und take() automatisch zur Verfuegung, ohne dass man sie selbst nachbauen muss.