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());

// …

ES6 – arrow functions

javascript

Kolejnym elementem standardu ECMAScript 2015 będą bez wątpienia arrow functions. Znane również jako fat functions, pozwolą tworzyć kod bez – zbędnego w wielu przypadkach – słowa kluczowego function oraz ze zbindowanym this do ciała funkcji.


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


Syntactic sugar

(w oryginalnym artykule użyłem właśnie tego nagłówka, jako, że polski odpowiednik brzmi idiotycznie)

Spójrzmy na sygnaturę nowej składni anonimowej funkcji:

([param] [, param]) => {
 statements
}
           param => expression
(param1, param2) => { block }

, którą możemy użyć w następujący sposób:

    () => { … }            // no argument
     x => { … }            // one argument
(x, y) => { … }            // several arguments
     x => { return x * x } // block
    x => x * x             // expression, same as above
Pamiętaj: każda tak tworzona funkcja jest anonimowa!

Wyrażenia lambda w JavaScript! Wspaniale :)

Zamiast pisać:

[3, 4, 5].map(function (n) {
 return n * n;
});

możemy po prostu zrobić coś takiego:

[3, 4, 5].map(n => n * n);

Już jest dobrze, a jest coś jeszcze..

Fixed “this” = lexical “this”

Funkcja tworzona przy pomocy konstrukcji z => binduje this wewnątrz ciała funkcji w miejscu jej deklaracji, a nie użycia. Tym samym nie musimy już używać funkcji bind, call czy apply. Nie musimy również tworzyć konstrukcji typu:

var self = this;

Dla mnie to naprawdę spore ułatwienie, a przy okazji sposób na lepszą optymalizację kodu.

// ES5
function FancyObject() {
 var self = this;
 
 self.name = 'FancyObject';
 setTimeout(function () {
  self.name = 'Hello World!';
 }, 1000);
}

// ES6
function FancyObject() {
 this.name = 'FancyObject';
 setTimeout(() => {
  this.name = 'Hello World!'; // properly refers to FancyObject
 }, 1000);
}

Konstrukcja funkcji za pomocą =>, to:

  • typeof zwracające function
  • instanceof zwracające Function

, ale niestety także kilka niedogodności:

  • Tak stworzona funkcja nie może być użyta w formie konstruktora i każde jej wywołanie ze słowem new rzuci wyjątek.
  • Fixed this oznacza, że wartość zbindowana wewnątrz funkcji nie może zostać zmieniona przez cały cykl jej życia.
  • Funkcja nie może być funkcją nazwaną.
  • Nie mamy samej deklaracji funkcji, stąd nie istnieje tutaj hoisting pozwalający użyć danej funkcji przed jej inicjalizacją.