Nowości w JS — czyli co pojawiło się od czasów ECMAScript 6

JavaScript - uważany za najpopularniejszy język programowania (wg. rankingów stworzonych przez stackoverflow.com), plasuje się w czołówce popularności już 8 rok z rzędu! Z podanych statystyk wynika, że aż 70% profesjonalnych programistów biorących udział w tegorocznym badaniu, używa w swoim kodzie właśnie tego języka. Nic dziwnego, że dzięki tak ogromnemu zainteresowaniu jego składnia i funkcjonalności są stale rozwijane, a nowe frameworki pojawiają się niczym grzyby po deszczu.

Żeby pisać dobry kod, co przekłada się na tworzenie szybkich aplikacji, nie wystarczy raz a dobrze nauczyć się danego języka. Trzeba regularnie śledzić wprowadzane zmiany, zaprzyjaźnić się z dokumentacją i być ze wszystkimi nowościami na bieżąco.

Pierwsza edycja ECMAScript (standard języka JavaScript definiujący jego semantykę) została wydana w 1997 roku, a dzisiaj mamy możliwość pracy z jej 11 wydaniem zwanym jako ECMAScript 2020. Niektórzy uważają jednak, że momentem przełomowym w historii JavaScriptu było wydanie wersji ES6 w 2015 roku. Od tamtej pory pojawiło się wiele niezwykle przydatnych funkcjonalności, które znacznie ułatwiły pracę programistów. Kilka z nich postanowiłem omówić nieco szerzej.

1. Deklaracja zmiennych - let i const

Wielu programistów pamięta czasy kiedy jedyną opcją deklarowania zmiennych w JS było użycie var. Zmienne zadeklarowane za pomocą var miały zasięg globalny, a to oznacza, że można było po nie sięgnąć praktycznie z każdego miejsca w naszym programie. Często sprawiało to problem początkującym programistom, którzy po przesiadce z innego języka jak np. C#, zaczynali swoją przygodę z JavaScriptem. W wersji ES6 pojawiła się możliwość deklarowania zmiennych za pomocą słów kluczowych: let oraz const. Różnią się one od var przede wszystkim zasięgiem blokowym, co oznacza, że są one dostępne jedynie dla bloku (np. funkcji), w którym zostały zadeklarowane. Dodatkowo const różni się od let tym, że nie pozwala na zadeklarowanie zmiennej bez jej inicjalizowania, a także blokuje przypisanie jej wartości na nowo. Wyjątek stanowią jedynie tablice i obiekty, których wartości możemy modyfikować. W dużym uproszczeniu można powiedzieć, że let to taki nowy var, a const używamy kiedy mamy pewność, że wartość danej zmiennej nie ulegnie modyfikacji w dalszej części kodu.

1
2
3
4
5
6
7
var zmienna = 0;

function test() {
    zmienna = 1;
}

console.log(test); // wyświetli nam 1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let zmienna = 0;

function test() {
    let zmienna = 1;
    const stala = 2;
    
    console.log(zmienna); // wyświetli nam 1
    console.log(stala); // wyświetli nam 2
}

    console.log(zmienna); // wyświetli nam 0
    console.log(stala); // Uncaught ReferenceError: stala is not defined

2. Nowy sposób na zapis stringów

Jeszcze jakiś czas temu, jeśli zależało nam na stworzeniu wielowierszowej zmiennej typu string, czyli przechowującej ciągi znaków tekstowych, trzeba było tak naprawdę łączyć ze sobą wiele różnych stringów. Dodatkowo, aby poinformować program o zmianie wiersza należało użyć zapisu \n - co oznacza przejście do nowej linii.

1
2
3
4
5
let tekst = 'Idzie Grześ przez wieś \n Worek piasku niesie';

console.log(tekst); 
// “Idzie Grześ przez wieś
// Worek piasku niesie"

Nowy sposób na zapis stringów został zdecydowanie uproszczony, z pomocą przychodzi nam znak diakrytyczny - grawis. Dzięki niemu możemy oddzielać kolejne linijki tekstu tak jak w zwykłym dokumencie tekstowym.

1
2
3
4
5
6
let tekst = `Idzie Grześ przez wieś
Worek piasku niesie`;

console.log(tekst); 
// “Idzie Grześ przez wieś
// Worek piasku niesie”

3. Interpolacja stringów

Dzięki zapisowi stringów z użyciem grawisów dochodzi nam jeszcze jedna funkcjonalność, która znacznie poprawia czytelność kodu - interpolacja zmiennych i wyrażeń. Wcześniejszym sposobem na to, by dodać do zmiennej typu string wyrażenie dynamiczne trzeba było użyć zapisu z plusem:

1
2
3
4
let liczba = 16*16;
let napis = "16 do potęgi 2 to: " + liczba;

console.log(napis); // “16 do potęgi 2 to 256”

Interpolacja pozwala na dołączanie do stringa dowolnego wyrażenia bez konieczności tworzenia dodatkowych zmiennych czy stosowania konkatenacji. Wystarczy, że zastosujemy znaki grawis (`...`), a dołączane wyrażenie umieścimy w znakach ${...}:

1
2
3
let napis = `16 do potęgi 2 to ${16*16}`;;

console.log(napis); // “16 do potęgi 2 to 256”

4. Funkcje strzałkowe (Arrow functions)

ES6 wprowadziło do obiegu nowy sposób na tworzenie funkcji. Zrewolucjonizował on sposób na zapis bloku funkcyjnego, upraszczając przede wszystkim jego wygląd, czyniąc nasz kod zdecydowanie bardziej czytelnym. Stary sposób na zapis funkcji wygląda następująco:

1
2
3
let zwyklaFunkcja = function(a, b) {
    return a+b;
}

To co wyróżnia funkcje strzałkowe to ich prostota zapisu, a także możliwość użycia tzw. domniemanego return dla funkcji jednolinijkowych.

1
2
3
4
5
6
7
// Funkcja strzałkowa
let funkcjaStrzalkowa = (a, b) => {
    return a+b;
} 

// Funkcja strzałkowa jednolinijkowa
let funkcjaStrzalkowa = (a, b) => a+b; 

Jeszcze jednym ważnym elementem, który został zastosowany w funkcjach strzałkowych jest zmiana sposobu wykorzystywania słowa this. W starym zapisie funkcje nie posiadały tak naprawdę własnego this, zapożyczały je z kontekstu w jakim zostały wywołane przez co niespodziewanie potrafiły pojawiać się błędne i niezamierzone działania w kodzie. Arrow functions korzystają z mechanizmu o nazwie lexical this, co oznacza, że this użyte w tych funkcjach ma tę samą wartość co kontekst, w którym została utworzona funkcja. Arrow functions posiadają również kilka ograniczeń co uniemożliwia ich stosowanie chociażby jako konstruktorów, ale o tym opowiemy w osobnym artykule.

5. Domyślne parametry funkcji

W udoskonalonej składni wprowadzonej w ES6 pojawiła się również przydatna możliwość dotycząca parametrów funkcji, a konkretnie - dodawania wartości domyślnych do parametrów. Domyślne parametry mają za zadanie zastąpić zmienne konkretnymi wartościami, gdy nie zostaną one podane w momencie wywołania funkcji. Składnia takiego rozwiązania jest bardzo prosta, wystarczy że w miejscu na parametry funkcji od razu przypiszemy im domyślne wartości, tak jak w przykładzie poniżej:

1
2
3
4
let niePusta = (a = 1) => a;

console.log(niePusta(3)); // Wyświetli nam 3
console.log(niePusta()); // Wyświetli nam 1 jako wartość domyślną

Jest to o tyle przydatna funkcjonalność, że eliminuje ona problem wywołania funkcji z pustymi parametrami, co mogłoby powodować jej nieprawidłowe działanie, a w niektórych przypadkach wyrzucać błędy do konsoli.

6. Destrukturyzacja - wyciąganie zmiennych z obiektów i tablic

Jedną z nowszych funkcjonalności, z której korzystamy wręcz nałogowo jest destrukturyzacja (przypisania destruktywne). Pozwala ona na rozłożenie obiektu (lub tablicy) i przypisanie konkretnych należących do niego wartości wprost do nowo utworzonej zmiennej. Na pierwszy rzut oka może to brzmieć na nieco skomplikowany proces, jednak po zwróceniu uwagi na poniższy przykład wszystko powinno stać się jasne:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const samochod = {
    kolor: "Czerwony",
    predkoscMax: 260
}

let {kolor, predkoscMax} = samochod;


console.log(kolor); // Wyświetli nam “Czerwony”
console.log(predkoscMax); // Wyświetli nam 260

Jak widać, wystarczy zastosować odpowiednią notację i nazwy zmiennych. Istnieje jednak możliwość na podmianę nazwy zmiennej:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const samochod = {
    kolor: "Czerwony",
    predkoscMax: 260
}

let {kolor: barwa, predkoscMax} = samochod;


console.log(kolor); // ReferenceError: kolor is not defined
console.log(barwa); // Wyświetli nam “Czerwony”
console.log(predkoscMax); // Wyświetli nam 260

Wyżej wspomniałem mimochodem, że destrukturyzować można również tablice, proces ten jest tak samo prosty jak destrukturyzacja obiektów, zmienia się jedynie notacja:

1
2
3
4
5
6
const tablica = [1,3,5,7,11,13,17];
let {jedynka, trojka, piatka} = tablica;

console.log(jedynka); // Wyświetli nam 1
console.log(trojka); // Wyświetli nam 3
console.log(piatka); // Wyświetli nam 5

Patrząc na przykład można zauważyć, że wartości przypisywane z tablicy do zmiennych są wybierane kolejno jedna po drugiej, dlatego jeśli chcemy wybrać elementy leżące kilka miejsc od siebie wystarczy w zapisie pozostawić puste pola:

1
2
3
4
5
6
const tablica = [1,2,3,4,5];
let {jedynka, , trojka, , piatka} = tablica;

console.log(jedynka); // Wyświetli nam 1
console.log(trojka); // Wyświetli nam 3
console.log(piatka); // Wyświetli nam 5

7. Moduły - require, export, import

Każdy kto pracował już z użyciem frameworków takich jak Angular, React czy Vue.js, bądź używał w swoich projektach narzędzi jak Webpack lub Babel, doskonale zna pojęcie modularności. Dzięki modułom mamy możliwość umieszczania osobnych funkcjonalności czy komponentów w oddzielnych plikach, a następnie załadowywać je do pliku głównego. Takie rozwiązanie wprowadza zdecydowanie większy porządek do naszego kodu i sprawia, że łatwo się w nim odnaleźć. Aby przygotować elementy z pliku, takie jak funkcje, klasy, obiekty czy zmienne do bycia zaimportowanymi przez inny plik (np. plik główny), dodajemy do nich frazę export:

1
2
3
4
5
6
7
8
9
export default loggedIn;

function loggedIn (user) {
    if(user) {
        return true;
    } else {
        return false;
    }
}

Powyższy przykład to tzw. eksport domyślny, w każdym pliku tylko jeden eksport może mieć status domyślny. Jeżeli chcemy eksportować więcej elementów, powinniśmy zastosować eksporty nazwane:

1
2
3
4
5
export { healthPoints, stamina, power };

let healthPoints = 100;
let stamina = 100;
let power = 100;

Aby zaimportować w pliku powyższe moduły należy zastosować frazę import:

1
2
3
4
5
import userLogged from "./fileName"; 
// userLogged to tak naprawdę funkcja logged in, nazwy importowanych funkcji domyślnych nie muszą być identyczne

import { healthPoints, stamina, power } from "./fileName2";
// Należy pamiętać o dodaniu prawidłowej ścieżki do pliku z exportami

Sposobów eksportu i importu funkcjonalności jest wiele, istnieje również coś takiego jak Common JavaScript Modules, czyli typ modułów stosowanych stricte po stronie serwera z użyciem Node.js. Stworzenie modułów w ES6 miało na celu połączenie wszystkich typów modułów, czyniąc tą funkcjonalność prostą, asynchroniczną oraz działającą zarówno po stronie serwera jak i przeglądarki.

8. Generatory

Generatory to elementy specjalne języka JavaScript, za pomocą których możemy tworzyć procesy. Procesy to inaczej funkcje, które potrafią zatrzymywać swoje działanie i wracać do dalszego wykonania w odpowiednim momencie. Ciało generatora wygląda podobnie do zwykłej funkcji:

1
2
3
4
5
6
7
8
9
function* engineStart() {
    console.log("Wciśnij sprzęgło");
    yield "Sprzęgło wciśnięte";
    console.log("Przekręć kluczyk");
    yield "Kluczyk przekręcony";
    console.log("Wrzuć bieg");
    yield "Bieg wrzucony";
    console.log("Wciśnij gaz");
}

Łatwo można zauważyć, że w bloku generatora pojawia się słowo kluczowe - yield, służy ono do zatrzymania procesu działania. Dzięki niemu, podczas wywołania generatora za pomocą .next(), zostaną wykonane wszystkie polecenia znajdujące się przed słowem yield.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const engineProcedure = engineStart(); 
// Utworzenie instancji generatora engineStart()

const firstStep = engineProcedure.next(); // “Wciśnij sprzęgło”
console.log(firstStep); // { done: false, value: “Sprzęgło wciśnięte” }

const secondStep = engineProcedure.next(); // “Przekręć kluczyk”
console.log(secondStep); // { done: false, value: “Kluczyk przekręcony” }

const thirdStep = engineProcedure.next(); // “Wrzuć bieg”
console.log(thirdStep); // { done: false, value: “Bieg wrzucony” }

const fourthStep = engineProcedure.next(); // “Wciśnij gaz”
console.log(fourthStep); // { done: true, value: undefined }

Dzięki zastosowaniu generatorów możemy kontrolować wykonywane procesy np. w zależności od informacji przekazanej przez yield w parametrze value.

9. Mapy

Mapa to nowa struktura danych, która pozwala na tworzenie zbiorów przechowujących wartości parami: klucz - wartość. Mapy do złudzenia przypominają klasyczne obiekty, jednak to co stanowi ich wyróżnik, to klucze będące dowolnym typem danych. Aby stworzyć mapę musimy ją zadeklarować, a następnie z użyciem .set() przypisujemy jej kolejne klucze i wartości:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const myMap = new Map();
myMap.set("worker1","Programmer");
myMap.set("worker2", "Designer");
myMap.set("worker3", "DevOps");

// lub

const myMap = new Map([
    ["worker1", "Programmer"],
    ["worker2", "Designer"],
    ["worker3", "DevOps"],
]);

Jak wspomniałem wyżej, klucz może być dowolnego typu - dosłownie. Spójrzcie na poniższy przykład gdzie klucz jest obiektem:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const myMap = new Map();

const worker1 = { name: "Jadzia" }
const worker2 = { name: "Józef" }
const worker3 = { name: "Halina" }

myMap.set([
    [worker1, "Programmer"],
    [worker2, "Designer"],
    [worker3, "DevOps"],
]);

Podsumowanie

Nie bez powodu mówi się, że ES6 to rewolucja sposobu w jaki piszemy JavaScript. Znajomość nowych lub udoskonalonych funkcjonalności z pewnością pozwoli pisać kod, który będzie optymalny, efektywny, z mniejszą ilością błędów, a także skalowalny i zrozumiały dla reszty programistów, którzy będą nad nim pracować. Należy jednak pamiętać, że praktyka i ćwiczenia są najważniejsze, a stałe pozyskiwanie wiedzy i bycie ‘na bieżąco’ to podstawa bycia dobrym programistą. To co przedstawiłem w powyższym artykule to jedynie wierzchołek góry lodowej możliwości, które uznałem że zasługują na wyróżnienie. Jeśli chcesz porozmawiać o nowościach technologicznych w IT skontaktuj się z nami lub wyślij maila na kontakt@webalize.me.