JavaScript/Iterator

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Ein Iterator-Objekt ist ein Baustein des iterator/iterable Protokolls, das in ECMAScript 2015 eingeführt wurde.

Iterierbare Objekte enthalten eine Menge von Werten. Um diese Menge Element für Element verarbeiten zu können, stellen sie ein Iterator-Objekt zur Verfügung.

Einführung[Bearbeiten]

Grundsätzlich sind iterierbare Objekte nichts Neues, es gibt sie schon lange. Arrays sind iterierbar, die Eigenschaften eines Objektes ebenfalls. Das Problem war nur, dass es keine einheitliche Schnittstelle dafür gab. Das in ECMAScript 2015 eingeführte iterable-Protokoll ist eine Voraussetzung für die for..of Schleife und den Spread-Operator.

Damit ein Objekt iterierbar ist (oder: das iterable-Protokoll befolgt), muss es die @@iterator Methode bereitstellen. @@iterator ist ein Symbol, das vom Symbol Objekt unter der Eigenschaft Symbol.iterator bereitgestellt wird. Das Objekt kann die Methode selbst implementieren, oder über seine Prototypenkette erben.

Wer iterieren möchte, ruft diese Methode auf und benutzt das erhaltene Iterator-Objekt. Solche Objekte befolgen das iterator Protokoll. Sie definieren dafür eine Methode next(), die pro Iterationsschritt einmal aufzurufen ist und ein Objekt mit zwei Eigenschaften zurückgibt:

done
Ein boolescher Wert, der angibt, ob das Ende der Aufzählung erreicht ist. true gibt an, dass die Iteration zu Ende ist und kein weiteres Element mehr zur Verfügung steht.
value
Einer der im iterierbaren Objekt gespeicherten Werte. Das Vorhandensein von value ist nur garantiert, wenn done den Wert false hat.
Beachten Sie: Ein iterierbares Objekt stellt keine einheitliche Methode zur Verfügung, um die Anzahl der im Objekt gespeicherten Elemente zu ermitteln. Das iterable-Protokoll befasst sich ausschließlich mit dem sequenziellen Durchlaufen dieser Elemente.
Verwendung eines iterierbaren Objekts
let liste = [ "Deutschland", "Frankreich", "Belgien", "Niederlande" ];

for (land of liste) {
   console.log(land);
}

Wie jetzt – wo ist der Iterator? Dieses Beispiel zeigt ein iterierbares Objekt (ein Array), das mit Hilfe der for..of Schleife durchlaufen wird. Dazu verwendet for..of den vom Array bereitgestellten Iterator.

Einen vorhandenen Iterator benutzen[Bearbeiten]

Bauen wir das, was for..of tut, einmal nach:

Ein iterierbares Objekt selbst iterieren
let liste = [ "Deutschland", "Frankreich", "Belgien", "Niederlande" ];
let iterator = liste[Symbol.iterator]();
while (true) {
   let entry = iterator.next();
   if (entry.done) break;
   console.log(entry.value);
}
Beachten Sie: [Symbol.iterator] ist eine Methode. Die Klammern dahinter sind unbedingt erforderlich.

Die Verwendung von for..of ist deutlich bequemer. Es kann aber Fälle geben, wo spezielleres Verhalten beim Durchlaufen eines iterable-Objekts gewünscht ist, und man die Iteration von Hand durchführen möchte. Beispielsweise, wenn man mehrere iterierbare Objekte parallel durchlaufen will.

Einen Iterator selbst erstellen[Bearbeiten]

Schauen wir nun unter die Motorhaube und bauen einen eigenen Iterator für das Länder-Array. Er unterscheidet sich vom Standard-Iterator eines Arrays in zwei Aspekten. Zum einen überspringt er leere Einträge im Array, und zum anderen liefert er Index und Wert des Array-Elements.

Ein eigener Iterator
let liste = [ "Belgien", "Deutschland", "Frankreich", "Niederlande" ];
liste[9] = "Österreich";
liste[Symbol.iterator] = function() {
   let index = -1;
   let arr = this;
   return {
      next() {
         while (true) {
            index++;
            if (index >= arr.length) return { done: true };
            if (arr[index] !== undefined) break;
         }
         return {
            done: false,
            value: { index: index, entry: arr[index] }
         };
      }
   }
}

for (land of liste) {
   console.log(land);
}

Wenn man dieses Programm laufen lässt, findet man als Ergebnis eine Liste von Objekten mit Index und Wert der gefüllten Array-Einträge.

   {index: 0, entry: "Deutschland"}
   {index: 1, entry: "Frankreich"}
   {index: 2, entry: "Belgien"}
   {index: 3, entry: "Niederlande"}
   {index: 9, entry: "Österreich"}

Wo JavaScript Iteratoren erwartet[Bearbeiten]

Wir haben mit for..of bereits eine Stelle gesehen, wo ein iterierbares Objekt erwartet wird. Eine andere Stelle ist der Spread-Operator ..., der ein iterierbares Objekt in seine einzelnen Werte zerlegt. Ebenso unterstützten viele Methoden iterables als Datenquelle, z. B. Array.from oder Promise.all.

Generatoren[Bearbeiten]

Es ist relativ mühsam, einen Iterator von Hand zu schreiben. Das liegt vor allem daran, dass man dafür eine invertierte Programmlogik benutzen muss, d. h. an der Stelle, wo der nächste Wert der Iteration verfügbar ist, muss man sich den aktuellen Zustand merken, die next() Funktion verlassen und beim nächsten Aufruf an der gemerkten Position fortfahren. Diese Aufgabe wird durch einen Generator deutlich vereinfacht.

Verwendung der Generatorsyntax für einen Iterator
let liste = [ "Belgien", "Deutschland", "Frankreich", "Niederlande" ];
liste[9] = "Österreich";
liste[Symbol.iterator] = function*() {
   for (let i=0; i<this.length; i++) {
      if (this[i] !== undefined)
         yield { index: i, entry: this[i] }
   }
}

let iterator = liste[Symbol.iterator]();
while (true) {
   let entry = iterator.next();
   if (entry.done) break;
   console.log(entry.value);
}

Das Beispiel verwendet bewusst nicht for..of, sondern iteriert von Hand, damit Sie sehen, dass hier tatsächlich ein Iterator-Objekt entsteht.

JavaScript erkennt den Unterschied zwischen Funktion und Generator an dem Sternchen hinter dem function Schlüsselwort. Das ganze Brimborium mit next(), done und value wird nun intern erzeugt, und Sie können ganz so tun, als würden Sie einen klassischen prozeduralen Ablauf programmieren. An Stelle von return schreiben Sie yield. In dem Moment, wo der yield-Befehl ausgeführt wird, greift JavaScript ein. Es speichert den Zustand der Funktion intern ab und beendet sie. Sobald der Konsument der Iteration das nächste Mal next() aufruft, wird der gespeicherte Zustand wiederhergestellt und die Verarbeitung im Generator dort fortgesetzt, wo sie unterbrochen wurde. Wenn die Generatorfunktion endet, erzeugt JavaScript das Iterationsobjekt, in dem done auf true gesetzt ist.

Beachten Sie dabei, dass yield kein Statement ist, sondern ein unärer Operator in einem Ausdruck. Als solcher liefert es sogar einen Wert zurück, nämlich den Parameter, den der Konsument des erzeugten Iterables an die next-Funktion übergibt. Ein mögliches Anwendungsbeispiel dafür wäre eine Generatorfunktion, die das DOM durchläuft. Der Parameter für next() könnte festlegen, ob die Kind-Elemente des zuletzt von yield bereitgestellten Elements ebenfalls durchlaufen werden sollen oder nicht. Ob sich auf diese Weise lesbarer und verständlicher Code ergibt, ist im Einzelfall zu prüfen.

Außer yield gibt es auch noch yield*. Damit delegieren Sie die Iteration an einen anderen Generator. Ein Anwendungsfall dafür wäre zum Beispiel das zuvor erwähnte Durchlaufen des DOM. Dies ist eine rekursive Aufgabe, und man kann sie mit einem rekursiven Generator lösen. Das folgende Beispiel findet alle Kommentare im Dokument:

Ein rekursiver Generator mit yield*
function* getCommentNodes(parentNode) {
   for (let childNode of parentNode.childNodes) {
      if (childNode.nodeType == Node.COMMENT_NODE) {
         yield childNode;
      }
      else if (childNode.hasChildNodes) {
         yield* getCommentNodes(childNode);
      }
   }
}

for (let commentNode of document) {
  console.log("Comment: " + commentNode.textContent);
}

yield* ist ebenfalls ein Operator, kein Statement. Er liefert den Rückgabewert der aufgerufenen Generatorfunktion.

Sie können sich die Arbeitsweise von yield* so vorstellen, dass JavaScript an dieser Stelle eine for..of Schleife einsetzt, die das Iterable durchläuft, das an yield* als parameter übergeben wurde, und jeden einzelnen gelieferten Wert mittels yield bereitstellt. Es gibt aber keine Situation, wo Sie eine solche Funktion wirklich benötigen. Browser, die yield unterstützen, unterstützen auch yield* (von den obsoleten Firefox Versionen 26 bis 32 abgesehen).

Simulation von yield*
function* simulateYield(iterable) {
   let iterationStep = iterable.next();
   while (!iterationStep.done)
      yield iterationStep.value;
   }
   // für done=true enthält value den Rückgabewert der Generatorfunktion
   return iterationStep.value;
}

Generatoren machen ein Konzept verfügbar, das aus anderen Programmiersprachen als Koroutine bekannt ist. Zwei Programmstücke – Routinen – laufen parallel bzw. quasiparallel und spielen sich gegenseitig den Ball zu, wer fortsetzen darf. Viele Sprachen (z. B. Python) setzen das durch Multithreading um, d. h. Generator und Konsument laufen echt parallel. JavaScript tut das nicht, es verwendet eine Simulationstechnik, um Koroutinen in einem einzigen Thread auszuführen.

ToDo (weitere ToDos)

Artikel muss qualitätsgesichert werden.Rolf b (Diskussion) 23:07, 10. Okt. 2020 (CEST)

Siehe auch[Bearbeiten]

Weblinks[Bearbeiten]