Lekcja 12: Nowoczesny JavaScript (ES6+) - Część 1

ECMAScript (ES) to standard, na którym opiera się JavaScript. Od 2015 roku (wersja ES6, znana też jako ES2015), co roku pojawiają się nowe wersje standardu, wprowadzające ulepszenia i nowe funkcje do języka. W tej i kolejnych lekcjach przyjrzymy się najważniejszym z nich, które znacząco ułatwiają i usprawniają pisanie kodu JavaScript.

let i const - Nowe deklaracje zmiennych

ES6 wprowadziło dwa nowe słowa kluczowe do deklarowania zmiennych, let i const, które w większości przypadków powinny zastąpić tradycyjne var.

// Zakres blokowy
if (true) {
    var x = 10; // var ma zakres funkcyjny lub globalny
    let y = 20; // let ma zakres blokowy
    const z = 30; // const ma zakres blokowy
    console.log("Wewnątrz bloku:", y, z);
}

console.log("Poza blokiem x:", x); // 10 (var jest widoczny)
// console.log("Poza blokiem y:", y); // Błąd: y is not defined
// console.log("Poza blokiem z:", z); // Błąd: z is not defined

// Re-przypisanie
let a = 5;
a = 6; // OK

const b = 10;
// b = 11; // Błąd: Assignment to constant variable.

// Uwaga dla const z obiektami/tablicami
const osoba = { imie: "Jan" };
osoba.imie = "Anna"; // OK - modyfikujemy zawartość obiektu, a nie samą referencję
// osoba = { imie: "Piotr" }; // Błąd - próba przypisania nowej referencji do stałej

const tablica = [1, 2];
tablica.push(3); // OK - modyfikujemy zawartość tablicy
// tablica = [1, 2, 3]; // Błąd - próba przypisania nowej referencji

Zalecenie: Używaj const domyślnie. Jeśli wiesz, że wartość zmiennej będzie musiała się zmienić, użyj let. Unikaj używania var w nowoczesnym kodzie.

Funkcje strzałkowe (Arrow Functions) - Powtórzenie i `this`

Przypomnienie składni:

// Tradycyjna funkcja
const sumaTradycyjna = function(a, b) {
    return a + b;
};

// Funkcja strzałkowa
const sumaStrzalkowa = (a, b) => a + b;

// Z jednym parametrem
const kwadrat = x => x * x;

// Bez parametrów
const losuj = () => Math.random();

// Z wieloma instrukcjami (wymaga bloku {})
const przetworz = (x) => {
    console.log("Przetwarzam:", x);
    return x * 2;
};

Kluczowa różnica: zachowanie `this`

Funkcje strzałkowe nie mają własnego kontekstu this. Zamiast tego, dziedziczą this z otaczającego je zakresu leksykalnego (miejsca, w którym zostały zdefiniowane w kodzie). Jest to bardzo przydatne, szczególnie wewnątrz metod obiektów i funkcji zwrotnych (callbacków).

const licznik = {
    wartosc: 0,
    // Metoda z tradycyjną funkcją (ma własne 'this')
    inkrementujTradycyjnie: function() {
        console.log("Tradycyjne this:", this); // Wskazuje na obiekt 'licznik'
        this.wartosc++;
    },
    // Metoda z funkcją strzałkową (dziedziczy 'this' z zakresu obiektu)
    dekrementujStrzalkowo: () => {
        // UWAGA: Tutaj 'this' NIE wskazuje na 'licznik'!
        // Wskazuje na 'this' z zakresu, gdzie zdefiniowano obiekt 'licznik'
        // (w tym przypadku globalne 'window' lub 'undefined' w trybie ścisłym)
        console.log("Strzałkowe this:", this);
        // this.wartosc--; // To spowoduje błąd lub nie zadziała zgodnie z oczekiwaniami
    },
    // Poprawne użycie funkcji strzałkowej w callbacku
    startAutoInkrementacji: function() {
        console.log("Start auto-inkrementacji, this:", this); // Wskazuje na 'licznik'
        // Zapisujemy ID interwału, aby móc go później zatrzymać
        this.intervalId = setInterval(() => {
            // Funkcja strzałkowa dziedziczy 'this' z metody startAutoInkrementacji
            console.log("Callback setInterval this:", this); // Wskazuje na 'licznik'
            this.wartosc++;
            console.log("Aktualna wartość:", this.wartosc);
        }, 1000);
    },
    stopAutoInkrementacji: function() {
        clearInterval(this.intervalId);
        console.log("Zatrzymano auto-inkrementację.");
    },
    // Dla porównania - problem z 'this' w tradycyjnym callbacku
    startAutoDekrementacjiProblem: function() {
        setInterval(function() {
            // Tradycyjna funkcja w setInterval ma własne 'this'
            // (zazwyczaj 'window' lub 'undefined')
            console.log("Callback tradycyjny this:", this);
            // this.wartosc--; // Nie zadziała zgodnie z oczekiwaniami
        }, 1000);
    }
};

licznik.inkrementujTradycyjnie(); // wartosc = 1
// licznik.dekrementujStrzalkowo(); // Pokazuje problem z 'this'
// licznik.startAutoInkrementacji(); // Działa poprawnie dzięki funkcji strzałkowej
// setTimeout(() => licznik.stopAutoInkrementacji(), 5000); // Zatrzymanie po 5 sekundach
// licznik.startAutoDekrementacjiProblem(); // Pokazuje problem z 'this' w tradycyjnym callbacku

Szablony Literałowe (Template Literals)

Pozwalają na łatwiejsze tworzenie stringów, zwłaszcza tych zawierających zmienne lub wyrażenia oraz stringów wieloliniowych. Używamy do tego odwrotnych apostrofów (backticks) ` `.

let imie = "Alicja";
let wiek = 25;

// Stary sposób (konkatenacja)
let powitanieStare = "Witaj, " + imie + "! Masz " + wiek + " lat.";
console.log(powitanieStare);

// Nowy sposób (template literals)
let powitanieNowe = `Witaj, ${imie}! Masz ${wiek} lat.`;
console.log(powitanieNowe);

// Z wyrażeniem
let cena = 100;
let podatek = 0.23;
let infoCena = `Cena netto: ${cena} PLN, cena brutto: ${cena * (1 + podatek)} PLN.`;
console.log(infoCena);

// String wieloliniowy
let htmlFragment = `
<div>
    <h2>${imie}</h2>
    <p>Wiek: ${wiek}</p>
</div>
`;
console.log(htmlFragment);

Destrukturyzacja (Destructuring Assignment)

Specjalna składnia pozwalająca na "rozpakowanie" wartości z tablic lub właściwości z obiektów do osobnych zmiennych. Upraszcza dostęp do danych.

Destrukturyzacja tablic

let kolory = ["czerwony", "zielony", "niebieski"];

// Stary sposób
// let pierwszyKolor = kolory[0];
// let drugiKolor = kolory[1];

// Destrukturyzacja
let [pierwszy, drugi, trzeci] = kolory;
console.log(pierwszy); // "czerwony"
console.log(drugi);   // "zielony"
console.log(trzeci);  // "niebieski"

// Pominięcie elementów
let [kolor1, , kolor3] = kolory;
console.log(kolor1); // "czerwony"
console.log(kolor3); // "niebieski"

// Wartości domyślne
let [k1, k2, k3, k4 = "czarny"] = kolory;
console.log(k4); // "czarny"

// Z parametrami resztowymi
let liczby = [1, 2, 3, 4, 5];
let [pierwszaLiczba, drugaLiczba, ...resztaLiczb] = liczby;
console.log(pierwszaLiczba); // 1
console.log(drugaLiczba);  // 2
console.log(resztaLiczb);  // [3, 4, 5]

// Zamiana wartości zmiennych
let x = 10;
let y = 20;
[x, y] = [y, x];
console.log(x, y); // 20 10

Destrukturyzacja obiektów

Używamy nawiasów klamrowych {}, a nazwy zmiennych muszą odpowiadać nazwom właściwości obiektu.

let samochod = {
    marka: "Opel",
    model: "Astra",
    rok: 2020,
    kolor: "szary"
};

// Stary sposób
// let markaAuta = samochod.marka;
// let modelAuta = samochod.model;

// Destrukturyzacja
let { marka, model, rok } = samochod;
console.log(marka); // "Opel"
console.log(model); // "Astra"
console.log(rok);   // 2020

// Zmiana nazwy zmiennej
let { marka: brand, model: carModel } = samochod;
console.log(brand);    // "Opel"
console.log(carModel); // "Astra"

// Wartości domyślne
let { kolor = "czarny", przebieg = 0 } = samochod;
console.log(kolor);    // "szary" (bo istnieje w obiekcie)
console.log(przebieg); // 0 (bo nie istnieje w obiekcie)

// Destrukturyzacja zagnieżdżonych obiektów
let uzytkownik = {
    id: 123,
    dane: {
        imie: "Ewa",
        email: "ewa@example.com"
    }
};
let { id, dane: { imie, email } } = uzytkownik;
console.log(id);    // 123
console.log(imie);  // "Ewa"
console.log(email); // "ewa@example.com"

// Destrukturyzacja w parametrach funkcji
function wyswietlDane({ marka, model }) {
    console.log(`Samochód: ${marka} ${model}`);
}
wyswietlDane(samochod); // Samochód: Opel Astra

Zadanie praktyczne

Masz obiekt reprezentujący użytkownika:

const user = {
    firstName: "Marta",
    lastName: "Nowak",
    age: 32,
    city: "Poznań"
};

Użyj destrukturyzacji obiektów, aby wydobyć właściwości firstName i age do osobnych zmiennych. Następnie użyj szablonów literałowych, aby stworzyć string powitalny w formacie: "Witaj Marta! Masz 32 lata." i wyświetl go w konsoli.

Pokaż rozwiązanie
const user = {
    firstName: "Marta",
    lastName: "Nowak",
    age: 32,
    city: "Poznań"
};

// Destrukturyzacja
const { firstName, age } = user;

// Szablon literałowy
const greeting = `Witaj ${firstName}! Masz ${age} lata.`;

console.log(greeting); // Witaj Marta! Masz 32 lata.

Zadanie do samodzielnego wykonania

Masz tablicę z wynikami: const scores = [150, 210, 180, 300, 195];. Użyj destrukturyzacji tablic, aby przypisać pierwszy wynik do zmiennej firstScore, drugi wynik do zmiennej secondScore, a resztę wyników do tablicy otherScores. Wyświetl te trzy zmienne w konsoli.

FAQ - Nowoczesny JavaScript (ES6+) - Część 1

Czy `var` jest całkowicie przestarzałe?

Technicznie `var` nadal działa, ale jego użycie jest zdecydowanie odradzane w nowoczesnym kodzie ze względu na mylący zakres funkcyjny i hoisting. Używanie `let` i `const` prowadzi do czystszego i mniej podatnego na błędy kodu.

Czy `const` oznacza, że wartość jest niezmienna (immutable)?

Nie do końca. `const` gwarantuje tylko, że sama zmienna nie zostanie ponownie przypisana. Jeśli `const` przechowuje referencję do obiektu lub tablicy, zawartość tego obiektu lub tablicy nadal może być modyfikowana (np. zmiana właściwości obiektu, dodanie elementu do tablicy).

Kiedy funkcja strzałkowa nie jest dobrym wyborem?

Głównie wtedy, gdy potrzebujesz, aby funkcja miała własny kontekst `this` dynamicznie powiązany w momencie wywołania. Dotyczy to np. metod obiektów, które mają być wywoływane w tradycyjny sposób (obiekt.metoda()) lub funkcji konstruktorów.

Czy mogę używać destrukturyzacji do pomijania elementów w środku tablicy?

Tak, możesz użyć pustego miejsca oddzielonego przecinkiem, aby pominąć element podczas destrukturyzacji tablicy. Na przykład: let [a, , c] = [1, 2, 3]; przypisze 1 do `a` i 3 do `c`, ignorując 2.

Czy destrukturyzacja obiektów działa tylko dla właściwości istniejących w obiekcie?

Jeśli spróbujesz zdestrukturyzować właściwość, która nie istnieje w obiekcie, zmienna otrzyma wartość `undefined`, chyba że podasz wartość domyślną za pomocą znaku równości (np. let { name, age = 18 } = user;).

Czy szablony literałowe są szybsze niż konkatenacja stringów?

Różnice w wydajności są zazwyczaj minimalne i nie powinny być głównym kryterium wyboru. Szablony literałowe oferują znacznie lepszą czytelność i wygodę pisania kodu, zwłaszcza przy złożonych stringach z wieloma zmiennymi.