Kodokon kodokon.com

Iteratoren, Generatoren und Iterationsprotokolle

Meistere die Protokolle hinter for...of, Spread und Destrukturierung und nutze dann Generatoren, um lazy Sequenzen zu erzeugen.

9 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Zwei unterschiedliche Protokolle regeln die Iteration. Ein Objekt ist iterierbar, wenn es eine Methode unter dem Schlüssel Symbol.iterator bereitstellt; diese Methode muss einen Iterator zurückgeben, also ein Objekt mit einer next()-Methode, die { value, done } liefert. Alles, was Sequenzen konsumiert - for...of, der Spread-Operator ..., Destrukturierung, Array.from, Promise.all - läuft über diese Protokolle. Du kannst also jedes beliebige Objekt kompatibel machen.

JAVASCRIPT
const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
};

console.log([...range]); // [1, 2, 3]
Ein von Hand geschriebenes Iterable, ohne Klasse oder Generator.

Iteratoren von Hand zu schreiben ist umständlich. Generatoren (function*) erzeugen automatisch einen Iterator: Jedes yield pausiert die Ausführung und bewahrt den lokalen Zustand der Funktion. Diese verzögerte Auswertung (lazy evaluation) erlaubt dir, unendliche Sequenzen zu modellieren, solange ein Konsument die Anzahl der angeforderten Werte begrenzt.

JAVASCRIPT
function* naturals() {
  let n = 0;
  while (true) yield n++;
}

function* take(iterable, count) {
  let index = 0;
  for (const value of iterable) {
    if (index >= count) return;
    yield value;
    index += 1;
  }
}

console.log([...take(naturals(), 4)]); // [0, 1, 2, 3]
Eine unendliche Sequenz, konsumiert, ohne je zu blockieren.

Generator-Iteratoren stellen außerdem return() und throw() bereit. Ein oft übersehenes Detail der Spezifikation: Wenn du eine for...of-Schleife mit break, return oder einer Ausnahme verlässt, ruft die Engine automatisch iterator.return() auf. In einem Generator führt das alle ausstehenden finally-Blöcke aus - der ideale Ort, um eine Ressource freizugeben (eine Datei, eine Verbindung, einen Lock).

JAVASCRIPT
function* readLines() {
  try {
    yield "line 1";
    yield "line 2";
  } finally {
    console.log("cleanup");
  }
}

for (const line of readLines()) {
  console.log(line);
  break; // triggers return(), hence the finally
}
// line 1, cleanup
break ruft return() auf: Der finally-Block läuft.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Was macht ein Objekt im Sinne des Protokolls iterierbar?
    • Eine length-Eigenschaft und numerische Indizes zu haben
    • Eine Symbol.iterator-Methode bereitzustellen, die ein Objekt mit next() zurückgibt
    • Von Array.prototype zu erben
    • Direkt eine next()-Methode zu haben
  2. Was passiert, wenn du eine for...of-Schleife mit break verlässt, während sie einen Generator konsumiert?
    • Nichts Besonderes, der Generator bleibt beim letzten yield pausiert
    • Die Engine ruft throw() auf dem Iterator auf
    • Die Engine ruft return() auf, was die finally-Blöcke des Generators ausführt
    • Der Generator startet bei der nächsten Iteration von vorn
  3. Warum kannst du mit einem unendlichen Generator arbeiten, ohne das Programm zu blockieren?
    • Die Engine begrenzt automatisch die Anzahl der Iterationen
    • Generatoren laufen in einem separaten Thread
    • Die Auswertung ist lazy: Jeder Wert wird erst berechnet, wenn next() aufgerufen wird
    • Die Werte werden von der Engine vorab berechnet und zwischengespeichert