jakubszpil

Renesans w JS, czyli nowoczesny JavaScript

📆
/
18 minut czytania

Witaj w świecie nowoczesnego JavaScriptu! 🚀 Jeśli HTML to szkielet strony, a CSS to jej wygląd, to JavaScript jest sercem i mózgiem – sprawia, że strona ożywa, staje się interaktywna i dynamiczna. W ciągu ostatnich lat JavaScript przeszedł prawdziwy "renesans". Dzięki wprowadzeniu standardu ES6 (ECMAScript 2015) i kolejnych aktualizacji, pisanie kodu w JS stało się znacznie przyjemniejsze, bezpieczniejsze i bardziej efektywne.

Ten przewodnik zabierze Cię w podróż po najważniejszych nowościach, które musisz znać, aby pisać nowoczesny JavaScript. Nie martw się, jeśli jesteś początkujący/a – wszystko wyjaśnimy krok po kroku, z przykładami i analogiami, byś mógł/mogła od razu zastosować nową wiedzę w praktyce. Czas unowocześnić swoje umiejętności! ✨

>>Spis treści

  1. Zmienne: let, const i zakresy blokowe – pożegnanie z var?
  2. Funkcje strzałkowe (Arrow Functions) i ich zastosowania – krócej i czytelniej
  3. Szablony stringów (Template Literals) – łatwiejsza praca z tekstem
  4. Destrukturyzacja tablic i obiektów – wyciągnij to, czego potrzebujesz
  5. Spread i Rest Operator (...) – elastyczność w manipulacji danymi
  6. Promise API i obsługa asynchroniczności – opowiadaj historie o przyszłości
  7. Async/await – nowoczesna obsługa kodu asynchronicznego – pisz asynchronicznie, myśl synchronicznie
  8. Zadania do wykonania: Sprawdź swoją wiedzę!

>>Zmienne: let, const i zakresy blokowe – pożegnanie z var?

W starszych wersjach JavaScriptu zmienne deklarowaliśmy wyłącznie za pomocą słowa kluczowego var. Często prowadziło to do pomyłek i trudnych do wykrycia błędów. Nowoczesny JavaScript wprowadza dwa nowe sposoby deklarowania zmiennych: let i const, które znacznie poprawiają bezpieczeństwo i przewidywalność kodu.

>>>Zakres blokowy (block scope)

To kluczowa różnica! Zmienne zadeklarowane za pomocą let i const mają zakres blokowy. Co to znaczy? Oznaczają, że zmienna istnieje tylko w obrębie najbliższego bloku kodu, czyli wszystko, co znajduje się między klamrowymi nawiasami { ... }.

Pomyśl o tym jak o specjalnej, prywatnej przestrzeni. Jeśli zmienna x jest zadeklarowana wewnątrz bloku if, będzie dostępna tylko w tym bloku. Próba użycia jej poza nim spowoduje błąd. To chroni Cię przed przypadkowym nadpisaniem zmiennych o tej samej nazwie w innej części kodu.

Przykład:

if (true) {
  let x = 5; // x istnieje tylko wewnątrz tego bloku if
  const y = 10; // y również istnieje tylko wewnątrz tego bloku
  console.log(x); // Wyświetli: 5 (dostępne, bo jesteśmy w bloku)
}
// console.log(x); // WYWOŁA BŁĄD! ReferenceError: x is not defined
// console.log(y); // WYWOŁA BŁĄD! ReferenceError: y is not defined

Dla porównania, var ma zakres funkcyjny (function scope), co oznacza, że zmienna zadeklarowana w funkcji jest dostępna w całej funkcji, ale poza funkcją jest dostępna globalnie, co jest źródłem wielu problemów. Dlatego w nowoczesnym kodzie zdecydowanie unikamy var.

>>>Różnice między let, const i var

Przykład const z obiektem/tablicą:

const tablica = [1, 2, 3];
tablica.push(4); // DOZWOLONE! Zmieniamy zawartość tablicy, nie przypisujemy nowej tablicy.
console.log(tablica); // Wyświetli: [1, 2, 3, 4]

// tablica = [5, 6]; // WYWOŁA BŁĄD! TypeError: Assignment to constant variable.
// Próbujesz przypisać zupełnie nową tablicę do stałej referencji.

const osoba = { imie: "Anna", wiek: 28 };
osoba.wiek = 29; // DOZWOLONE! Zmieniamy właściwość obiektu, nie przypisujemy nowego obiektu.
console.log(osoba); // Wyświetli: { imie: "Anna", wiek: 29 }

// osoba = { nazwisko: "Nowak" }; // WYWOŁA BŁĄD!

>>Funkcje strzałkowe (Arrow Functions) i ich zastosowania – krócej i czytelniej

Funkcje strzałkowe to zwięzła alternatywa dla tradycyjnych funkcji JavaScript, która została wprowadzona w ES6. Są nie tylko krótsze, ale także rozwiązują pewien problem z kontekstem this, który często mylił początkujących programistów.

>>>Składnia funkcji strzałkowej

Największą zaletą funkcji strzałkowych jest ich zwięzła składnia, zwłaszcza dla prostych funkcji.

>>>Funkcje strzałkowe a kontekst this

To jest jeden z najważniejszych powodów, dla których wprowadzono funkcje strzałkowe. W tradycyjnych funkcjach, wartość this zależy od tego, jak funkcja jest wywoływana. To prowadziło do częstych błędów, zwłaszcza w callbackach (funkcjach wywoływanych przez inne funkcje, np. w setTimeout czy w obsłudze zdarzeń).

Funkcje strzałkowe rozwiązują ten problem: nie mają własnego this. Zamiast tego, dziedziczą this z otoczenia, w którym zostały zadeklarowane (tzw. leksykalny this).

Przykład problemu ze this w tradycyjnych funkcjach:

// Tradycyjna funkcja, która "gubi" kontekst this
function TimerProblem() {
  this.sekundy = 0;
  setInterval(function () {
    // Tutaj 'this' nie odnosi się do obiektu TimerProblem,
    // ale do obiektu globalnego (np. window w przeglądarce) lub jest undefined w trybie 'strict mode'
    this.sekundy++; // To nie zadziała tak, jak byśmy chcieli!
    console.log(this.sekundy);
  }, 1000);
}
// new TimerProblem(); // W konsoli prawdopodobnie zobaczysz NaN lub błąd

Rozwiązanie z funkcją strzałkową:

function TimerPoprawny() {
  this.sekundy = 0;
  // Funkcja strzałkowa dziedziczy 'this' z TimerPoprawny (czyli z obiektu TimerPoprawny)
  setInterval(() => {
    this.sekundy++;
    console.log(`Sekundy: ${this.sekundy}`);
  }, 1000);
}
new TimerPoprawny(); // Będzie poprawnie liczyć sekundy: Sekundy: 1, Sekundy: 2, itd.

Dzięki temu funkcje strzałkowe są idealne do użycia w callbackach, metodach obiektów (choć nie jako metody obiektów najwyższego poziomu, bo wtedy this wskazałoby na window), czy w sytuacjach, gdzie chcesz, aby this zachowało kontekst nadrzędny.


>>Szablony stringów (Template Literals) – łatwiejsza praca z tekstem

Szablony stringów, wprowadzone w ES6, to potężne narzędzie do tworzenia ciągów znaków (stringów). Zastępują tradycyjne łączenie stringów za pomocą operatora + i oferują znacznie większą elastyczność i czytelność. Używamy ich, otaczając tekst backtickami (apostrofami wstecznymi) zamiast pojedynczych lub podwójnych cudzysłowów.

>>>Interpolacja i wielolinijkowość

>>>Dodatkowe możliwości

Szablony stringów wspierają także tzw. tagowane szablony (tagged templates), które pozwalają na zaawansowaną manipulację tekstem przed jego finalnym utworzeniem. Możesz zdefiniować funkcję ("tag"), która będzie przetwarzać części stringu i wartości zmiennych. Jest to bardziej zaawansowana funkcja, używana np. do tłumaczeń, walidacji, bezpiecznego generowania HTML (zapobieganie XSS) czy formatowania danych.

// Przykład funkcji "tagującej"
function upper(strings, ...values) {
  // 'strings' to tablica tekstów, 'values' to tablica zmiennych
  let wynik = strings[0];
  for (let i = 0; i < values.length; i++) {
    wynik += values[i].toUpperCase() + strings[i + 1]; // Zamienia zmienne na duże litery
  }
  return wynik;
}

const imie = "Kasia";
const wiek = 20;
console.log(upper`Cześć, ${imie}! Masz ${wiek} lat?`); // Wyświetli: Cześć, KASIA! Masz 20 lat?

W tym przykładzie funkcja upper została "przypięta" do szablonu stringu i zmodyfikowała zmienną imie na wielkie litery.


>>Destrukturyzacja tablic i obiektów – wyciągnij to, czego potrzebujesz

Destrukturyzacja to potężna i bardzo często używana funkcja w nowoczesnym JavaScript. Pozwala na "rozpakowanie" wartości z tablic lub właściwości z obiektów do oddzielnych zmiennych w bardzo zwięzły i czytelny sposób. Upraszcza kod i czyni go bardziej deklaratywnym.

>>>Destrukturyzacja tablic

Pozwala na szybkie przypisanie wartości z tablicy do zmiennych, bazując na ich pozycji.

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

// Stary sposób:
// const kolor1 = kolory[0];
// const kolor2 = kolory[1];

// Nowy sposób z destrukturyzacją tablic:
const [pierwszyKolor, drugiKolor, trzeciKolor] = kolory;
console.log(pierwszyKolor); // Wyświetli: czerwony
console.log(drugiKolor); // Wyświetli: zielony

// Można pomijać niektóre elementy (używając przecinków):
const [a, , b] = [1, 2, 3]; // Pomijamy drugi element (2)
console.log(a, b); // Wyświetli: 1 3

// Można przypisać wartości domyślne, jeśli element nie istnieje:
const [x, y, z = "domyślny"] = ["A", "B"];
console.log(x, y, z); // Wyświetli: A B domyślny

// Można zbierać pozostałe elementy do nowej tablicy (rest operator - patrz dalej):
const [pierwszy, ...resztaKolorow] = kolory;
console.log(pierwszy); // Wyświetli: czerwony
console.log(resztaKolorow); // Wyświetli: ["zielony", "niebieski"]

>>>Destrukturyzacja obiektów

Pozwala na przypisanie wartości z obiektu do zmiennych na podstawie nazw ich kluczy (właściwości). Kolejność nie ma znaczenia, liczy się nazwa klucza.

const samochod = { marka: "Ford", model: "Focus", rok: 2018 };

// Stary sposób:
// const markaSamochodu = samochod.marka;
// const modelSamochodu = samochod.model;

// Nowy sposób z destrukturyzacją obiektów:
const { marka, model } = samochod;
console.log(marka); // Wyświetli: Ford
console.log(model); // Wyświetli: Focus

// Można przypisać wartości domyślne, jeśli właściwość nie istnieje:
const { marka: m, kolor = "czarny" } = samochod; // 'kolor' nie istnieje w obiekcie 'samochod'
console.log(m, kolor); // Wyświetli: Ford czarny

// Można zmienić nazwę zmiennej, do której przypisujemy wartość:
const { marka: nazwaMarki, model: typModelu } = samochod;
console.log(nazwaMarki, typModelu); // Wyświetli: Ford Focus

// Można też zbierać pozostałe właściwości do nowego obiektu (rest operator - patrz dalej):
const { rok, ...resztaSamochodu } = samochod;
console.log(rok); // Wyświetli: 2018
console.log(resztaSamochodu); // Wyświetli: { marka: "Ford", model: "Focus" }

Destrukturyzacja jest szczególnie przydatna w funkcjach, gdy chcesz łatwo wyciągnąć konkretne dane z obiektów konfiguracyjnych lub z wyników API.


>>Spread i Rest Operator (...) – elastyczność w manipulacji danymi

Operator ... (trzy kropki) jest jednym z najbardziej uniwersalnych i potężnych narzędzi w nowoczesnym JavaScript. Nazywany jest operatorem spread lub operatorem rest, w zależności od kontekstu, w jakim jest używany.

>>>Spread Operator (operator rozproszenia)

Gdy używasz ... do rozbicia elementów tablicy lub właściwości obiektu na pojedyncze elementy, mówimy o operatorze spread. Jest to jak "rozłożenie" zawartości kolekcji. 🌐

Zastosowania Spread Operatora:

>>>Rest Operator (operator reszty)

Gdy używasz ... do zbierania wielu elementów w jedną tablicę (lub właściwości w obiekt), mówimy o operatorze rest. Jest to jak "zbieranie" luźnych elementów w jedną kolekcję. 🧺

Zastosowania Rest Operatora:

Zarówno spread, jak i rest operator są niezwykle przydatne i sprawiają, że kod jest znacznie bardziej zwięzły i czytelny, szczególnie podczas pracy z tablicami i obiektami.


>>Promise API i obsługa asynchroniczności – opowiadaj historie o przyszłości

JavaScript jest językiem jednowątkowym, co oznacza, że może wykonywać tylko jedną operację w danym momencie. Jednak wiele operacji, takich jak pobieranie danych z internetu, odczyt plików, czy operacje na bazie danych, trwa pewien czas. Gdyby JavaScript musiał czekać na ich zakończenie, strona internetowa by się "zawiesiła" (nie reagowałaby na kliknięcia, przewijanie itp.).

Tutaj wkracza asynchroniczność. Pozwala ona na rozpoczęcie operacji, a następnie przejście do kolejnych zadań, nie czekając na zakończenie tej pierwszej. Kiedy asynchroniczna operacja się zakończy, JavaScript wróci do niej i wykona dalsze kroki.

Starszym sposobem obsługi asynchroniczności były callbacki (funkcje zwrotne), które jednak często prowadziły do tzw. "callback hell" (piekła callbacków) – zagnieżdżonego, trudnego do czytania i utrzymania kodu.

>>>Czym jest Promise?

Promise (Obietnica) to obiekt w JavaScript, który reprezentuje ostateczne zakończenie (lub niepowodzenie) operacji asynchronicznej. Możesz myśleć o Promise jako o "przyszłej wartości". Składasz obietnicę, że w przyszłości otrzymasz wynik – albo sukces, albo błąd. 📦

Promise może znajdować się w jednym z trzech stanów:

  1. pending (oczekujący): Początkowy stan; ani spełniony, ani odrzucony. Operacja asynchroniczna nadal trwa.
  2. fulfilled (spełniony): Operacja asynchroniczna zakończyła się sukcesem i zwróciła wartość.
  3. rejected (odrzucony): Operacja asynchroniczna zakończyła się błędem.

Możesz "podpiąć" funkcje, które zostaną wywołane, gdy Promise zmieni swój stan:

Przykład tworzenia i używania Promise:

const obietnica = new Promise((resolve, reject) => {
  // Symulujemy operację asynchroniczną (np. pobieranie danych)
  setTimeout(() => {
    const sukces = Math.random() > 0.5; // Losowo decydujemy o sukcesie lub błędzie

    if (sukces) {
      resolve("Dane pobrane pomyślnie!"); // Jeśli sukces, wywołujemy resolve z wynikiem
    } else {
      reject(new Error("Nie udało się pobrać danych.")); // Jeśli błąd, wywołujemy reject z błędem
    }
  }, 2000); // Operacja trwa 2 sekundy
});

console.log("Rozpoczęto operację...");

obietnica
  .then((wiadomosc) => {
    console.log("SUKCES:", wiadomosc); // Wykona się, jeśli obietnica się spełni
  })
  .catch((blad) => {
    console.error("BŁĄD:", blad.message); // Wykona się, jeśli obietnica zostanie odrzucona
  })
  .finally(() => {
    console.log("Operacja zakończona (niezależnie od wyniku)."); // Wykona się zawsze
  });

console.log("Kod po obietnicy nadal się wykonuje..."); // Pokazuje asynchroniczność

>>>Łańcuchy Promise (Promise Chaining)

Jedną z największych zalet Promise jest możliwość łączenia wielu operacji asynchronicznych w jeden czytelny "łańcuch". Każdy .then() zwraca nowy Promise, co pozwala na kolejne .then() wywołane po sobie. Jest to idealne, gdy jedna operacja asynchroniczna zależy od wyniku poprzedniej.

Przykład łańcucha Promise (pobieranie danych z API):

// fetch() to wbudowana funkcja JS, która zwraca Promise
fetch("https://jsonplaceholder.typicode.com/users/1") // Pobierz dane użytkownika o ID 1
  .then((response) => {
    // Pierwsze .then: sprawdź, czy odpowiedź jest OK i sparsuj ją jako JSON
    if (!response.ok) {
      throw new Error(`Błąd HTTP! Status: ${response.status}`);
    }
    return response.json(); // Zwróć kolejny Promise z danymi JSON
  })
  .then((user) => {
    // Drugie .then: otrzymaj sparsowane dane użytkownika
    console.log("Pobrany użytkownik:", user.name, user.email);
    return fetch(
      `https://jsonplaceholder.typicode.com/posts?userId=${user.id}`
    ); // Pobierz posty tego użytkownika
  })
  .then((response) => {
    // Trzecie .then: sprawdź odpowiedź i sparsuj posty
    if (!response.ok) {
      throw new Error(`Błąd HTTP! Status: ${response.status}`);
    }
    return response.json();
  })
  .then((posts) => {
    // Czwarte .then: otrzymaj posty
    console.log("Liczba postów użytkownika:", posts.length);
  })
  .catch((error) => {
    // .catch: Obsłuż każdy błąd, który wystąpił na dowolnym etapie łańcucha
    console.error("Wystąpił błąd w łańcuchu Promise:", error);
  })
  .finally(() => {
    console.log("Pobieranie danych zakończone.");
  });

Promise to podstawa nowoczesnego JS w obsłudze asynchroniczności, a kolejny rozdział pokaże Ci, jak jeszcze bardziej uprościć ich użycie.


>>Async/await – nowoczesna obsługa kodu asynchronicznego – pisz asynchronicznie, myśl synchronicznie

Mimo że Promise są ogromnym krokiem naprzód w porównaniu do callbacków, kod z długimi łańcuchami .then().then().catch() może wciąż być trudny do czytania. Standard ES2017 wprowadził słowa kluczowe async i await, które pozwalają pisać kod asynchroniczny w sposób, który wygląda i zachowuje się niemal identycznie jak kod synchroniczny, jednocześnie zachowując asynchroniczną naturę. To game changer! 🎮

>>>Uproszczenie pracy z Promise

Przykład użycia async/await (porównaj z przykładem z Promise Chaining):

async function pobierzDaneUzytkownikaIPosty() {
  try {
    console.log("Rozpoczynam pobieranie danych...");

    // Krok 1: Pobierz dane użytkownika
    const userResponse = await fetch(
      "https://jsonplaceholder.typicode.com/users/1"
    );
    if (!userResponse.ok) {
      throw new Error(
        `Błąd HTTP przy pobieraniu użytkownika! Status: ${userResponse.status}`
      );
    }
    const user = await userResponse.json(); // Poczekaj na parsowanie JSON
    console.log("Pobrany użytkownik:", user.name, user.email);

    // Krok 2: Pobierz posty tego użytkownika
    const postsResponse = await fetch(
      `https://jsonplaceholder.typicode.com/posts?userId=${user.id}`
    );
    if (!postsResponse.ok) {
      throw new Error(
        `Błąd HTTP przy pobieraniu postów! Status: ${postsResponse.status}`
      );
    }
    const posts = await postsResponse.json(); // Poczekaj na parsowanie JSON
    console.log("Liczba postów użytkownika:", posts.length);

    console.log("Wszystkie dane zostały pobrane pomyślnie.");
  } catch (error) {
    // Obsługa każdego błędu, który wystąpił na dowolnym etapie
    console.error("Wystąpił błąd w funkcji async:", error.message);
  } finally {
    console.log("Operacja pobierania danych zakończona.");
  }
}

// Wywołanie funkcji async
pobierzDaneUzytkownikaIPosty();
console.log("Kod poza funkcją async nadal się wykonuje..."); // Pokazuje asynchroniczność

Widzisz, jak kod stał się znacznie bardziej liniowy i czytelny? Wygląda to prawie jak tradycyjny kod synchroniczny, mimo że w tle nadal działają Promise i cała magia asynchroniczności.

>>>Zwracanie wartości i obsługa błędów

async/await to obecnie najczęściej rekomendowany sposób pracy z asynchronicznością w JavaScript, ponieważ znacząco poprawia czytelność i łatwość utrzymania kodu, sprawiając, że złożone operacje asynchroniczne stają się bardziej przejrzyste.


>>Zadania do wykonania: Sprawdź swoją wiedzę!

Czas na praktykę! Rozwiąż te zadania, aby utrwalić to, czego się nauczyłeś/aś. Po każdym zadaniu możesz rozwinąć sekcję Pokaż rozwiązanie, aby sprawdzić swój kod. Powodzenia! 💪

>>>Zadanie 1

Napisz funkcję strzałkową, która przyjmuje dowolną liczbę argumentów numerycznych i zwraca ich sumę. Użyj rest operatora do zebrania argumentów.

Pokaż rozwiązanie
const sumaWszystkichLiczb = (...liczby) => {
  return liczby.reduce(
    (akumulator, aktualnaLiczba) => akumulator + aktualnaLiczba,
    0
  );
};

console.log(sumaWszystkichLiczb(1, 2, 3)); // Oczekiwany wynik: 6
console.log(sumaWszystkichLiczb(10, 20, 30, 40)); // Oczekiwany wynik: 100
console.log(sumaWszystkichLiczb()); // Oczekiwany wynik: 0

>>>Zadanie 2

Utwórz obiekt reprezentujący produkt z kluczami: nazwa, cena, dostepny. Następnie użyj destrukturyzacji obiektu, aby wyodrębnić te wartości do osobnych zmiennych i wyświetlić je w konsoli. Dodaj również zmienną producent z wartością domyślną "Nieznany", na wypadek gdyby nie było jej w obiekcie.

Pokaż rozwiązanie
const produkt = { nazwa: "Książka JS", cena: 49.99, dostepny: true };

const { nazwa, cena, dostepny, producent = "Nieznany" } = produkt;

console.log(`Nazwa: ${nazwa}`); // Oczekiwany wynik: Nazwa: Książka JS
console.log(`Cena: ${cena} zł`); // Oczekiwany wynik: Cena: 49.99 zł
console.log(`Dostępny: ${dostepny}`); // Oczekiwany wynik: Dostępny: true
console.log(`Producent: ${producent}`); // Oczekiwany wynik: Producent: Nieznany

>>>Zadanie 3

Stwórz tablicę ulubionych filmów. Następnie użyj operatora spread do utworzenia nowej tablicy, która będzie zawierać wszystkie te filmy plus dwa dodatkowe filmy na końcu. Wyświetl nową tablicę w konsoli.

Pokaż rozwiązanie
const ulubioneFilmy = ["Incepcja", "Matrix", "Interstellar"];

const nowaListaFilmow = [...ulubioneFilmy, "Pulp Fiction", "Forrest Gump"];

console.log(nowaListaFilmow);
// Oczekiwany wynik: ["Incepcja", "Matrix", "Interstellar", "Pulp Fiction", "Forrest Gump"]

>>>Zadanie 4

Napisz funkcję asynchroniczną o nazwie pobierzWiadomosc, która symuluje pobieranie wiadomości z serwera. Funkcja powinna używać setTimeout, aby po 3 sekundach zwrócić tekst "Wiadomość pobrana!". Użyj async/await do wywołania tej funkcji i wyświetlenia jej wyniku w konsoli. Dodaj również blok try...catch do obsługi potencjalnych błędów.

Pokaż rozwiązanie
// Funkcja symulująca pobieranie danych (zwraca Promise)
const pobierzWiadomoscAPI = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Możesz zmienić to na reject, aby przetestować obsługę błędów
      if (Math.random() > 0.1) {
        // 90% szans na sukces, 10% na błąd
        resolve("Wiadomość pobrana pomyślnie!");
      } else {
        reject(new Error("Błąd podczas pobierania wiadomości!"));
      }
    }, 3000); // Symulujemy 3 sekundy oczekiwania
  });
};

// Funkcja asynchroniczna używająca async/await
async function pobierzWiadomosc() {
  console.log("Rozpoczynam pobieranie wiadomości...");
  try {
    const wiadomosc = await pobierzWiadomoscAPI(); // Czekaj, aż Promise się spełni
    console.log("Sukces:", wiadomosc);
  } catch (error) {
    console.error("Wystąpił błąd:", error.message); // Złap i wyświetl błąd
  } finally {
    console.log("Zakończono próbę pobierania wiadomości.");
  }
}

// Wywołaj funkcję asynchroniczną
pobierzWiadomosc();
console.log(
  "Ten tekst wyświetli się natychmiast po uruchomieniu programu, zanim wiadomość zostanie pobrana."
);

Gratulacje! 🎉 Dotarłeś/aś do końca kursu wprowadzającego w nowoczesny JavaScript. Masz teraz solidne podstawy do dalszego rozwoju. Pamiętaj, że kluczem jest praktyka – im więcej będziesz kodować, tym lepiej zrozumiesz te koncepcje.

Co chciałbyś/chciałabyś zgłębić dalej? Może:

Daj znać! 😊

Quiz: Podstawy nowoczesnego JavaScript

Pytanie 1 z 5

Jaką główną różnicę między let a var podkreśla się w nowoczesnym JavaScript?

Widzisz jakiś błąd, bądź literówkę? Chcesz coś poprawić?✏️ Przejdź do edycji tego pliku