ES6 – set, map, weak

javascript

Wraz z ECMAScript2015 otrzymujemy w końcu pełnoprawne kolekcje: Set i Map. Porzucamy więc spartańskie podejście do tworzenia struktur danych. Niniejszy rozdział opisuje w jaki sposób korzystać z Set, Map, WeakSet i WeakMap.


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


Map

Mapy to kolekcje typu key => value. Zarówno klucz, jak i jego wartość mogą być typami prymitywnymi lub obiektami / referencjami.

Nie rozwodząc się długo, spójrzmy na przykład:

let map = new Map(),
    val2 = 'val2',
    val3 = {
      key: 'value'
    };

map.set(0, 'val1');
map.set('1', val2);
map.set({ key: 2 }, val3);

console.log(map); // Map {0 => 'val1', '1' => 'val2', Object {key: 2} => Object {key: 'value'}}

Tworząc mapę, używamy konstruktora klasy Map. By dodać kolejną parę klucz => wartość, wywołujemy metodę set(key, value).

Kolekcję typu Map możemy również zbudować poprzez przekazanie do konstruktora klasy, tablicy kolejnych par:

let map,
    val2 = 'val2',
    val3 = {
      key: 'value'
    };

map = new Map([[0, 'val1'], ['1', val2], [{ key: 2 }, val3]]);

console.log(map); // Map {0 => 'val1', '1' => 'val2', Object {key: 2} => Object {key: 'value'}}

Nie będzie pewnie dla nikogo zaskoczeniem, że aby pobrać wartość z kolekcji na podstawie klucza skorzystamy z metody get(key):

let map = new Map(),
    val2 = 'val2',
    val3 = {
      key: 'value'
    };

map.set(0, 'val1');
map.set('1', val2);
map.set({ key: 2 }, val3);

console.log(map.get('1')); // val2

By iterować po elementach kolekcji, możemy użyć dobrze znanej metody forEach lub posłużyć się pętlą for-of, z racji tego, że klasa Map udostępnia nam swój wbudowany iterator.

let map = new Map(),
    val2 = 'val2',
    val3 = {
      key: 'value'
    };

map.set(0, 'val1');
map.set('1', val2);
map.set({ key: 2 }, val3);

// forEach
map.forEach(function (value, key) {
  console.log(`Key: ${key} has value: ${value}`);
  // Key: 0 has value: val1
  // Key: 1 has value: val2
  // Key: [object Object] has value: [object Object] 
});

// for-of
for (let entry of map) {
  console.log(`Key: ${entry[0]} has value: ${entry[1]}`);
  // Key: 0 has value: val1
  // Key: 1 has value: val2
  // Key: [object Object] has value: [object Object] 
};

Do dyspozycji mamy również metody:

  • entries() — w zasadzie wynik wywołania nie różni się niczym od powyższych przykładów. Metoda entries() zwraca całą kolekcję elementów.
  • keys() — zwraca jedynie klucze wszystkich elementów.
  • values() — zwraca wyłącznie wszystkie wartości.

Do sprawdzenia, czy dany klucz znajduje się w naszej kolekcji, wykorzystamy metodę has(key), która zwraca wartość typu boolean:

let map = new Map(),
    val2 = 'val2',
    val3 = {
      key: 'value'
    };

map.set(0, 'val1');
map.set('1', val2);
map.set({ key: 2 }, val3);

console.log(map.has(0));     // true
console.log(map.has('key')); // false

By usunąć dany element z kolekcji użyjemy metody delete(key):

let map = new Map(),
    val2 = 'val2',
    val3 = {
      key: 'value'
    };

map.set(0, 'val1');
map.set('1', val2);
map.set({ key: 2 }, val3);

console.log(map.size); // 3

map.delete('1');
console.log(map.size); // 2

Jeżeli zechcemy usunąć wszystkie elementy z mapy, mamy do dyspozycji metodę clear():

let map = new Map(),
    val2 = 'val2',
    val3 = {
      key: 'value'
    };

map.set(0, 'val1');
map.set('1', val2);
map.set({ key: 2 }, val3);

console.log(map.size); // 3

map.clear();
console.log(map.size); // 0

Set

Kolekcja typu Set różni się od Map tym, że zawiera pojedyncze wartości, które na dodatek muszą być unikalne. Tutaj także mogą być to wartości prymitywne, jak i obiekty lub ich referencje.

let set = new Set();
set.add(1);
set.add('1');
set.add({ key: 'value' });

console.log(set); // Set {1, '1', Object {key: 'value'}}

Klasa Set udostępnia nam podobny zbiór metod, co Map. Jednak zamiast metody set(key, value), mamy do dyspozycji metodę add(value). Kolekcję możemy również utworzyć w ramach konstruktora:

let set = new Set([1, '1', { key: 'value' }]);

console.log(set); // Set {1, '1', Object {key: 'value'}}

Iterowanie po elementach kolekcji przebiega identycznie jak dla Map:

let set = new Set([1, '1', { key: 'value' }]);

// forEach
set.forEach(function (value) {
  console.log(value);
  // 1
  // '1'
  // Object {key: 'value'}
});

// for-of
for (let value of set) {
  console.log(value);
  // 1
  // '1'
  // Object {key: 'value'}
};

Jak już wyżej wspomniałem, Set zawierać może jedynie unikalne wartości:

let set = new Set([1, 1, 1, 2, 5, 5, 6, 9]);
console.log(set.size); // 5!

WeakMap

Kolekcje typu Weak pozwalają zwalniać zasoby w momencie, gdy elementy przestają istnieć. W ten sposób garbage collector jest w stanie usunąć wystąpienia danych obiektów z kolekcji i zwolnić pamięć. Kluczami kolekcji mogą być wyłącznie obiekty.

Kolekcja WeakMap udostępnia nam prawie identyczne metody, jak w przypadku zwykłej klasy Map. Różnica polega na tym, że po elementach kolekcji nie jesteśmy w stanie iterować, zarówno przy pomocy metody forEach oraz pętli for-of. Tak samo nie możemy zbadać rozmiaru kolekcji, gdyż pole size nie istnieje.

new WeakMap([iterable])
WeakMap.prototype.get(key)        : any
WeakMap.prototype.set(key, value) : this
WeakMap.prototype.has(key)        : boolean
WeakMap.prototype.delete(key)     : boolean
let wm = new WeakMap(),
    obj = {
      key1: {
        k: 'v1'
      },
      key2: {
        k: 'v2'
      }
   };

wm.set(obj.key1, 'val1');
wm.set(obj.key2, 'val2');

console.log(wm); // WeakMap {Object {k: 'v1'} => 'val1', Object {k: 'v2'} => 'val2'}

console.log(wm.has(obj.key1)); // true

delete obj.key1;
console.log(wm.has(obj.key1)); // false

WeakSet

WeakSet, to odpowiednik kolekcji Set, z taką samą możliwością zwalniania zasobów jak WeakMap. Udostępnia nam nieco prostsze API i zapewnia unikalność wartości, gdzie każda musi być obiektem.

new WeakSet([iterable])
WeakSet.prototype.add(value)    : any
WeakSet.prototype.has(value)    : boolean
WeakSet.prototype.delete(value) : boolean

Po elementach kolekcji WeakSet nie możemy również iterować, tak samo jak nie jesteśmy w stanie pobrać ich ilości.

let ws = new WeakSet(),
    obj = {
      key1: {
        k: 'v1'
      },
      key2: {
        k: 'v2'
      }
   };

ws.add(obj.key1);
ws.add(obj.key2);

console.log(ws); // WeakSet {Object {k: 'v1'}, Object {k: 'v2'}}

console.log(ws.has(obj.key1)); // true

delete obj.key1;
console.log(ws.has(obj.key1)); // false

na koniec

I to by było na tyle. Cała seria dobiegła końca. Standard ECMAScript2015 został już ratyfikowany. Pozostaje nam tylko poczekać, aż przeglądarki będą w pełni wspierać wszystkie założenia nowej składni JavaScript. Zachęcam do samodzielnego eksperymentowania z nowym API. Internet wypełnia się coraz większą ilością narzędzi i przykładów opartych o ES6, a nawet ES7.

ES6 – promises

javascript

Promises (obietnice?) to nie nowa idea. Każdego dnia używam ich w swoich projektach wraz z AngularJS $q, bazującym na bibliotece: kriskowal / q.

A tool for creating and composing asynchronous promises in JavaScript.

Jest to biblioteka do obsługi i radzenia sobie z asynchronicznością w JavaScript. Zanim jednak przejdę do obietnic, napiszę co nieco o tak zwanych callback‘ach oraz ich “piekle”.


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


callback & callback hell

Odkąd pamiętam, deweloperzy JavaScript używają callbacków (funkcji zwrotnych) do obsługi asynchroniczności oraz wszelkich zapytań HTTP. Z czasem jednak może się okazać (i pewnie nie raz sami się na to nadzialiście), że powstają nam w kodzie takie oto potwory:

console.log('start!');
setTimeout(function () {
  console.log('ping');
  setTimeout(function () {
    console.log('pong');
    setTimeout(function () {
      console.log('end!');
    }, 1000);
  }, 1000);
}, 1000);
// start!
// after 1 sec: ping
// .. 1 sec later: pong
// .. and: end!

W tym prostym przykładzie użyłem funkcji setTimeout(), by pokazać wykonywanie funkcji callback, która odpalana jest z 1-sekundowym opóźnieniem. Wygląda to koszmarnie, czytelnością zupełnie nie zachwyca, a przecież to tylko trzy wywołania funkcji. Wyobraźcie sobie jak szybko w naszym kodzie może powstać takie spaghetti. Tworzenie tego typu piramid nazwane zostało terminem: callback hell.

Możecie o nim poczytać więcej na stronie: callbackhell.com

promises

By poradzić sobie z wyżej opisanym problemem – spotykanym praktycznie wszędzie – wymyślono obietnice. Nie lubię tego zwrotu, ale oddaje on wprost sens działania.

Promise to klasa, udostępniająca API reprezentujące wartość, która będzie dostępna w momencie, gdy wywołanie zostanie skończone, a sama wartość uzupełniona (spełnienie obietnicy).

Wraz z ECMAScript2015 otrzymujemy pełne, natywne wsparcie tej techniki.

Każdy promise może być w stanie:

  • fulfilled — pomyślnie obsłużony
  • rejected — odrzucony
  • pending —  w trakcie – ani fulfilled, ani rejected
  • settled — obsłużony, fulfilled lub rejected

Zwracany promise jest obiektem, który udostępnia nam metodę then(fn: onResolve, fn: onReject), pozwalającą obsłużyć promise w momencie, gdy przechodzi on do stanu settled.

Tak, każdy promise jest obiektem, gdzie callback, to przecież funkcja. Funkcja callback pozwala nam na obsłużenie zdarzenia, a promise to obiekt, który przechowuje pewien stan.

Spójrzmy na przykład prostej obietnicy:

new Promise((resolve, reject) => {
  // when success, resolve
  let value = 'success';
  resolve(value);
 
  // when an error occurred, reject
  reject(new Error('Something happened!'));
});

Każdy promise pozwala zdefiniować funkcję (resolve), która wywołana zostanie w momencie, gdy osiągnięty zostanie stan fullfiled. W przeciwnym wypadku wykonana zostaje funkcja (reject), która obsługuje stan rejected.

Obietnice to obiekty, więc nie są one przekazywane jako parametry, jak funkcje callback – są zwracane. Najlepiej oddaje to zdanie:

The return statement is an object which is a placeholder for the result, which will be available in the future.

Obietnice biorą odpowiedzialność wyłącznie za jedno zdarzenie, gdzie funkcje callback obsługiwać mogą ich wiele, wielokrotnie.

Poprzedni przykład możemy nieco rozbudować i przypisać zwrócony obiekt promise do wyrażenia let:

let promise = new Promise((resolve, reject) => {
  // when success, resolve
  let value = 'success';
  resolve(value);
 
  // when an error occurred, reject
  reject(new Error('Something happened!'));
});

Jak już wyżej wspomniałem – zwrócony obiekt udostępnia metodę then(), która pozwala obsłużyć wartość, w momencie, gdy promise przechodzi do stanu settled:

promise.then(onResolve, onReject)

Możemy użyć tej metody do obsłużenia zwracanych wartości w ramach funkcji onResolve, onReject lub obu.

let promise = new Promise((resolve, reject) => {
  // when success, resolve
  let value = 'success';
  resolve(value);
 
  // when an error occurred, reject
  reject(new Error('Something happened!'));
});

promise.then(response => {
  console.log(response);
}, error => {
  console.log(error);
});

// success

W naszym przykładzie funkcja onReject nigdy się nie wykona i błąd nie zostanie rzucony. Możemy więc usunąć jej obsługę:

let promise = new Promise(resolve => {
  let value = 'success';
  resolve(value);
});

promise.then(response => {
  console.log(response); // success
});

W przypadku wywołania metody then(), obsługiwana jest nie tylko wartość danej obietnicy, zwracany jest również nowy obiekt Promise:

let promise = new Promise(resolve => {
  let value = 'success';
  resolve(value);
});

promise.then(response => {
  console.log(response); // success
  return 'another success';
}).then(response => {
  console.log(response); // another success 
});

Tworząc kod w ten sposób pozostaje on zawsze “płaski”. Nie budujemy tutaj piramid, więc termin callback hell może odejść w niepamięć.

W momencie, gdy interesuje nas tylko obsłużenie wyjątku (stan rejected), wystarczy, że podamy funkcję onReject do metody then():

let promise = new Promise((resolve, reject) => {
  let reason = 'failure';
  reject(reason);
});

promise.then(
  null,
  error => {
    console.log(error); // failure
  }
);

Istnieje jednak lepszy sposób. Poza metodą then(), otrzymujemy także metodę catch(), która pozwala na obsługę sytuacji wyjątkowych:

let promise = new Promise((resolve, reject) => {
  let reason = 'failure';
  reject(reason);
});

promise.catch(err => {
  console.log(err); // failure
});

W momencie, gdy mamy kilka wystąpień metody then() po sobie, wyjątek przekazywany jest tak długo, aż nie zostanie on obsłużony:

let promise = new Promise(resolve => {
  resolve();
});

promise
  .then(response => {
    return 1;
  })
  .then(response => {
    throw new Error('failure');
  })
  .catch(error => {
    console.log(error.message); // failure
  });

W momencie gdy do obsłużenia mamy wiele zdarzeń i na dodatek zależy nam, by wykonać kod po skończeniu ich wszystkich, przychodzi nam z pomocą kolejna metoda: all(). Jest to statyczna metoda klasy Promise, która jako swój argument przyjmuje tablicę obietnic do obsłużenia.

let doSmth = new Promise(resolve => {
    resolve('doSmth');
  }),
  doSmthElse = new Promise(resolve => {
    resolve('doSmthElse');
  }),
  oneMore = new Promise(resolve => {
    resolve('oneMore'); 
  });

Promise.all([
    doSmth,
    doSmthElse,
    oneMore
  ])
  .then(response => {
    let [one, two, three] = response;
    console.log(one, two, three); // doSmth doSmthElse oneMore
  });

W ramach response otrzymujemy tablicę z kolejnymi obiektami promise.

Poza tym – mamy do dyspozycji jeszcze dwie przydatne metody:

  • Promise.resolve(value)  – zwraca promise w stanie fullfiled, który przypisuje wartość value lub zwraca value jeżeli jest to już obiekt typu promise
  • Promise.reject(value) – zwraca obiekt promise w stanie rejected, gdzie wartością jest value

uwaga!

Jak wszystko, również obietnice mają swoje słabe punkty. W momencie, gdy wyjątek wystąpi w metodzie then(), musi on zostać obsłużony w naszym kodzie. W przeciwnym wypadku zostanie on zignorowany!

ES6 – modules

javascript

Do tej pory język JavaScript nie posiadał możliwości modułowego podejścia do architektury aplikacji. Mam oczywiście na myśli podejście natywne, bez używania zewnętrznych bibliotek. Wraz z implementacją ECMAScript2015 w przeglądarkach, zmieni się to na lepsze. Otrzymamy dzięki temu kolejne API, które pozwoli nam tworzyć aplikacje w bardziej ustandaryzowany sposób.


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


Do tej pory musieliśmy się zadowalać dwoma standarami: CommonJS oraz AMD. Są to dwa najpopularniejsze sposoby w podejściu do modułowości w JavaScript, jednak zupełnie ze sobą niekompatybilne.

CommonJS – znane jest z ekosystemu Node.js. Dedykowane serwerom aplikacji, wspiera synchroniczne ładowanie modułów. Składnia oparta jest na niewielkim API, skupionym na słowach kluczowych export oraz require.

AMD – to podejście zaimplementowane zostało w bibliotece RequireJS. Dedykowane jest przeglądarkom, które wzbogacone zostają o asynchroniczne ładowanie modułów. Ceną – w porównaniu z CommonJS – jest jednak bardziej skomplikowane API.

Zadaniem ES6 jest połączenie obu znanych dzisiaj standardów oraz zapewnienie poprawnego działania zarówno po stronie serwerowej, jak i klienckiej. Otrzymujemy łatwe oraz spójne API do asynchronicznego i konfigurowalnego ładowania modułów w naszych aplikacjach.

Model asynchroniczny zakłada również, że kod nie zostaje wykonany póki wszystkie potrzebne moduły nie zostały załadowane oraz zinterpretowane.

named export

Jeden moduł posiadać może wiele nazwanych eksportów, gdzie każdy pozwala udostępniać na zewnątrz pojedyncze zmienne, obiekty, czy funkcje.

export function multiply (x, y) {
  return x * y;
};

Nie ma również problemu, by powyższą funkcję przypisać do zmiennej i eksportować na zewnątrz:

var multiply = function (x, y) {
  return x * y;
};

export { multiply };

Jedyne o czym musimy pamiętać, to nawiasy klamrowe, którymi musi być otoczone eksportowane wyrażenie.

Nic nie stoi również na przeszkodzie, by eksportować całe obiekty. Tutaj także musimy pamiętać o nawiasach klamrowych.

export hello = 'Hello World';
export function multiply (x, y) {
  return x * y;
};

// === OR ===

var hello = 'Hello World',
    multiply = function (x, y) {
      return x * y;
    };

export { hello, multiply };

import

Przejdźmy teraz do importu modułów. Wyobraźmy sobie, że mamy plik export.js, gdzie eksportowany jest obiekt z poprzedniego przykładu. Następnie tworzymy plik import.js, w którym importujemy owy obiekt. Posłużymy się tutaj składnią:

import { ... } from ...

Nawiasy klamrowe określają nam co chcemy zaimportować, a w kolejnej części wyrażenia podajemy skąd chcemy (z jakiego modułu) dokonać importu.

I tak, by zaimportować wyrażenie ‘hello’ z powyższego przykładu napiszemy:

import { hello } from './export';

W przypadku podawania nazwy modułu, możemy pominąć rozszerzenie .js. Podobnie jest w CommonJS oraz AMD.

Za zaimportowanie obu wartości posłuży nam poniższy kawałek kodu:

import { hello, multiply } from './export';

Każda z importowanych wartości może na dodatek posiadać swój alias:

import { multiply as pow2 } from './export';

Jeżeli zależy nam na imporcie całego modułu, wystarczy, że użyjemy wyrażenia typu wildcard (*):

import * as all from './export';

Musimy w tym wypadku skorzystać z aliasu, by móc się później odwoływać do metod w naszym module.

default export

Poza wartościami nazwanymi, eksportować możemy również wartość domyślną. Przydaje się to w wielu sytuacjach, ponieważ zwykle eksportujemy jeden model per moduł. Domyślny export jest więc najważniejszą wartością naszego modułu. Warte zapamiętania, biorąc pod uwagę zasadę SRP (Single responsibility principle).

Oczywiste jest również, że moduł posiadać może tylko jeden domyślny eksport. By stworzyć taki eksport w naszym module, musimy dodać słowo kluczowe default zaraz po export:

export default function (x, y) {
  return x * y;
};

Do importowania domyślnego wyrażenia nie musimy używać nawiasów klamrowych. Możemy ponad to nazwać (alias) importowane wyrażenie jak nam się podoba:

import multiply from './export';

// === OR ===

import pow2 from './export';

// === OR ===
...

Każdy moduł posiadać może zarówno eksporty nazwane, jak i jeden eksport domyślny:

// export.js
export hello = 'Hello World';
export default function (x, y) {
  return x * y;
};

// import.js
import pow2, { hello } from './export';

Domyślne wyrażenie możemy również zaimportować jak pozostałe eksporty nazwane. Importować w tym momencie będziemy wyrażenie default:

// export.js
export default function (x, y) {
  return x * y;
};

// import.js
import { default } from './export';

API modułów zakłada, że praca z nimi odbywa się z poziomu kodu i jest w pełni konfigurowalna. Specyfikacja przewiduje, że moduły zapewniają dynamiczne ładowanie oraz izolację od globalnej przestrzeni nazw.

Przy okazji modułów, warto wspomnieć o projekcie SystemJS. Jest to uniwersalna biblioteka do obsługi modułów ze specyfikacji ECMAScript2015 oraz CommonJS i AMD. Współpracuje także z transpilerami Traceur i Babel, zapewniając w ten sposób modułowość w aktualnych wersjach przeglądarek, które nie oferują wsparcia nowego standardu ES6.

ES6 – classes and inheritance

javascript

OO (Object Oriented) – termin ten był chyba najbardziej wyczekiwaną częścią nowego standardu ECMAScript. Wprowadzenie klas, to coś zupełnie świeżego w świecie JavaScript. Wraz z ES6 otrzymujemy spójne podejście do tworzenia obiektów. Nową funkcjonalność zbudowano ponad to w oparciu o prototypy, by zachować wsteczną kompatybilność.


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


Zacznijmy od przykładu prostej klasy w ES6:

class Vehicle {
 
  constructor (name, type) {
    this.name = name;
    this.type = type;
  }
 
  getName () {
    return this.name;
  }
 
  getType () {
    return this.type;
  }
 
}

let car = new Vehicle('Tesla', 'car');
console.log(car.getName()); // Tesla
console.log(car.getType()); // car

Widzimy tutaj, że pojawiają nam się nowe słowa kluczowe class oraz constructor, które znane są z innych obiektowych języków programowania. Mamy również dwie metody: getName() oraz getType(). Poniżej odpowiednik tej klasy z wykorzystaniem składni ES5:

function Vehicle (name, type) {
  this.name = name;
  this.type = type;
};
 
Vehicle.prototype.getName = function getName () {
  return this.name;
};
 
Vehicle.prototype.getType = function getType () {
  return this.type;
};

var car = new Vehicle('Tesla', 'car');
console.log(car.getName()); // Tesla
console.log(car.getType()); // car

Jest to podejście oparte o prototype.

dziedziczenie

ECMAScript2015 wspiera dziedziczenie, wykonywanie kodu klas dziedziczonych (super), metody statyczneinstancyjne oraz konstrukcję obiektu poprzez słowo kluczowe constructor.

Rozbudujmy poprzedni przykład o dziedziczenie. Zacznę tym razem od wersji ES5:

function Vehicle (name, type) {
  this.name = name;
  this.type = type;
};
 
Vehicle.prototype.getName = function getName () {
  return this.name;
};
 
Vehicle.prototype.getType = function getType () {
  return this.type;
};

function Car (name) {
  Vehicle.call(this, name, ‘car’);
}

Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.parent = Vehicle.prototype;
Car.prototype.getName = function () {
  return 'It is a car: '+ this.name;
};

var car = new Car('Tesla');
console.log(car.getName()); // It is a car: Tesla
console.log(car.getType()); // car

Widzimy, że całość mocno się komplikuje, a to przecież prosty przykład. Spójrzmy teraz na nowe podejście:

class Vehicle {
 
  constructor (name, type) {
    this.name = name;
    this.type = type;
  }
 
  getName () {
    return this.name;
  }
 
  getType () {
    return this.type;
  }
 
}

class Car extends Vehicle {
 
  constructor (name) {
    super(name, 'car');
  }
 
  getName () {
    return 'It is a car: ' + super.getName();
  }
 
}

let car = new Car('Tesla');
console.log(car.getName()); // It is a car: Tesla
console.log(car.getType()); // car

Wygląda znajomo? Z pewnością jest to znacznie lepsze rozwiązanie. Mamy tutaj klasę Vehicle oraz Car, która przy pomocy extends dziedziczy po tej pierwszej. W konstruktorze klasy Car wykonywany jest ponad to konstruktor klasy dziedziczonej poprzez wywołanie z super. Dodatkowo metoda getName() zostaje nadpisana w klasie Car i wykonuje metodę getName() z klasy dziedziczonej również przy użyciu słowa kluczowego super.

By w ES5 osiągnąć podobną funkcjonalność musielibyśmy kombinować z wykorzystaniem metod call lub apply.

static

Podejście obiektowe pozwala nam na definiowanie metod statycznych, które dostępne są bez konieczności tworzenia instancji obiektu za pomocą new. Dostęp do metod statycznych otrzymujemy poprzez odwołanie do nazwy klasy, w której dana metoda została zdefiniowana. Poza tym, metody statyczne mogą być również dziedziczone oraz wywoływane z podklas.

class Vehicle {
 
  constructor (name, type) {
    this.name = name;
    this.type = type;
  }
 
  getName () {
    return this.name;
  }
 
  getType () {
    return this.type;
  }
 
  static create (name, type) {
    return new Vehicle(name, type);
  }
 
}

let car = Vehicle.create('Tesla', 'car');
console.log(car.getName()); // Tesla
console.log(car.getType()); // car

get / set

Kolejnym standardowym elementem języków obiektowych są tak zwane settery oraz gettery. Pozwalają one zdefiniować dostęp do pól obiektu (get) oraz ostawienie ich wartości (set).

class Car {
 
  constructor (name) {
    this._name = name;
  } 
 
  set name (name) {
    this._name = name;
  }
 
  get name () {
    return this._name;
  }
 
}

let car = new Car('Tesla');
console.log(car.name); // Tesla

car.name = 'BMW';
console.log(car.name); // BMW

enhanced object properties

Warto również nadmienić, że ES6 to również skrócone oraz wyliczane tworzenie własności w obiekcie oraz przemodelowane funkcje, które widzieliśmy już w powyższych przykładach.

skrócone własności

To nic innego, jak tworzenie własności na podstawie nazwy zmiennej. Najlepiej zilustruje to poniższy przykład:

// ES6
let x = 1,
    y = 2,
    obj = { x, y };

console.log(obj); // Object { x: 1, y: 2 }

// ES5
var x = 1,
    y = 2,
    obj = {
      x: x,
      y: y
    };

console.log(obj); // Object { x: 1, y: 2 }

W przypadku składni ES6 nie musimy podawać nazwy własności obiektu jeśli chcemy, by miała ona taką samą nazwę jak nazwa zmiennej, której wartość ma zostać przypisana do obiektu.

wyliczane własności

Kolejną funkcjonalnością ECMAScript2015 są wyliczane własności obiektu. Pozwalają one na użycie zmiennych oraz innych wartości do dynamicznego tworzenia nazw własności.

// ES6
let getKey = () => '123',
    obj = {
      foo: 'bar',
      ['key_' + getKey()]: 123
    };

console.log(obj); // Object { foo: 'bar', key_123: 123 }

// ES5
var getKey = function () {
      return '123';
    },
    obj = {
      foo: 'bar'
    };

obj['key_' + getKey()] = 123;
console.log(obj); // Object { foo: 'bar', key_123: 123 }

metody w obiekcie

Na koniec metody tworzone jako własności obiektu. Widzieliśmy je już w poprzednich przykładach, dotyczących podejścia obiektowego w ES6.

// ES6
let obj = {
  name: 'object name',
  toString () { // 'function' keyword is omitted here
    return this.name;
  }
};

console.log(obj.toString()); // object name

// ES5
var obj = {
  name: 'object name',
  toString: function () {
    return this.name;
  }
};

console.log(obj.toString()); // object name

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

ES6 – destructuring

javascript

Destructuring jest kolejną nowością, którą wprowadza standard ECMAScript 2015. Konstrukcja pozwala wykorzystać dopasowanie wzorca (pattern matching) w operowaniu na tablicach i obiektach.


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


arrays

Dzisiaj często możemy spotkać kod podobny do:

// ES5
var point = [1, 2];
var xVal = point[0],
    yVal = point[1];
 
console.log(xVal); // 1
console.log(yVal); // 2

Jednak ES6 daje nam dostęp do nowych możliwości. Konstrukcja pozwalająca na dopasowanie wzorca umożliwia nam pisanie kodu w bardziej intuicyjny i przejrzysty sposób. Spójrzmy na przykład:

// ES6
let point = [1, 2];
let [xVal, yVal] = point;
 
console.log(xVal); // 1
console.log(yVal); // 2
// .. and reverse!
[xVal, yVal] = [yVal, xVal];
 
console.log(xVal); // 2
console.log(yVal); // 1

Dopasowanie wzorca pozwala nam na przypisywanie wartości poszczególnych elementów tablicy do zmiennych, bez odwoływania się bezpośrednio do ich indeksów.

Kolejny przykład pokazuje, że możemy także pomijać poszczególne elementy:

let threeD = [1, 2, 3];
let [a, , c] = threeD;
console.log(a); // 1
console.log(c); // 3

.. lub sięgać po zagnieżdżone elementy tablic:

let nested = [1, [2, 3], 4];
let [a, [b], d] = nested;
console.log(a); // 1
console.log(b); // 2
console.log(d); // 4

objects

Podobnie jak w przypadku tablic, ES6 daje nam możliwość wyciągania z obiektów konkretnych wartości. Składnia jest bardzo podobna jak w powyższych przykładach. Tym razem musimy jednak użyć literału obiektu:

let point = {
  x: 1,
  y: 2
};
let { x: a, y: b } = point;
console.log(a); // 1
console.log(b); // 2

I również podobnie jak w tablicach, możemy sięgać po zagnieżdżone obiekty:

let point = {
  x: 1,
  y: 2,
  z: {
    one: 3,
    two: 4
  }
};
let { x: a, y: b, z: { one: c, two: d } } = point;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(d); // 4

mixed

Nic nie stoi również na przeszkodzie by mieszać tablice oraz obiekty w naszych strukturach danych i jednocześnie korzystać z dopasowania wzorca:

let mixed = {
  one: 1,
  two: 2,
  values: [3, 4, 5]
};

let { one: a, two: b, values: ['c, , e'] } = mixed;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(e); // 5

Jednak według mnie, najciekawszą formą użycia dopasowania wzorca jest wykorzystanie funkcji. Spójrzmy na przykład kodu:

function mixed () {
  return {
    one: 1,
    two: 2,
    values: [3, 4, 5]
  };
}
let { one: a, two: b, values: ['c, , e'] } = mixed();

console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(e); // 5

Rezultat dokładnie ten sam, a możliwości jakby więcej.

uwaga!

1. Jeżeli obiekt lub tablica nie posiadają wartości w ramach użycia wzorca dopasowania, zwrócona zostanie wartość undefined:

let point = {
  x: 1
};
let { x: a, y: b } = point;
console.log(a); // 1
console.log(b); // undefined

2. Jeżeli ominiemy varlet lub const z lewej strony naszego wyrażenia, to zwrócony zostanie wyjątek:

let point = {
  x: 1
};
{ x: a } = point; // throws error

.. jest jednak proste obejście tego problemu. Wystarczy, że do całego wyrażenia dostawimy nawiasy i stanie się ono zupełnie poprawne:

let point = {
  x: 1
};
({ x: a } = point);
console.log(a); // 1