JavaScript/Iterator
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.
Inhaltsverzeichnis
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, wenndone
den Wertfalse
hat.
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:
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);
}
[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.
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.
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:
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).
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)