Deutsch

Ein umfassender Leitfaden zum Verständnis und zur Implementierung des JavaScript Iterator-Protokolls, der Sie befähigt, benutzerdefinierte Iteratoren für eine verbesserte Datenverarbeitung zu erstellen.

Das JavaScript Iterator-Protokoll und benutzerdefinierte Iteratoren entmystifiziert

Das Iterator-Protokoll von JavaScript bietet eine standardisierte Methode zur Traversierung von Datenstrukturen. Das Verständnis dieses Protokolls befähigt Entwickler, effizient mit eingebauten Iterables wie Arrays und Strings zu arbeiten und ihre eigenen benutzerdefinierten Iterables zu erstellen, die auf spezifische Datenstrukturen und Anwendungsanforderungen zugeschnitten sind. Dieser Leitfaden bietet eine umfassende Untersuchung des Iterator-Protokolls und der Implementierung von benutzerdefinierten Iteratoren.

Was ist das Iterator-Protokoll?

Das Iterator-Protokoll definiert, wie ein Objekt iteriert werden kann, d.h. wie seine Elemente sequenziell zugänglich gemacht werden. Es besteht aus zwei Teilen: dem Iterable-Protokoll und dem Iterator-Protokoll.

Iterable-Protokoll

Ein Objekt wird als Iterable (iterierbar) betrachtet, wenn es eine Methode mit dem Schlüssel Symbol.iterator hat. Diese Methode muss ein Objekt zurückgeben, das dem Iterator-Protokoll entspricht.

Im Wesentlichen weiß ein iterierbares Objekt, wie es einen Iterator für sich selbst erstellen kann.

Iterator-Protokoll

Das Iterator-Protokoll definiert, wie Werte aus einer Sequenz abgerufen werden. Ein Objekt wird als Iterator betrachtet, wenn es eine next()-Methode hat, die ein Objekt mit zwei Eigenschaften zurückgibt:

Die next()-Methode ist das Arbeitspferd des Iterator-Protokolls. Jeder Aufruf von next() bewegt den Iterator vorwärts und gibt den nächsten Wert in der Sequenz zurück. Wenn alle Werte zurückgegeben wurden, gibt next() ein Objekt zurück, bei dem done auf true gesetzt ist.

Eingebaute Iterables

JavaScript bietet mehrere eingebaute Datenstrukturen, die von Natur aus iterierbar sind. Dazu gehören:

Diese Iterables können direkt mit der for...of-Schleife, der Spread-Syntax (...) und anderen Konstrukten verwendet werden, die auf dem Iterator-Protokoll basieren.

Beispiel mit Arrays:


const myArray = ["apple", "banana", "cherry"];

for (const item of myArray) {
  console.log(item); // Ausgabe: apple, banana, cherry
}

Beispiel mit Zeichenketten:


const myString = "Hello";

for (const char of myString) {
  console.log(char); // Ausgabe: H, e, l, l, o
}

Die for...of-Schleife

Die for...of-Schleife ist ein mächtiges Konstrukt zum Iterieren über iterierbare Objekte. Sie kümmert sich automatisch um die Komplexität des Iterator-Protokolls und macht es einfach, auf die Werte in einer Sequenz zuzugreifen.

Die Syntax der for...of-Schleife lautet:


for (const element of iterable) {
  // Code, der für jedes Element ausgeführt wird
}

Die for...of-Schleife ruft den Iterator vom iterierbaren Objekt ab (mittels Symbol.iterator) und ruft wiederholt die next()-Methode des Iterators auf, bis done true wird. Bei jeder Iteration wird der Variablen element die von next() zurückgegebene value-Eigenschaft zugewiesen.

Erstellen von benutzerdefinierten Iteratoren

Obwohl JavaScript eingebaute Iterables bietet, liegt die wahre Stärke des Iterator-Protokolls in der Möglichkeit, benutzerdefinierte Iteratoren für Ihre eigenen Datenstrukturen zu definieren. Dies ermöglicht Ihnen die Kontrolle darüber, wie Ihre Daten durchlaufen und abgerufen werden.

So erstellen Sie einen benutzerdefinierten Iterator:

  1. Definieren Sie eine Klasse oder ein Objekt, das Ihre benutzerdefinierte Datenstruktur darstellt.
  2. Implementieren Sie die Symbol.iterator-Methode für Ihre Klasse oder Ihr Objekt. Diese Methode sollte ein Iterator-Objekt zurückgeben.
  3. Das Iterator-Objekt muss eine next()-Methode haben, die ein Objekt mit den Eigenschaften value und done zurückgibt.

Beispiel: Erstellen eines Iterators für einen einfachen Bereich

Erstellen wir eine Klasse namens Range, die einen Zahlenbereich darstellt. Wir werden das Iterator-Protokoll implementieren, um das Iterieren über die Zahlen im Bereich zu ermöglichen.


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let currentValue = this.start;
    const that = this; // 'this' für die Verwendung im Iterator-Objekt erfassen

    return {
      next() {
        if (currentValue <= that.end) {
          return {
            value: currentValue++,
            done: false,
          };
        } else {
          return {
            value: undefined,
            done: true,
          };
        }
      },
    };
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Ausgabe: 1, 2, 3, 4, 5
}

Erklärung:

Beispiel: Erstellen eines Iterators für eine verkettete Liste

Betrachten wir ein weiteres Beispiel: die Erstellung eines Iterators für eine Datenstruktur einer verketteten Liste. Eine verkettete Liste ist eine Sequenz von Knoten, wobei jeder Knoten einen Wert und eine Referenz (Zeiger) auf den nächsten Knoten in der Liste enthält. Der letzte Knoten in der Liste hat eine Referenz auf null (oder undefined).


class LinkedListNode {
    constructor(value, next = null) {
        this.value = value;
        this.next = next;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
    }

    append(value) {
        const newNode = new LinkedListNode(value);
        if (!this.head) {
            this.head = newNode;
            return;
        }

        let current = this.head;
        while (current.next) {
            current = current.next;
        }
        current.next = newNode;
    }

    [Symbol.iterator]() {
        let current = this.head;

        return {
            next() {
                if (current) {
                    const value = current.value;
                    current = current.next;
                    return {
                        value: value,
                        done: false
                    };
                } else {
                    return {
                        value: undefined,
                        done: true
                    };
                }
            }
        };
    }
}

// Beispielverwendung:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");

for (const city of myList) {
    console.log(city); // Ausgabe: London, Paris, Tokyo
}

Erklärung:

Generatorfunktionen

Generatorfunktionen bieten eine prägnantere und elegantere Möglichkeit, Iteratoren zu erstellen. Sie verwenden das Schlüsselwort yield, um Werte bei Bedarf zu erzeugen.

Eine Generatorfunktion wird mit der Syntax function* definiert.

Beispiel: Erstellen eines Iterators mit einer Generatorfunktion

Schreiben wir den Range-Iterator mit einer Generatorfunktion neu:


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Ausgabe: 1, 2, 3, 4, 5
}

Erklärung:

Generatorfunktionen vereinfachen die Erstellung von Iteratoren, indem sie die next()-Methode und das done-Flag automatisch verwalten.

Beispiel: Fibonacci-Folgen-Generator

Ein weiteres großartiges Beispiel für die Verwendung von Generatorfunktionen ist die Erzeugung der Fibonacci-Folge:


function* fibonacciSequence() {
  let a = 0;
  let b = 1;

  while (true) {
    yield a;
    [a, b] = [b, a + b]; // Destrukturierungszuweisung für gleichzeitige Aktualisierung
  }
}

const fibonacci = fibonacciSequence();

for (let i = 0; i < 10; i++) {
  console.log(fibonacci.next().value); // Ausgabe: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

Erklärung:

Vorteile der Verwendung des Iterator-Protokolls

Fortgeschrittene Iterator-Techniken

Kombinieren von Iteratoren

Sie können mehrere Iteratoren zu einem einzigen Iterator kombinieren. Dies ist nützlich, wenn Sie Daten aus mehreren Quellen auf einheitliche Weise verarbeiten müssen.


function* combineIterators(...iterables) {
  for (const iterable of iterables) {
    for (const item of iterable) {
      yield item;
    }
  }
}

const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";

const combined = combineIterators(array1, array2, string1);

for (const value of combined) {
  console.log(value); // Ausgabe: 1, 2, 3, a, b, c, X, Y, Z
}

In diesem Beispiel nimmt die `combineIterators`-Funktion eine beliebige Anzahl von Iterables als Argumente entgegen. Sie iteriert über jedes Iterable und gibt jedes Element aus. Das Ergebnis ist ein einzelner Iterator, der alle Werte aus allen Eingabe-Iterables erzeugt.

Filtern und Transformieren von Iteratoren

Sie können auch Iteratoren erstellen, die die von einem anderen Iterator erzeugten Werte filtern oder transformieren. Dies ermöglicht es Ihnen, Daten in einer Pipeline zu verarbeiten, wobei verschiedene Operationen auf jeden Wert angewendet werden, während er generiert wird.


function* filterIterator(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

function* mapIterator(iterable, transform) {
  for (const item of iterable) {
    yield transform(item);
    }
}

const numbers = [1, 2, 3, 4, 5, 6];

const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);

for (const value of squaredEvenNumbers) {
    console.log(value); // Ausgabe: 4, 16, 36
}

Hier nimmt `filterIterator` ein Iterable und eine Prädikatsfunktion entgegen. Es gibt nur die Elemente aus, für die das Prädikat `true` zurückgibt. Der `mapIterator` nimmt ein Iterable und eine Transformationsfunktion entgegen. Er gibt das Ergebnis der Anwendung der Transformationsfunktion auf jedes Element aus.

Anwendungen in der Praxis

Das Iterator-Protokoll wird in JavaScript-Bibliotheken und -Frameworks häufig verwendet und ist in einer Vielzahl von realen Anwendungen wertvoll, insbesondere beim Umgang mit großen Datenmengen oder asynchronen Operationen.

Bewährte Praktiken

Fazit

Das JavaScript Iterator-Protokoll bietet eine leistungsstarke und flexible Möglichkeit, Datenstrukturen zu durchlaufen. Durch das Verständnis der Iterable- und Iterator-Protokolle und die Nutzung von Generatorfunktionen können Sie benutzerdefinierte Iteratoren erstellen, die auf Ihre spezifischen Bedürfnisse zugeschnitten sind. Dies ermöglicht es Ihnen, effizient mit Daten zu arbeiten, die Lesbarkeit des Codes zu verbessern und die Leistung Ihrer Anwendungen zu steigern. Die Beherrschung von Iteratoren eröffnet ein tieferes Verständnis der Fähigkeiten von JavaScript und befähigt Sie, eleganteren und effizienteren Code zu schreiben.