Od ES6 do ESNext - Nowości w ES6
Od ES6 do ESNext - Nowości w ES6

Kiedy czytasz o JavaScripcie, możesz mieć pewne problemy z terminologią. ES5, ES6, ES7, ES8, ES2015, ES2016, ES2017, ECMAScript 2015, ECMAScript 2016, ECMAScript 2017 i tak dalej. Dowiedzmy się, co te terminy tak naprawdę znaczą, czym się różnią i dlaczego powstały.

Czym jest ECMAScript

Wszystkie powyższe terminy czy skróty nawiązują do standardu nazywanego ECMAScript. ECMAScript to standard języka programowania, a JavaScript jest implementacją. W wielkim uproszczeniu standard to prosty plan samochodu i tego, jak się zachowuje (ma koła, karoserię, kierownice, można nim jeździć, zatrzymać się, skręcać itp.), a konkretny samochód (np. Multipla… albo cokolwiek innego) to implementacja tego protokołu.

Poza JavaScriptem, który jest najpopularniejszy, są inne języki, które implementują ECMAScript, na przykład:

  • ActionScript – język skryptowy używany przez Flash
  • JScript – język skryptowy Microsoftu

Podczas tworzenia kolejnych wersji standardu ES, ich nazwy dostawały kolejne numerki lub rok powstania danej wersji, dlatego można spotkać na przykład ES6, ECMAScript 2015 oraz ES2015. Poniżej tabelka wyjaśniająca zawiłości tych wersji:

Wersja
Oficjalna nazwa
Długa nazwa
Data wydania
ES9ES2018ECMAScript 2018Czerwiec 2018
ES8ES2017ECMAScript 2017Czerwiec 2017
ES7ES2016ECMAScript 2016Czerwiec 2016
ES6ES2015ECMAScript 2015Czerwiec 2015
ES5.1ES5.1ECMAScript 5.1Czerwiec 2011
ES5ES5ECMAScript 5Grudzień 2009
ES4ES4ECMAScript 4Porzucone
ES3ES3ECMAScript 3Grudzień 1998
ES2ES2ECMAScript 2Czerwiec 1998
ES1ES1ECMAScript 1Czerwiec 1997

Ta tabelka powinna wam coś rozjaśnić. Jak widzicie, od 2009 roku coraz szybciej standard był rozwijany, a od 2015 roku już co roku były wydawane nowe wersje. Ciekawe co nas czeka w roku 2019… 🙂

Poniżej znajdziecie opis wszystkich nowości dodanych w ES2015/ECMAScript 2015/ES6. Zapraszam.

let oraz const

Przed ES6 var był jedyną opcją na zadeklarowanie zmiennej. var jest function-scoped, czyli jest widoczna w funkcji lub zasięgu globalnym, jeżeli jest deklarowana poza jakąkolwiek funkcją.

function test() {
   var zmienna = 0;
   console.log(zmienna); // 0
}
console.log(zmienna); // ReferenceError: zmienna is not defined

Gdy zapomniałeś użyć var, zmienna była przypisywana do zasięgu globalnego, co go zaśmiecało, ale też działało. var nie było block-scoped, przez co mogły pojawiać się różne problemy.

var zmienna = 0
if (true) {
  var zmienna = 1;
}
console.log(zmienna); // 1

ES6 wprowadził let i const. Są to słowa kluczowe, które pozwalają na deklarację zmiennych, podobnie jak var, ale względem var mają parę różnic. Deklaruje się je tak:

let zmienna1 = ‘wartość 1’;
const zmienna2 = 'wartość 2';

Zmienne let i constblock-scoped, czyli są widoczne tylko w bloku, w którym zostały zadeklarowane:

let zmienna = 0;
if (true) {
  let zmienna = 1;
  const stala = 0;
}
console.log(zmienna); // 0
console.log(stala); // Uncaught ReferenceError: stala is not defined

Co więcej, w wielkim uproszczeniu let i const zachowują się na pierwszy rzut oka tak, jakby nie działał na nie hoisting:

console.log(a); // undefined
var a = 2;
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(c); // Uncaught ReferenceError: c is not defined
let c = 2;

Prawda jest jednak inna. Sprawdźmy przykład:

var zmienna = 'wartosc zewnetrzna';
(function() {
  console.log(zmienna); // wartosc zewnetrzna
}());

let zmienna2 = 'wartosc zewnetrzna';
(function() {
  console.log(zmienna2); // wartosc zewnetrzna
}());

W obu przypadkach (dla var i let) kod zadziała tak samo. W zasięgu wewnętrznym funkcji nie ma zmiennej zmienna lub zmienna2, więc JS bierze z zasięgu wyżej. Dlatego wypisuje się w konsoli „wartosc zewnetrzna”. Teraz dodajmy sobie jedną małą deklarację kolejnej zmiennej, tym razem w funkcji już po console.log:

var zmienna = 'wartosc zewnetrzna';
(function() {
  console.log(zmienna); // undefined
  var zmienna = 'wartosc wewnetrzna';
}());

let zmienna2 = 'wartosc zewnetrzna';
(function() {
  console.log(zmienna2); // Uncaught ReferenceError: zmienna2 is not defined
  let zmienna2 = 'wartosc wewnetrzna';
}());

Tutaj zaczynają się schody. W przypadku var jest to, czego się spodziewaliśmy. Deklaracja zmiennej wewnętrznej jest hoistowana na początek funkcji, dlatego mamy undefined. Jest ok. Za to przy let – tu sprawa się komplikuje. Gdyby hoisting nie działał, mielibyśmy wypisane „wartosc zewnetrzna”. Gdyby działał normalnie, mielibyśmy „undefined”. Co więc się dzieje?

let i const są hoistowane na początek funkcji, ale wtedy tez zaczyna działać mechanizm Temporal Dead Zone (TDZ). Mechanizm ten sprawia, że nie można użyć zmiennych let i const przed ich zadeklarowaniem. JS zwraca po prostu błąd, zamiast wartości undefined. Jest to bardzo ważne zagadnienie i lepiej opisze je w oddzielnym wpisie. Na razie należy pamiętać, że tak naprawdę zmienne let i const są hoistowane, mimo że działają tak, jakby nie były.

const dodatkowo nie pozwala na zadeklarowanie zmiennej bez jej inicjalizowania, zabrania też przypisywania wartości na nowo. Pozwala tylko zmieniać wartości tablic i obiektów:

const zmienna = 0;
zmienna = 1; // TypeError: Assignment to constant variable.
const zmienna2; // SyntaxError: Missing initializer in const declaration
const obiekt = {};
obiekt.zmienna = 1;
console.log(obiekt); // { zmienna: 1 }
obiekt = {} // TypeError: Assignment to constant variable.

Najlepiej używać let jak var, a const w przypadku, kiedy wiemy, że nie będziemy zmieniali danej zmiennej. let to taki nowy var. const to stała jak np. liczba Pi.

Arrow Functions

ES6 wprowadza nowy sposób tworzenia funkcji, tak zwane Arrow functions, fat arrow functions czy funkcje strzałkowe. Największym plusem tej funkcji jest uproszczenie składni. Podstawowa funkcja strzałkowa wygląda następująco:

var funkcjaZwykla = function() {
    // tutaj cos robimy
}

const funkcjaStrzalkowa = () => {
  // tutaj cos robimy
}

Jak widzisz, pomijamy słowo kluczowe function. Jeżeli funkcja ma jeden, tylko jeden i aż jeden parametr, można pominąć nawiasy:

const funkcjaStrzalkowa = zmienna => {
  console.log(zmienna);
}

Jeżeli funkcja ma tylko jedno wykonanie, można pominąć klamry i napisać wszystko w jednej linii:

const funkcjaStrzalkowa = (zmienna) => console.log(zmienna);

Łącząc dwie poprzednie właściwości, uzyskujemy:

const funkcjaStrzalkowa = zmienna => console.log(zmienna);

Funkcje strzałkowe pozwalają również na użycie domniemanego return, czyli zwracanie wartości bez słówka kluczowego return. Działa to tylko dla funkcji jednolinijkowych (czyli dla jednego wywołania, którym byłoby return):

const funkcjaStrzalkowa = zmienna => zmienna * 4;
console.log(funkcjaStrzalkowa(5)) // 20

Jeżeli chcemy w ten sposób zwrócić obiekt, pamiętajmy, żeby obiekt ten otoczyć nawiasami, aby klamry obiektu nie były brane za klamry funkcji:

const funkcjaStrzalkowa = zmienna => ({ a: zmienna, b: zmienna*2 })

Porównanie funkcji podwajającej przy użyciu nowego i starego zapisu:

//Zwykły JS, zwykła funkcja
var podwojenie = function(numer){
    return numer * 2;
}
console.log(podwojenie(2))
  
//to samo używając arrow function z ES6
const podwojenie = numer => numer * 2;
console.log(podwojenie(2))

Funkcje tworzone przy użyciu fat arrow nie tworzy ona swojego this, a bierze go z kontekstu wywołania. Zwykle jest to rodzic. Inaczej mówiąc, odpowiada zwykłej funkcji z dodatkiem .bind(this). Dzięki temu możemy robić na przykład to:

const programista = {
  technologia: 'JS',
  prezentacja: () => {
    return `Programuje w ${this.technologia}`
  }
}

Class / Klasy

Jedynym sposobem na dziedziczenie przed ES6 było dziedziczenie przez prototypy. Mimo że działało, miało sporo problemów i nie było podobne do tego, co znamy z innych języków obiektowych, takich jak Java czy C#, czyli do dziedziczenia przez klasy. Dopiero ES6 wprowadza klasy, co pozwala uprościć często zawiły kod. Upodabnia też składnię do tej znanej z języków obiektowych. Klasy tworzy się następująco:

class Pracownik {
    constructor(name) {
        this.name = name;
    }
    przywitanie() {
        return "Witaj, nazywam sie " + this.name;
    }
}

Klasa ma swoją nazwę, konstruktor i wewnętrzne funkcje. Instancję klasy tworzy się przez operator new i nazwę klasy oraz argumenty przekazywane do konstruktora:

const pracownik = new Pracownik(‘Marcin’);

Kiedy tworzymy instancję klasy, odpalany jest konstruktor. W naszym przypadku jest to przypisanie imienia ‚Marcin’ do zmiennej w obiekcie o nazwie ‚name’. Żeby wywołać funkcję z klasy, trzeba użyć instancji klasy i nazwy funkcji:

pracownik.przywitanie() // Witaj, nazywam sie Marcin

Dziedziczenie przy pomocy nowych klas jest bardzo proste. Używa się słówka kluczowego extend. Przykład:

class Pracownik {
    constructor(name) {
        this.name = name;
    }
    przywitanie() {
        return "Witaj, nazywam sie " + this.name + ". ";
    }
}
 
class Programista extends Pracownik {
    constructor(name) {
        super(name);
    }
    programuj() {
        return super.przywitanie() + "Programuje!";
    }
}

const programista = new Programista('Andrzej');
console.log(programista.programuj());

Jeżeli chcesz odwołać się do komponentu, z którego dziedziczysz, użyj ‚super’. Klasy ES6 pozwalają nam również tworzyć metody statyczne. Aby je zrobić, należy przed nazwą metody użyć słówka kluczowego ‚static’:

class Pracownik {
    constructor(name) {
        this.name = name;
    }
    static przywitanie() {
        return "Witaj”;
    }
}

Person.przywitanie();

Klasy ES6 niestety nie dają nam możliwości tworzenia metod prywatnych. Pozwalają za to na stworzenie getterów i setterów:

class Pracownik {
  constructor(name) {
    this._name = name
  }
  set name(value) {
    this._name = value
  }
  get name() {
    return this._name
  }
}

Default parameters / parametry domyślne

ES6 daje nam możliwość dodawania wartości domyślnych do parametrów funkcji. Przykład:

const funkcja = (zmienna = ‘text’) => {
  console.log(zmienna);
}

funkcja(‘zmienna’); // zmienna
funkcja(); // text

Powyższa funkcja wypisze w konsoli wartość przekazaną do niej lub wartość domyślną, w naszym wypadku ‚text’; Parametry domyślne działają również dla wielu parametrów w jednej funkcji:

const funkcja = (zmienna1 = ‘text1’, zmienna2 = ‘text2’, zmienna3 = ‘text3’) => {
  // ...
}

Jeżeli przekazujemy do funkcji obiekt jako parametr i chcemy, żeby obiekt ten miał wartość domyślną, używamy poniższej składni:

const funkcja = ({imie: ‘Dawid’}) => {
  console.log(imie);
}

funkcja({imie: ‘Test’}); // Test
funkcja({imie: ‘Test’, nazwisko: ‘Test2’}); // Test
funkcja(); // Dawid

Template Literals

Template literals pozwalają nam tworzyć stringi w o wiele bardziej przyjemny sposób. Zwykły string tworzy się z użyciem ` zamiast ‘ czy “ :

let zmienna = `jakis tekst`

Template literals daje nam wiele nowych, fajnych właściwości. Po pierwsze – pozwala bezproblemowo tworzyć wieloliniowe stringi. Wcześniej robiło się to tak:

var zmienna1 = “Hello \n World!” // Tworzylo 2 linijki: Hello
				   //			World!

var zmienna2 = “Hello \	//  Tutaj z kolei mimo zapisania tego w 2 liniach w kodzie,
World!”				// renderowane jest jako jedna linia: Hello World

var zmienna3 = “Hello \n \	// Dopiero ten zapis pozwalal na zapisanie 2 linii w kodzie i
World!”				// renderowal sie jako 2 linie jak w zmienna1

Całkiem sporo zachodu, prawda? Template literals ułatwiają to, jak mogą:

let zmienna = `Hello
World

!!!`

Powyższa zmienna zostanie wyrenderowana dokładnie tak, jak ją zapisaliście – 4 linijki, trzecia pusta, pierwsza, druga i czwarta z zawartością. Dostajecie dokładnie to, co stworzycie w kodzie. Prawda, że proste?

Kolejną przydatną funkcją jest interpolacja zmiennych i wyrażeń. Wcześniej, żeby do stringa dodać coś dynamicznie, musieliście robić coś takiego:

var zmienna = … // Dowolne wyrażenie, np. 15*15
var text = ‘Hello World ’ + zmienna;

Lub dowolne wariacje na ten temat. Teraz jest to prostsze:

let text = `Hello World ${15*15}`
let text2 = `New ${text}`

I tyle. Nie potrzebujemy dodatkowych zmiennych czy dodawania do siebie stringów, aby wstawić zmienną. Używamy ${…} a w środku tej konstrukcji wstawiamy dowolną zmienną czy wyrażenie.

Destructuring assignments / Przypisanie destruktywne

Przypisanie destruktywne pozwala nam na rozłożenie obiektu, wybranie konkretnych wartości i przypisanie ich od razu do zmiennych. Brzmi skomplikowanie, ale oto przykład:

let obiekt = {
  hello: ‘World’,
  text: ‘Tekst’
}

let {hello, text} = obiekt; // Przypisanie destruktywne

console.log(hello); // World
console.log(text); // Tekst

Proste, prawda? Jeżeli chcemy zmienić nazwę nowo utworzonej zmiennej z domyślnej, robimy to tak:

let obiekt = {
  hello: ‘World’,
  text: ‘Tekst’
}

let {hello, text: nowaZmienna} = obiekt; // Przypisanie destruktywne

console.log(hello); // World
console.log(text); // ReferenceError: text is not defined
console.log(nowaZmienna); // Tekst

Przypisanie działa też z tablicami:

const tablica = [1,2,3,4,5]
const [pierwsza, druga] = tablica;

console.log(pierwsza); // 1
console.log(druga); // 2

Jeżeli chcemy wziąć tylko konkretne miejsca z tablicy, zamiast nazwy zmiennej zostawiamy puste pole:

const tablica = [1,2,3,4,5]
const [pierwsza, , , , piata] = tablica;

console.log(pierwsza); // 1
console.log(piata); // 5

Enhanced Object Literals

Literały obiektów również dostały parę nowych funkcji:

Uproszczone dodawanie zmiennych do obiektu:

Jeżeli zmienna, którą chcesz przypisać do pola obiektu, ma taką samą nazwę jak to pole, można użyć uproszczonej składni. Zamiast:

let text = “Hello World!”;
let obiekt {
  text: text,
  hello: ‘Hello’,
}

Można użyć:

let text = “Hello World!”;
let obiekt {
  text,
  hello: ‘Hello’,
}

Przypisanie prototypu

Można przypisać prototyp następująco:

const dowolnyObiekt = { hello: 'World' }
const nowyObiekt = {
  __proto__: dowolnyObiekt
}

super()

super() pozwala nam na lepsze dojście do zmiennych i funkcji prototypu lub klasy, z której dziedziczymy:

const dowolnyObiekt = { hello: 'World' }
const nowyObiekt = {
  __proto__: dowolnyObiekt
  test() {
    console.log(`Hello ${‘super.hello}`);
  }
}

nowyObiekt.test(); / /Hello World;

Dynamiczne klucze

Możemy prościej tworzyć dynamiczne klucze w obiekcie:

const foo = "fooKey";
const nowyObiekt = {
  ['hello' + '_' + 'world']: ‘Hello World',
  [foo]: ‘Bar’
}

console.log(nowyObiekt.hello_world); // Hello World
console.log(nowyObiekt.fooKey); // Bar

Pętla For-of

Pętla for-of łączy w sobie moc pętli forEach() z możliwością przedwczesnego wyjścia z niej. Tworzymy ją następująco:

for (const zmienna of [1, 2, 3]) {
  console.log(zmienna);
}

Proste, prawda? Jeżeli chcemy mieć również indeksy, używamy .entries():

for (const [index, zmienna] of [‘Hello’, ' World', ' !!!'].entries()) {
  console.log(index) // index
  console.log(zmienna) // wartość
}

Jeżeli chcemy w dowolnym momencie przerwać, używamy break:

for (const [index, zmienna] of [‘Hello’, ' World', ' !!!'].entries()) {
  console.log(index) // index
  console.log(zmienna) // wartość
  if (index === 1) break;
}

Używamy const, ponieważ za każdym obrotem pętli jest tworzony nowy zasięg (scope).

Inny rodzajem for-on jest for-in. Różnica jest jedna:

  • For-on iteruje po wartościach właściwości.
  • For-in iteruje po nazwach właściwości.

Promises / Obietnice

Promises (obietnice) to obiekt udostępniony przez API, reprezentujący wartości, które będzie można wykorzystać w przyszłości.

W przypadku zapytań asynchronicznych, promises pozwoli użyć wartości dopiero po skończeniu zapytania asynchronicznego. Przykład:

var nowaObietnica = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", 'http://......');
    xhr.addEventListener('load', () => resolve(xhr.responseText));
    xhr.addEventListener('error', () => reject(new Error(xhr.statusText)));
    xhr.send();
});
 
nowaObietnica.then(
    result => console.log(result), // w obietnicy jest to przekazywane jako funkcja resolve
    error => console.error(error) // w obietnicy jest to przekazywane jako funkcja reject
);

Tworzymy obietnicę, a później ją wykorzystujemy, przekazując 2 funkcje, jedną na success, i jedną na error. Można użyć funkcji, która zaczeka, aż wszystkie promisy będą skończone, i dopiero coś z nimi zrobi. Przykład:

function zapytanieAsynchroniczne(url) { 
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open("GET", 'http://......');
      xhr.addEventListener('load', () => resolve(xhr.responseText));
      xhr.addEventListener('error', () => reject(new Error(xhr.statusText)));
      xhr.send();
    });
}
Promise.all([
    zapytanieAsynchroniczne('http://.../link/1'),
    zapytanieAsynchroniczne('http://.../link/2'),
    // ...
    zapytanieAsynchroniczne('http://.../link/100'),
  ])
  .then(response => {
    // response to array z odpowiedziami ze wszystkich zapytan
    console.log(response);
  });

Czy jedno, czy sto zapytań, uruchamiamy funkcję dopiero po skończeniu wszystkich.

Jeżeli mamy sytuację jak wyżej, ale czekamy aż co najmniej jedno, dowolne zapytanie się wykona, a nie wszystkie, zamiast .all użyjmy .race – dzięki temu po wykonaniu jednego zapytania reszta zostanie zignorowana i zostanie uruchomiona funkcje .then.

Można też chainować promises, czyli wykonywać je łańcuchowo, jedno po drugim:

const status = response => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  }
  return Promise.reject(new Error(response.statusText))
}
const json = response => response.json()

fetch('/todos.json')
  .then(status)
  .then(json)
  .then(data => {
    console.log('Request succeeded with JSON response', data)
  })
  .catch(error => {
    console.log('Request failed', error)
  })

Modules

Moduły pozwalają umieścić jakąś funkcjonalność w oddzielnym pliku i prosto to załadować do pliku głównego. Ładowanie modułów wygląda następująco:

import package from 'module-name'

Najpierw używamy słówka kluczowego import, później nazwa tego, co chcemy importować, później słowo kluczowe from, a na końcu string ze ścieżką do pliku lub nazwą modułu.

Jeżeli chcemy coś wyexportować, czyli przygotować do bycia importowanymi, robimy to tak:

export default (zmienna1, zmienna2) => console.log(zmienna1 + zmienna2)

Słowo kluczowe export, później możemy lub nie ustawić domyślny export dla tego pliku słówkiem default, a później funkcja, którą eksportujemy. Możemy eksportować dowolną rzecz, jaką chcemy, np. funkcja, array czy zmienna.

Bez problemu możemy tutaj również użyć przypisania dekonstrukcyjnego przy importowaniu wielu rzeczy z jednego pliku:

import {zmienna1, zmienna2, zmienna3} from ‘module-name’

Nowe metody Stringów

repeat()

Ta funkcja pozwala powtórzyć string określoną liczbę razy:

'Witaj'.repeat(5) //'WitajWitajWitajWitajWitaj'

codePointAt()

Metoda ta pozwala zarządzać znakami Unicode, które muszą być reprezentowane dwoma 16-bitowymi jednostkami Unicode zamiast jednej. Przykładowo chiński znak „𠮷” :

"𠮷".charCodeAt(0).toString(16) //d852
"𠮷".charCodeAt(1).toString(16) //df62

console.log("\ud842\udfb7"); //𠮷

Z użyciem nowej funkcji wygląda to tak:

"𠮷".codePointAt(0) //20bb7
console.log("\u{20bb7}"); // 𠮷

Nowe metody dla Object

Object.is()

Metoda ta sprawdza, czy dwie wartości są takie same:

Object.is(a, b)

Wyrażenie zwraca „true” jeżeli a i b:

  • Są dokładnie tym samym obiektem
  • Są dokładnie tym samym stringiem
  • Są równą liczbą
  • Są obydwie undefined, null, NaN, true lub false

Object.assign()

Ta metoda kopiuje wszystkie właściwości jednego lub więcej obiektów w inny obiekt.

const kopia = Object.assign({}, oryginal) // Jeden obiekt
const kopia2 = Object.assign({}, oryginal1, oryginal2, oryginal3...) // Wiele obiektow

Jest to płytka kopia – jeżeli mamy w obiekcie kolejny obiekt, kopiowany jest nie obiekt a referencja. Przez to po zmianie oryginalnego obiektu kopia również się zmienia:

const oryginal = {
  imie: 'Dawid',
  zawod: {
    nazwa: 'Programista'
  }
}
const kopia= Object.assign({}, oryginal)
oryginal.imie= 'Jan'
oryginal.zawod.nazwa = 'Frontend Developer'
kopia.imie //Dawid
kopia.zawod.nazwa //Frontend Developer

Object.setPrototypeOf()

Powyższa funkcja ustawia prototyp obiektu:

const pracownik = {
  jestPracownikiem: true
}
const programista = {
  jestProgramista: true
}
programista.__proto__ = pracownik;
console.log(programista.jestPracownikiem); //true

const jan = Object.create(pracownik)

console.log(jan.jestPracownikiem); //true
console.log(jan.jestProgramista); //undefined

Object.setPrototypeOf(jan, programista)

console.log(jan.jestPracownikiem); //true
console.log(jan.jestProgramista); //true

Spread Operator

Dzieki spread operator możesz rozszerzać obiekty, tablice lub stringi:

const zmienna = [1,2,3];
const zmienna2 = [zmienna, 4, 5, 6] //[[1, 2, 3], 4, 5, 6]
const zmienna3 = [...zmienna, 4, 5, 6] // [1, 2, 3, 4, 5, 6]

const zmienna4 = {
  klucz1: "wartosc1"
};
const zmienna5 = {
  ...zmienna,
  klucz2: "wartosc2"
}
console.log(zmienna5.klucz1); // wartosc1

Możesz również łatwo tworzyć kopie tablicy czy obiektu:

const nowaTablica = [...staraTablica];
const nowyObiekt = { ...staryObiekt};

Można też zamieniać string na tablice charów:

const napis = "Witaj";
const tablica = [...napis]; // ["W", "i", "t", "a", "j"]

Set / Zbiór

Set, inaczej zbiór, to struktura danych, która przechowuje wartości i informuje nas, czy dana wartość istnieje w zbiorze. Można o niej myśleć jak o mapie, gdzie kluczem jest obiekt lub prymityw, a wartością zawsze jest true. Zbiór tworzymy następująco:

const zbior = new Set()

Podstawowe operacje na zbiorze to dodawanie do zbioru, usuwanie ze zbioru oraz sprawdzanie, czy dana wartość lub obiekt jest w zbiorze:

// Dodanie do zbioru
zbior.add('jeden');
zbior.add(123);

// Sprawdzenie czy wartosc jest w zbiorze
zbior.has(1); // false
zbior.has('jeden'); // true

// Usuwanie wartosci ze zbioru
zbior.delete(1);

Dodawać można tylko pojedynczo, więc dodanie paru rzeczy po sobie wymaga wielokrotnego użycia .add(). Zbiór przechowuje też unikalne wartości, więc wielokrotne dodanie takiego samego elementu daje to samo, co jednokrotne.

Zbiory mają również dwie inne ciekawe funkcje:

// Pobranie rozmiaru zbioru
console.log(zbior.size);

//Usuniecie wszystkiego w zbiorze
zbior.clear();

Aby iterować po zbiorze, można użyć kluczy lub wartości – dają taki sam wynik:

//Wyniki dla obydwu pętli są takie same
for (const klucz of zbior.keys()) {
  console.log(klucz )
}

for (const wartosc of zbior.values()) {
  console.log(wartosc )
}

Map / Mapa

Map, inaczej mapa, to kolejna struktura danych. Pozwala nam przechowywać wartości pod konkretnymi kluczami. Przed ES6 używano do tego obiektów, za to ES6 daje nam konkretne narzędzie z różnymi funkcjami.

// Tworzenie mapy
const mapa = new Map();

//Dodawanie elementow do mapy z uzyciem klucza
mapa.set('dzien', 'poniedzialek');
mapa.set('rok', '2019');

//Branie obiektów z uzyciem klucza
const rok = mapa.get('rok');

//Usuwanie elementu z mapy:
mapa.delete('dzien');

//Usuniecie wszystkich elementow z mapy
mapa.clear();

// Sprawdzenie czy mapa zawiera wartosc przypisana do podanego klucza
const maRok = mapa.has('rok');

//Rozmiar mapy
const rozmiar = mapa.size;

Aby iterować po mapie, są trzy możliwości – iterowanie po kluczach, po wartościach lub jednocześnie po tym i po tym:

//Iterowanie po kluczach
for (const klucz of mapa.keys()) {
  console.log(klucz)
}

//Iterowanie po wartosciach
for (const wartosc of mapa.values()) {
  console.log(wartosc)
}

//Iterowanie po parach:
for (const [klucz, wartosc] of mapa.entries()) {
  console.log(klucz, wartosc)
}

Generators / Generatory

Generatory to specjalny obiekt w JS. Można o nim myśleć jak o procesie, który trzeba ręcznie uruchomić i ręcznie wywoływać jego kolejne kroki. Z punktu widzenia kodu, to funkcja, która może wstrzymywać swoje wykonanie i wracać do niego wtedy, kiedy chcemy. Brzmi to dziwnie, ale spójrz na przykład:

function* generator() {
  console.log('Generator');
}

Stworzyliśmy właśnie generator – prosty proces wypisujący napis „Generator” na konsolę. Trzeba go jeszcze uruchomić:

const iterator = generator(); // Stworzenie instancji generatora

const wynik1 = iterator.next(); //Uruchomienie generatora pierwszy raz
const wynik2 = iterator.next(); //Uruchomienie generatora drugi raz
const wynik3 = iterator.next(); //Uruchomienie generatora trzeci raz
const wynik4 = iterator.next(); //Uruchomienie generatora czwarty raz

console.log(wynik1); // { done: true, value: undefined }
console.log(wynik2); // { done: true, value: undefined }
console.log(wynik3); // { done: true, value: undefined }
console.log(wynik4); // { done: true, value: undefined }

Każde wywołanie generatora (.next()) zwraca wartość – w obecnym przypadku jest to obiekt ze zmienną done mówiąca, czy funkcja się zakończyła, oraz value – pole służące do komunikacji generatora z resztą kodu i zawierające informacje z generatora lub wynik jego działania.

Wspominałem o zatrzymywaniu – służy do tego słówko kluczowe yield. Spójrz:

function* generator() {
  console.log('Generator 1');
  yield;
  console.log('Generator 2');
  yield "wiadomosc"
  console.log('Generator 3');
}

const iterator = generator(); // Stworzenie instancji generatora

//Uruchomienie generatora pierwszy raz
const wynik1 = iterator.next();
// W konsoli wypisze Generator 1


//Uruchomienie generatora drugi raz
const wynik2 = iterator.next();
// W konsoli wypisze Generator 2


//Uruchomienie generatora trzeci raz
const wynik3 = iterator.next();
// W konsoli wypisze Generator 3


//Uruchomienie generatora czwarty raz
const wynik4 = iterator.next();
// Nic sie nie zmieni w konsoli


console.log(wynik1); // { done: false, value: undefined }
console.log(wynik2); // { done: false, value: "wiadomosc"}
console.log(wynik3); // { done: true, value: undefined }
console.log(wynik4); // { done: true, value: undefined }

Każde uruchomienie .next() pozwala nam zrobić kolejny krok w generatorze, aż do jego zakończenia. Jeżeli generator się zakończył, kolejne wywołania będą zwracały done: true i niezdefiniowaną wartość. Co więcej, używając pierwszego yield, przekazaliśmy wartość, która widzimy w wynik2. Można tak przekazać dowolny obiekt, tak więc widzimy komunikacje z generatora do kodu.

function* generator() {
  console.log('Generator 1');
  yield "wiadomosc"
  let val = yield;
  console.log(val);
  return "koniec!";
}

const iterator = generator(); // Stworzenie instancji generatora

//Uruchomienie generatora pierwszy raz
const wynik1 = iterator.next();
// W konsoli wypisze Generator 1


//Uruchomienie generatora drugi raz
const wynik2 = iterator.next();
// W konsoli nic sie nei zmieni


//Uruchomienie generatora trzeci raz
const wynik3 = iterator.next('Generator 2');
// W konsoli wypisze przekazane Generator 2 i zakonczy generator


//Uruchomienie generatora czwarty raz
const wynik4 = iterator.next();
// Nic sie nie zmieni w konsoli


console.log(wynik1); // { done: false, value: "wiadomosc" }
console.log(wynik2); // { done: false, value: undefined }
console.log(wynik3); // { done: true, value: "koniec!" }
console.log(wynik4); // { done: true, value: undefined }

W tym przypadku nie przekazaliśmy wartość DO generatora, dostaliśmy wartość Z generatora, używając yield oraz return. Dwukierunkowa komunkacja – prawda że przydatne?

Podsumowanie

Całkiem sporo nowości przyniosło nam ES6. Większość z nich bardzo przydatna, część mniej, polecam jednak poznać je wszystkie. A już za tydzień ES7!

ZOSTAW ODPOWIEDŹ

Please enter your comment!
Please enter your name here