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 – iterators

javascript

W kolejnym wpisie na temat ECMAScript2015 przedstawię Wam sposób na budowę i użycie własnych iteratorów, które możemy porównać do tych znanych z Javy (Iterable) lub .NET (IEnumerable). ES6 to również wbudowane iteratory dla typów: String i Array oraz nowych: TypedArray, Map i Set.


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


pętla for-of

Zanim zacznę opisywać budowanie własnych iteratorów, warto nadmienić, że bezpośrednio z nimi związana jest nowa pętla for-of. Powstała ona właśnie po to by obsługiwać iteratory budowane oraz te napisane przez nas.

Jej sygnatura to:

for (LET of ITERABLE) {
  CODE BLOCK
}

Jest ona bardzo podobna do pętli for-in, służącej iterowaniu po własnościach obiektów. Przykładowo, kolekcje typu Array posiadają od teraz wbudowany iterator, który pozwala na dostęp do kolejnych elementów:

const arr = [1, 2, 3, 4, 5];
for (let item of arr) {
  console.log(item); // 1
                     // 2
                     // 3
                     // 4
                     // 5
}

Wraz z pętlą for-of możemy również użyć: breakcontinue oraz return.

const arr = [1, 2, 3, 4, 5];
for (let item of arr) {
  if (item > 4) {
    break;
  }
  if (0 !== item % 2) {
    continue;
  }
  console.log(item); // 2
                     // 4
}

Symbol

Kolejnym elementem ECMAScript2015, o którym warto wspomnieć przed przejściem do części dotyczącej iteratora jest Symbol. Jest to niezmienna (immutable) struktura danych, która może nam posłużyć jako identyfikator obiektu. Symbol może posiadać opis, ale jest to element opcjonalny.

// Symbol
let s1 = Symbol('abc');
let s2 = Symbol('abc');
console.log(s1 !== s2); // true
console.log(typeof s1); // 'symbol'
let obj = {};
obj[s1] = 'abc';
console.log(obj); // Object { Symbol(abc): 'abc' }

iterator

Nieprzypadkowo wspomniałem wyżej o strukturze danych Symbol. By stworzyć swój własny iterator, musimy zbudować obiekt, którego identyfikatorem będzie [Symbol.iterator](). Będzie nam potrzebna jeszcze metoda next(), która “wołana” jest jest przez pętlę for-of w celu pobrania kolejnej wartości.

Spójrzmy na przykład prostego iteratora, który będzie zwracać wartości od 1 do 10, a ich ilość zależeć będzie od przekazanego parametru:

let random1_10 = function (items = 1) {
  return {
    [Symbol.iterator]() {
      let cur = 0;
      return {
        next() {
          let done = cur++ === items,
              random = Math.floor(Math.random() * 10) + 1;
          return {
            done: done,
            value: random
          }
        }
      }
    }
  };
};

for (let n of random1_10(5)) {
  console.log(n); // prints 5 random numbers
}

Iterator zwraca dynamicznie generowane numery, zwracając przy tym obiekt:

return {
  done: [Boolean],
  value: [Any]
}

gdzie wartość done, to false lub true, w zależności od tego, czy iterator powinien zakończyć już swoje działanie. W naszym przypadku koniec nastąpi w momencie, gdy zmienna cur osiągnie taką samą wartość, jak parametr items. Wartością value jest dowolny typ danych, który zwracany jest w ciele pętli for-of.

W momencie, gdy wartość dla done, to true, możemy pominąć własność value w zwracanym obiekcie:

let random1_10 = function (items = 1) {
  return {
    [Symbol.iterator]() {
      let cur = 0;
      return {
        next() {
          if (cur++ === items) {
            return {
              done: true 
            }
          }

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

for (let n of random1_10(5)) {
 console.log(n); // prints 5 random numbers
}

ES6 – strings

javascript

Kolejnym przystankiem na drodze do poznania ECMAScript2015 jest zapoznanie się z nowościami dotyczącymi łańcuchów znaków. Następna wersja języka JavaScript, wprowadza kilka bardzo ważnych udoskonaleń, które postaram się opisać w niniejszym wpisie.


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


template strings

W oparciu o interpolację możemy wreszcie tworzyć szablony, które uzupełniane będą wartościami zmiennych. Można rzec: w końcu!

let x = 1;
let y = 2;
let sumTpl = `${x} + ${y} = ${x + y}`;
 
console.log(sumTpl); // 1 + 2 = 3

Jak widać, wyłuskiwanie wartości w takim szablonie następuje poprzez użycie konstrukcji ${value}. Poza tym całe wyrażenie musi być zbudowane z użyciem znaku akcentu (`), by całość rozpoznana była jako szablon.

Powyższy przykład w ES5 przedstawia się następująco:

var x = 1;
var y = 2;
var sumTpl = "" + x + " + " + y + " = " + (x + y);
console.log(sumTpl); // 1 + 2 = 3

Znacznie lepiej wygląda sprawa z ECMAScript2015. Nie potrzebujemy już żadnych narzędzi pozwalających budować szablony.

Łańcuchy znaków są od teraz jeszcze bardziej użyteczne. Pozwalają na definiowanie wartości w wielu liniach, bez użycia znaków konkatenacji:

let types = `Number
String
Array
Object`;
console.log(types); // Number
                    // String
                    // Array
                    // Object

Z wykorzystaniem składni ES5 napisalibyśmy pewnie coś takiego:

var types = "Number\\nString\\nArray\\nObject";
console.log(types); // Number
                    // String
                    // Array
                    // Object

Obiekt String posiada od teraz również nową własność – raw. Pozwala ona pominąć interpretację znaków \\ w wyrażeniu typu multi-line:

let interpreted = 'raw\\nstring';
let esaped = 'raw\\\\nstring';
let raw = String.raw`raw\\nstring`;
console.log(interpreted);    // raw
                             // string
console.log(raw === esaped); // true

Unicode

Wraz z ES6, otrzymujemy pełne wsparcie dla standardu Unicode, mającego obejmować wszystkie znaki znane na całym świecie.

Skrótu ES6 używam w zasadzie naprzemiennie z ECMAScript2015. Kolejna wersja języka JavaScript oznaczona jest numerem 6, który przyjął się w środowisku deweloperów znacznie szybciej niż pojawiła się pełna nazwa nowego standardu.

Spójrzmy na przykład:

let str = '𠮷';
console.log(str.length);             // 2
console.log(str === '\\uD842\\uDFB7'); // true

Widzimy, że znak 𠮷 reprezentowany jest przez dwa 16-bitowe znaki kodowe. Tak więc mamy znak, który potrzebuje aż dwóch znaków kodowych do poprawnego zapisania. Mimo iż widzimy tylko jeden znak, jego długość wynosi 2.

Termin surrogate pairs jest ściśle związany z zapisem tego typu znaków. W ramach UTF-16 pozwala on na zapisanie wartości powyżej U+FFFF:

console.log(str.charCodeAt(0)); // 55362
console.log(str.charCodeAt(1)); // 57271

Metoda charCodeAt() zwraca 16-bitowy numer reprezentujący daną jednostkę znaku. W ramach standardu ECMAScript2015 otrzymujemy do dyspozycji nową metodę – codePointAt(), która zamiast poszczególnych jednostek (jak ma to miejsce w przypadku metody charCodeAt()) zwraca kody w ramach standardu Unicode UTF-8.

console.log(str.codePointAt(0)); // 134071
console.log(str.codePointAt(1)); // 57271
console.log(str.codePointAt(0) === 0x20BB7); // true

Metoda codePointAt() zwraca więc dokładnie takie same wartości, poza znakami BMP.

BMP — Basic Multilingual Plane — the first 2^16 code points.

Kolejna nowe metoda w ES6 związana z Unicode, to fromCodePoint(), czyli operacja odwrotna do funkcji codePointAt():

console.log(String.fromCodePoint(134071));  // "𠮷"
console.log(String.fromCodePoint(0x20BB7)); // "𠮷"

Kody znaków w Unicode reprezentowane są przez 6 znaków, z czego dwa pierwsze to \u, a kolejne 4, to cyfry w zapisie szesnastkowym.

W przypadku UTF-16 sytuacja wygląda nieco inaczej, bo tego typu wartości reprezentowane są przez co najmniej 5 znaków i maksymalnie 10. Ich format to:

\u{1-6 liczb w zapisie szesnastkowym}

Spójrzmy zatem na przykłady z wykorzystaniem składni ES6 oraz porównanie ze składnią ES5:

// ES6
console.log('\u{20BB7}'); // 𠮷
console.log('\u{20BB7}' === '\\uD842\\uDFB7'); // true

// ES5
console.log('\u20BB7); // 7!
console.log('\u20BB7' === '\\uD842\\uDFB7'); // false

W przypadku, gdy będziemy próbowali dopasować jeden znak poprzez wyrażenie regularne, w ES5 otrzymamy nieoczekiwany rezultat:

console.log(/^.$/.test(str)); // false

str ma bowiem długość 2, a nie 1.

Dzięki ES6 mamy możliwość wykorzystać w tym wypadku nową flagę (u), która jest odpowiedzialna za poprawne interpretowanie znaków Unicode.

console.log(/^.$/u.test(str)); // true

pętla for-of i konstrukcja spread

Przy okazji łańcuchów znaków wspomnę także o nowym sposobie iteracji za pomocą metody for-of. Napiszę o niej więcej w kolejnym artykule, ale już teraz nadmienię, że pozwala ona na iterowanie po łańcuchach znaków z pełnym wsparciem dla Unicode w standardzie UTF-16:

let str = 'abc\\uD842\\uDFB7';
console.log(str.length); // 5
for (let c of str) {
  console.log(c); // a
                  // b
                  // c
                  // 𠮷
}

Kolejna ciekawostka to fakt, że również konstrukcja spread pozwala nam bardzo prosto przetransformować łańcuch znaków do postaci tablicy i to z pełnym wsparciem dla UTF-16:

let str = 'abc\\uD842\\uDFB7';
let chars = […str];
console.log(chars); // ['a', 'b', 'c', '𠮷']

kolejne nowe metody

Na koniec zostawiłem sobie opis kilku nowych, ciekawych metod, dzięki którym możemy w prosty sposób operować na łańcuchach znaków.

repeat(n) – powtórzenie łańcucha znaków n razy

console.log('abc|'.repeat(3)); // 'abc|abc|abc|'

startsWith(str, starts = 0) : boolean – sprawdzenie, czy łańcuch znaków zaczyna się od str, zaczynając sprawdzanie od starts

console.log('ecmascript'.startsWith('ecma'));      // true
console.log('ecmascript'.startsWith('script', 4)); // true

endsWith(str, ends = str.length) : boolean – sprawdzenie, czy łańcuch znaków kończy się na str, gdzie parametr ends definiuje nam koniec

console.log('ecmascript'.endsWith('script'));  // true
console.log('ecmascript'.endsWith('ecma', 4)); // true

includes(str, starts = 0) : boolean – sprawdzenie, czy łańcuch znaków zawiera w sobie wartość str, zaczynając sprawdzanie od starts

onsole.log('ecmascript'.includes('ecma'));      // true
console.log('ecmascript'.includes('script', 4)); // true