Jak używać generatorów i iteratorów w JavaScript

Jak używać generatorów i iteratorów w JavaScript

Iteracja po zbiorach danych przy użyciu tradycyjnych pętli może szybko stać się uciążliwa i powolna, zwłaszcza w przypadku ogromnych ilości danych.

Generatory i iteratory JavaScript zapewniają rozwiązanie do wydajnej iteracji dużych zbiorów danych. Korzystając z nich, możesz kontrolować przebieg iteracji, uzyskiwać wartości pojedynczo oraz wstrzymywać i wznawiać proces iteracji.

Tutaj omówisz podstawy i elementy wewnętrzne iteratora JavaScript oraz sposób generowania iteratora ręcznie i za pomocą generatora.

Iteratory JavaScript

Iterator to obiekt JavaScript, który implementuje protokół iteratora. Obiekty te robią to za pomocą następnej metody. Ta metoda zwraca obiekt, który implementuje interfejs IteratorResult .

Interfejs IteratorResult zawiera dwie właściwości: done i value . Właściwość done to wartość logiczna, która zwraca wartość false , jeśli iterator może wygenerować następną wartość w swojej sekwencji, lub wartość true , jeśli iterator zakończył swoją sekwencję.

Właściwość value jest wartością JavaScript zwracaną przez iterator podczas jego sekwencji. Kiedy iterator zakończy swoją sekwencję (gdy done === true ), ta właściwość zwraca undefined .

Jak sama nazwa wskazuje, iteratory umożliwiają „iterację” obiektów JavaScript, takich jak tablice lub mapy. Takie zachowanie jest możliwe dzięki iterowalnemu protokołowi.

W języku JavaScript protokół iterowalny jest standardowym sposobem definiowania obiektów, które można iterować, na przykład w pętli for…of .

Na przykład:

const fruits = ["Banana", "Mango", "Apple", "Grapes"];

for (const iterator of fruits) {
  console.log(iterator);
}


/*
Banana
Mango
Apple
Grapes
*/

Ten przykład wykonuje iterację po tablicy owoców przy użyciu pętli for…of . W każdej iteracji rejestruje bieżącą wartość w konsoli. Jest to możliwe, ponieważ tablice są iterowalne.

Niektóre typy JavaScript, takie jak tablice, ciągi znaków, zestawy i mapy, są wbudowanymi obiektami iteracyjnymi, ponieważ implementują one (lub jeden z obiektów w łańcuchu prototypów) metodę @@iterator .

Inne typy, takie jak obiekty, domyślnie nie są iterowalne.

Na przykład:

const iterObject = {
  cars: ["Tesla", "BMW", "Toyota"],
  animals: ["Cat", "Dog", "Hamster"],
  food: ["Burgers", "Pizza", "Pasta"],
};

for (const iterator of iterObject) {
  console.log(iterator);
}


// TypeError: iterObject is not iterable

Ten przykład pokazuje, co się dzieje, gdy próbujesz wykonać iterację po obiekcie, który nie jest iterowalny.

Tworzenie obiektu iterowalnego

Aby obiekt był iterowalny, musisz zaimplementować metodę Symbol.iterator na obiekcie. Aby stać się iterowalnym, ta metoda musi zwrócić obiekt, który implementuje interfejs IteratorResult .

Poniższe bloki kodu zawierają przykład, jak uczynić obiekt iterowalnym za pomocą iterObject .

Najpierw dodaj metodę Symbol.iterator do obiektu iterObject, używając deklaracji funkcji.

jak tak:

iterObject[Symbol.iterator] = function () {
  // Subsequent code blocks go here...
}

Następnie musisz uzyskać dostęp do wszystkich kluczy w obiekcie, który chcesz uczynić iterowalnym. Dostęp do kluczy można uzyskać za pomocą metody Object.keys , która zwraca tablicę wyliczalnych właściwości obiektu. Aby zwrócić tablicę kluczy obiektu iterObject , przekaż słowo kluczowe this jako argument do obiektu Object.keys .

Na przykład:

let objProperties = Object.keys(this)

Dostęp do tej tablicy pozwoli na zdefiniowanie zachowania iteracyjnego obiektu.

Następnie musisz śledzić iteracje obiektu. Można to osiągnąć za pomocą zmiennych liczników.

Na przykład:

let propertyIndex = 0;
let childIndex = 0;

Użyjesz pierwszej zmiennej licznika do śledzenia właściwości obiektu, a drugiej do śledzenia dzieci właściwości.

Następnie musisz zaimplementować i zwrócić następną metodę.

jak tak:

return {
  next() {
    // Subsequent code blocks go here...
  }
}

W ramach następnej metody będziesz musiał obsłużyć przypadek skrajny, który występuje, gdy cały obiekt został powtórzony. Aby obsłużyć przypadek Edge, musisz zwrócić obiekt z wartością ustawioną na undefined i done ustawioną na true .

Oto jak postępować w przypadku Edge:

if (propertyIndex > objProperties.length - 1) {
  return {
    value: undefined,
    done: true,
  };
}

Następnie musisz uzyskać dostęp do właściwości obiektu i ich elementów podrzędnych za pomocą zadeklarowanych wcześniej zmiennych liczników.

jak tak:

// Accessing parent and child properties
const properties = this[objProperties[propertyIndex]];

const property = properties[childIndex];

Następnie musisz zaimplementować logikę zwiększania zmiennych licznika. Logika powinna zresetować childIndex , gdy w tablicy właściwości nie ma więcej elementów i przejść do następnej właściwości w obiekcie. Dodatkowo powinien inkrementować childIndex , jeśli w tablicy bieżącej właściwości nadal znajdują się elementy.

Na przykład:

// Index incrementing logic
if (childIndex >= properties.length - 1) {
  // if there are no more elements in the child array
  // reset child index
  childIndex = 0;

  // Move to the next property
  propertyIndex++;
} else {
  // Move to the next element in the child array
  childIndex++
}

Na koniec zwróć obiekt z właściwością done ustawioną na wartość false i właściwością value ustawioną na bieżący element potomny w iteracji.

Na przykład:

return {
  done: false,
  value: property,
};

Ukończona funkcja Symbol.iterator powinna być podobna do poniższego bloku kodu:

iterObject[Symbol.iterator] = function () {
  const objProperties = Object.keys(this);
  let propertyIndex = 0;
  let childIndex = 0;

  return {
    next: () => {
      //Handling edge case
      if (propertyIndex > objProperties.length - 1) {
        return {
          value: undefined,
          done: true,
        };
      }

      // Accessing parent and child properties
      const properties = this[objProperties[propertyIndex]];

      const property = properties[childIndex];

      // Index incrementing logic
      if (childIndex >= properties.length - 1) {
        // if there are no more elements in the child array
        // reset child index
        childIndex = 0;

        // Move to the next property
        propertyIndex++;
      } else {
        // Move to the next element in the child array
        childIndex++
      }

      return {
        done: false,
        value: property,
      };
    },
  };
};

Uruchomienie pętli for…of na obiekcie iterObject po tej implementacji nie spowoduje zgłoszenia błędu, ponieważ implementuje metodę Symbol.iterator .

Ręczne wdrażanie iteratorów, jak to zrobiliśmy powyżej, nie jest zalecane, ponieważ jest bardzo podatne na błędy, a logika może być trudna do zarządzania.

Generatory JavaScript

Generator JavaScript to funkcja, której działanie możesz wstrzymać i wznowić w dowolnym momencie. Takie zachowanie pozwala na tworzenie sekwencji wartości w czasie.

Funkcja generatora, która jest funkcją zwracającą generator, stanowi alternatywę dla tworzenia iteratorów.

Możesz utworzyć funkcję generatora w ten sam sposób, w jaki tworzysz deklarację funkcji w JavaScript. Jedyna różnica polega na tym, że do słowa kluczowego funkcji należy dołączyć gwiazdkę ( * ).

Na przykład:

function* example () {
  return "Generator"
}

Kiedy wywołujesz normalną funkcję w JavaScript, zwraca ona wartość określoną przez jej słowo kluczowe return lub niezdefiniowaną w inny sposób. Ale funkcja generatora nie zwraca natychmiast żadnej wartości. Zwraca obiekt Generatora, który można przypisać do zmiennej.

Aby uzyskać dostęp do bieżącej wartości iteratora, wywołaj następną metodę w obiekcie Generator.

Na przykład:

const gen = example();

console.log(gen.next()); // { value: 'Generator', done: true }

W powyższym przykładzie właściwość value pochodzi ze słowa kluczowego return , skutecznie kończąc generator. Takie zachowanie jest ogólnie niepożądane w przypadku funkcji generatora, ponieważ tym, co odróżnia je od normalnych funkcji, jest możliwość wstrzymywania i ponownego uruchamiania wykonywania.

Wydajność Słowo kluczowe

Słowo kluczowe yield umożliwia iterację wartości w generatorach poprzez wstrzymywanie wykonywania funkcji generatora i zwracanie następującej po niej wartości.

Na przykład:

function* example() {
  yield "Model S"
  yield "Model X"
  yield "Cyber Truck"

  return "Tesla"
}

const gen = example();

console.log(gen.next()); // { value: 'Model S', done: false }

W powyższym przykładzie, gdy następna metoda zostanie wywołana w przykładowym generatorze, zatrzyma się za każdym razem, gdy napotka słowo kluczowe yield . Właściwość done również zostanie ustawiona na wartość false , dopóki nie napotka słowa kluczowego return .

Wielokrotne wywołanie metody next w przykładowym generatorze w celu zademonstrowania tego spowoduje, że jako dane wyjściowe otrzymasz następujący wynik.

console.log(gen.next()); // { value: 'Model X', done: false }
console.log(gen.next()); // { value: 'Cyber Truck', done: false }
console.log(gen.next()); // { value: 'Tesla', done: true }

console.log(gen.next()); // { value: undefined, done: true }

Możesz także iterować po obiekcie Generatora, używając pętli for…of .

Na przykład:

for (const iterator of gen) {
  console.log(iterator);
}

/*
Model S
Model X
Cyber Truck
*/

Korzystanie z iteratorów i generatorów

Chociaż iteratory i generatory mogą wydawać się pojęciami abstrakcyjnymi, tak nie jest. Mogą być pomocne podczas pracy z nieskończonymi strumieniami danych i zbiorami danych. Można ich również używać do tworzenia unikalnych identyfikatorów. Biblioteki zarządzania stanem, takie jak MobX-State-Tree (MST), również używają ich pod maską.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *