ES6 – generators

javascript

Generator to chyba najbardziej tajemnicza funkcjonalność, pojawiająca się wraz ze standardem ECMAScript2015. Jest to pewien podtyp iteratora, o którym pisałem poprzednio, a zarazem specjalny rodzaj funkcji, której wykonanie może zostać wstrzymane, a potem znów wznowione.


Zobacz całą serię: Let-s talk about ECMAScript 2015


Na początek chciałbym wprowadzić dwa nowe słowa kluczowe języka JavaScript:

yield – operator zwraca wartość z funkcji w momencie, gdy następuje zatrzymanie działania iteratora (wykonanie metody next()).

funtion* – specjalny rodzaj funkcji, która zwraca instancję generatora.

Spójrzmy na przykład prostego generatora:

function* generator () {
  yield 1; 
  // pause
  yield 2; 
  // pause
  yield 3; 
  // pause
  yield 'done?'; 
  // done
}

let gen = generator(); // [object Generator]
console.log(gen.next()); // Object {value: 1, done: false}
console.log(gen.next()); // Object {value: 2, done: false}
console.log(gen.next()); // Object {value: 3, done: false}
console.log(gen.next()); // Object {value: 'done?', done: false}

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

for (let val of generator()) {
  console.log(val); // 1
                    // 2
                    // 3
                    // 'done?'
}

Widzimy, że generator tworzony jest z wykorzystaniem function*, po którym standardowo podajemy nazwę funkcji. W środku znajdujemy cztery operatory yield, zwracające kolejne wartości: 1, 2, 3, ‘done?’. Następnie przypisujemy instancję generatora do let get poprzez standardowe wywołanie funkcji. Potem wywołujemy metodę next(), która pozwala pobrać kolejną wartość zwracaną przez operator yield oraz zatrzymuje działanie generatora. Metoda next() jest w zasadzie odpowiednikiem znanym już z iteratorów. Po czwartym wykonaniu metody next(), nie posiadamy już kolejnych operatorów yield, zwracających wartość, więc kolejne wywołanie zwraca nam obiekt:

{
  value: undefined,
  done: true
}

Tym samym własność done informuje nas, że generator zakończył swoje działanie. Dzięki temu, że mamy do dyspozycji metodę next(), a sam generator jest podtypem iteratora, bez przeszkód możemy go wykorzystać w pętli for-of, co pokazane zostało na końcu przykładu.

Poniżej przedstawiam kolejny generator, który zwraca losowe liczby od 1 do 10 i może je zwracać bez końca, zawsze wtedy gdy wywołamy metodę next():

function* random1_10 () {
  while (true) {
    yield Math.floor(Math.random() * 10) + 1;
  }
}

let rand = random1_10();
console.log(rand.next());
console.log(rand.next());

// …

W ES5 byłoby to coś w rodzaju:

function random1_10 () {
  return {
    next: function() {
      return {
        value: Math.floor(Math.random() * 10) + 1,
        done: false
      };
    }
  };
}

let rand = random1_10();
console.log(rand.next());
console.log(rand.next());

// …

Oczywiście nic nie stoi na przeszkodzie, by w generatorach używać innych generatorów i mieszać je razem ze sobą. Ilustruje to poniższy przykład, który jest małym rozwinięciem powyższego. Tym razem zwracane będą wartości od 1 do 20:

function* random (max) {
  yield Math.floor(Math.random() * max) + 1;
}

function* random1_20 () {
  while (true) {
    yield* random(20);
  }
}

let rand = random1_20();
console.log(rand.next());
console.log(rand.next());

// …