Iteratoren und Generatoren

Iteratoren und Generatoren bringen das Konzept der Iteration direkt in die Kernsprache und bieten einen Mechanismus zur Anpassung des Verhaltens von for...of-Schleifen.

Für Details siehe auch:

Iteratoren

In JavaScript ist ein Iterator ein Objekt, das eine Sequenz definiert und möglicherweise einen Rückgabewert bei seiner Beendigung bereitstellt.

Genauer gesagt ist ein Iterator jedes Objekt, das das Iterator-Protokoll implementiert, indem es eine next()-Methode hat, die ein Objekt mit zwei Eigenschaften zurückgibt:

value

Der nächste Wert in der Iterationssequenz.

done

Dies ist true, wenn der letzte Wert in der Sequenz bereits konsumiert wurde. Wenn value zusammen mit done vorhanden ist, ist es der Rückgabewert des Iterators.

Sobald ein Iterator-Objekt erstellt ist, kann es durch wiederholtes Aufrufen von next() explizit durchlaufen werden. Das Durchlaufen eines Iterators wird als Verbrauchen des Iterators bezeichnet, da es in der Regel nur einmal möglich ist. Nachdem ein Endwert bereitgestellt wurde, sollten zusätzliche Aufrufe von next() weiterhin {done: true} zurückgeben.

Der häufigste Iterator in JavaScript ist der Array-Iterator, der jeden Wert im zugehörigen Array der Reihe nach zurückgibt.

Obwohl es leicht vorstellbar ist, dass alle Iteratoren als Arrays ausgedrückt werden könnten, ist dies nicht der Fall. Arrays müssen in ihrer Gesamtheit zugewiesen werden, aber Iteratoren werden nur bei Bedarf konsumiert. Aus diesem Grund können Iteratoren Sequenzen unbegrenzter Größe ausdrücken, wie z.B. die Menge der Ganzzahlen zwischen 0 und Infinity.

Hier ist ein Beispiel, das genau das tun kann. Es ermöglicht die Erstellung eines Bereichsiterators, der eine Sequenz von Ganzzahlen von start (einschließlich) bis end (ausschließlich) in Intervallen von step definiert. Der abschließende Rückgabewert ist die Größe der erstellten Sequenz, die durch die Variable iterationCount verfolgt wird.

js
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
}

Die Verwendung des Iterators sieht dann so aus:

js
const iter = makeRangeIterator(1, 10, 2);

let result = iter.next();
while (!result.done) {
  console.log(result.value); // 1 3 5 7 9
  result = iter.next();
}

console.log("Iterated over sequence of size:", result.value); // [5 numbers returned, that took interval in between: 0 to 10]

Hinweis: Es ist nicht möglich, reflektiv zu erkennen, ob ein bestimmtes Objekt ein Iterator ist. Wenn Sie dies tun müssen, verwenden Sie Iterables.

Generatorfunktionen

Während benutzerdefinierte Iteratoren ein nützliches Werkzeug sind, erfordert ihre Erstellung sorgfältige Programmierung aufgrund der Notwendigkeit, ihren internen Zustand explizit zu pflegen. Generatorfunktionen bieten eine leistungsstarke Alternative: Sie ermöglichen es, einen iterativen Algorithmus durch das Schreiben einer einzigen Funktion zu definieren, deren Ausführung nicht kontinuierlich ist. Generatorfunktionen werden unter Verwendung der function*-Syntax geschrieben.

Wenn sie aufgerufen werden, führen Generatorfunktionen ihren Code zunächst nicht aus. Stattdessen geben sie eine spezielle Art von Iterator zurück, der Generator genannt wird. Wenn ein Wert durch Aufrufen der next-Methode des Generators konsumiert wird, wird die Generatorfunktion ausgeführt, bis sie auf das yield-Schlüsselwort trifft.

Die Funktion kann beliebig oft aufgerufen werden und gibt jedes Mal einen neuen Generator zurück. Jeder Generator kann nur einmal durchlaufen werden.

Wir können nun das obige Beispiel anpassen. Das Verhalten dieses Codes ist identisch, aber die Implementierung ist viel einfacher zu schreiben und zu lesen.

js
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let iterationCount = 0;
  for (let i = start; i < end; i += step) {
    iterationCount++;
    yield i;
  }
  return iterationCount;
}

Iterables

Ein Objekt ist iterable, wenn es sein Iterationsverhalten definiert, zum Beispiel, welche Werte in einer for...of-Schleife durchlaufen werden. Einige eingebaute Typen, wie Array oder Map, haben ein Standarditerationsverhalten, während andere Typen (wie Object) dies nicht tun.

Um iterable zu sein, muss ein Objekt die Methode [Symbol.iterator]() implementieren. Das bedeutet, dass das Objekt (oder eines der Objekte in seiner Prototypkette) eine Eigenschaft mit einem Symbol.iterator-Schlüssel haben muss.

Es kann möglich sein, über ein Iterable mehr als einmal oder nur einmal zu iterieren. Es liegt in der Verantwortung des Programmierers zu wissen, welcher Fall zutrifft.

Iterables, die nur einmal durchlaufen werden können (wie Generatoren), geben normalerweise this aus ihrer [Symbol.iterator]()-Methode zurück, während Iterables, die viele Male iteriert werden können, bei jedem Aufruf von [Symbol.iterator]() einen neuen Iterator zurückgeben müssen.

js
function* makeIterator() {
  yield 1;
  yield 2;
}

const iter = makeIterator();

for (const itItem of iter) {
  console.log(itItem);
}

console.log(iter[Symbol.iterator]() === iter); // true

// This example show us generator(iterator) is iterable object,
// which has the [Symbol.iterator]() method return the `iter` (itself),
// and consequently, the it object can iterate only _once_.

// If we change the [Symbol.iterator]() method of `iter` to a function/generator
// which returns a new iterator/generator object, `iter`
// can iterate many times

iter[Symbol.iterator] = function* () {
  yield 2;
  yield 1;
};

Benutzerdefinierte Iterables

Sie können Ihre eigenen Iterables auf diese Weise erstellen:

js
const myIterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
};

Benutzerdefinierte Iterables können wie gewohnt in for...of-Schleifen oder in der Spread-Syntax verwendet werden.

js
for (const value of myIterable) {
  console.log(value);
}
// 1
// 2
// 3

[...myIterable]; // [1, 2, 3]

Eingebaute Iterables

String, Array, TypedArray, Map und Set sind alle eingebaute Iterables, da ihre Prototypobjekte alle eine Symbol.iterator-Methode haben.

Syntaxen, die Iterables erwarten

Einige Anweisungen und Ausdrücke erwarten Iterables. Zum Beispiel: die for...of-Schleifen, Spread-Syntax, yield*, und Destrukturierung-Syntax.

js
for (const value of ["a", "b", "c"]) {
  console.log(value);
}
// "a"
// "b"
// "c"

[..."abc"];
// ["a", "b", "c"]

function* gen() {
  yield* ["a", "b", "c"];
}

gen().next();
// { value: "a", done: false }

[a, b, c] = new Set(["a", "b", "c"]);
a;
// "a"

Erweiterte Generatoren

Generatoren berechnen ihre yield-Werte bei Bedarf, was es ihnen ermöglicht, effizient Sequenzen darzustellen, die teuer zu berechnen sind (oder sogar unendliche Sequenzen, wie oben gezeigt).

Die next()-Methode akzeptiert auch einen Wert, der verwendet werden kann, um den internen Zustand des Generators zu ändern. Ein an next() übergebener Wert wird von yield empfangen.

Hinweis: Ein Wert, der an den ersten Aufruf von next() übergeben wird, wird immer ignoriert.

Hier ist der Fibonacci-Generator, der next(x) verwendet, um die Sequenz neu zu starten:

js
function* fibonacci() {
  let current = 0;
  let next = 1;
  while (true) {
    const reset = yield current;
    [current, next] = [next, next + current];
    if (reset) {
      current = 0;
      next = 1;
    }
  }
}

const sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2

Sie können einen Generator zwingen, eine Ausnahme zu werfen, indem Sie seine throw()-Methode aufrufen und den Ausnahmewert übergeben, den er werfen soll. Diese Ausnahme wird aus dem derzeit angehaltenen Kontext des Generators geworfen, als ob das derzeit angehaltene yield stattdessen eine throw value-Anweisung wäre.

Wenn die Ausnahme nicht innerhalb des Generators abgefangen wird, wird sie durch den Aufruf von throw() propagiert, und nachfolgende Aufrufe von next() führen dazu, dass die done-Eigenschaft true ist.

Generatoren haben eine return()-Methode, die den angegebenen Wert zurückgibt und den Generator selbst beendet.