1Wprowadzenie: HTML, CSS, JS - Wzajemne relacje

Podział odpowiedzialności w nowoczesnym tworzeniu stron WWW

HTML (HyperText Markup Language) stanowi szkielet każdej strony internetowej. Definiuje on strukturę dokumentu, semantykę poszczególnych elementów oraz treść, która będzie wyświetlana użytkownikowi. Bez HTML strona nie miałaby żadnej formy organizacyjnej.

CSS (Cascading Style Sheets) odpowiada za wizualną prezentację dokumentu HTML. Za pomocą CSS określamy kolory, czcionki, układ elementów na stronie (layout), animacje oraz responsywność (dostosowanie do różnych rozmiarów ekranów). CSS to właściwie "skóra" naszej strony.

JavaScript to język programowania, który dodaje do strony interaktywność i logikę działania. JS pozwala na reagowanie na akcje użytkownika (kliknięcia, wpisywanie tekstu), komunikację z serwerem, modyfikowanie treści strony bez jej przeładowania oraz wiele więcej. Można powiedzieć, że JS jest "mózgiem" strony.

Te trzy technologie współpracują ze sobą w harmonii. HTML dostarcza strukturę, CSS nadaje jej wygląd, a JavaScript ożywia stronę, czyniąc ją dynamiczną i interaktywną. Żaden nowoczesny projekt webowy nie istnieje bez synergii tych trzech filarów internetu.

<!DOCTYPE html>
<html lang="pl">
<head>
    <title>Synergia</title>
</head>
<body>
    <h1 id="tytul">Treść HTML</h1>
    <script src="app.js"></script>
</body>
</html>

HTML, CSS i JavaScript to trzy fundamenty nowoczesnego tworzenia stron internetowych, które współpracują ze sobą na zasadzie ścisłego podziału odpowiedzialności. HTML zapewnia strukturę i semantykę dokumentu, definiując nagłówki, akapity, listy, tabele i inne elementy treści. Bez HTML strona internetowa byłaby pozbawiona organizacji i znaczenia dla przeglądarek oraz robotów indeksujących. CSS odpowiada za warstwę wizualną, określając kolorystykę, typografię, układ elementów i responsywność dostosowującą się do różnych rozdzielczości ekranów.

JavaScript wprowadza interaktywność i dynamiczne zachowanie, umożliwiając reakcję na zdarzenia użytkownika, komunikację z serwerem oraz modyfikację treści strony bez przeładowania. W praktyce front-end developer musi biegle operować wszystkimi trzema technologiami, rozumiejąc ich wzajemne zależności i ograniczenia. Nowoczesne frameworki takie jak React, Vue czy Angular abstrahują część tej złożoności, ale solidne podstawy HTML, CSS i JS pozostają niezbędne dla każdego profesjonalisty w branży web developmentu.

2Anatomia Dokumentu HTML: HEAD vs BODY

Dwie główne sekcje dokumentu HTML

Dokument HTML jest podzielony na dwie podstawowe, ściśle określone części: sekcję <head> (nazywaną również "głową" dokumentu) oraz sekcję <body> (ciało dokumentu).

Sekcja <head> zawiera informacje konfiguracyjne i metadane, które nie są bezpośrednio widoczne dla użytkownika, ale są kluczowe dla prawidłowego działania strony. Znajdują się tutaj: kodowanie znaków (<meta charset="UTF-8">), tytuł strony wyświetlany na karcie przeglądarki, linki do zewnętrznych arkuszy stylów CSS, skrypty JavaScript, a także metadane SEO (opis strony, słowa kluczowe) wykorzystywane przez wyszukiwarki internetowe.

Sekcja <body> zawiera całą widoczną treść strony internetowej - teksty, obrazy, linki, formularze, tabele, nagłówki, akapity i wszystkie inne elementy, które użytkownik może zobaczyć i z którymi może wchodzić w interakcję. To jest właściwa zawartość dokumentu HTML.

<head>
    <meta name="description" content="Opis strony">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <p>Widoczny tekst.</p>
</body>

Sekcja head dokumentu HTML pełni kluczową rolę w konfiguracji strony, zawierając metadane, które nie są bezpośrednio widoczne dla użytkownika, ale mają ogromny wpływ na działanie i pozycjonowanie witryny. W head umieszczamy deklarację kodowania znaków meta charset, bez której polskie znaki diakrytyczne mogą być wyświetlane nieprawidłowo. Tytuł strony definiowany znacznikiem title jest wykorzystywany przez przeglądarki na karcie oraz przez wyszukiwarki jako główny nagłówek wyniku wyszukiwania, co ma bezpośrednie przełożenie na SEO.

Sekcja body zawiera całą widoczną treść strony i jest tym, co użytkownik faktycznie ogląda i z czym wchodzi w interakcję. Wszystkie elementy wizualne, takie jak nagłówki, akapity, obrazy, formularze i przyciski, muszą znajdować się wewnątrz body. Przeglądarka renderuje zawartość body zgodnie z regułami CSS, a JavaScript może dynamicznie modyfikować zarówno strukturę, jak i styl elementów w tej sekcji. Rozdzielenie metadanych od treści wizualnej jest fundamentem architektury każdego dokumentu HTML.

3Semantyczne Tagowanie w HTML5

Semantyczny HTML a dostępność i pozycjonowanie w wyszukiwarkach

Semantyczne tagowanie w HTML5 to stosowanie elementów, które jasno określają swoje przeznaczenie i znaczenie w kontekście dokumentu. Zamiast używać licznych, nieokreślonych znaczników <div> (które same w sobie nic nie znaczą), zaleca się stosowanie elementów semantycznych, takich jak:

  • <header> - nagłówek dokumentu lub sekcji, często zawierający logo, nawigację lub wprowadzenie
  • <nav> - sekcja nawigacji głównej lub podrzędnej
  • <main> - główna, unikalna zawartość strony (tylko jedna na stronę)
  • <article> - samodzielna, niezależna treść (np. wpis na blogu, artykuł)
  • <section> - tematyczne grupowanie treści w dokumencie
  • <aside> - treść poboczna, powiązana z główną (np. sidebar)
  • <footer> - stopka dokumentu lub sekcji

Stosowanie semantycznych znaczników znacząco poprawia dostępność strony dla osób niewidomych korzystających z czytników ekranu, pomaga wyszukiwarkom lepiej indeksować treść (SEO) oraz ułatwia innym programistom zrozumienie struktury kodu.

<!-- Zamiast: <div class="naglowek"> -->
<header>...</header>
<main>
    <section>
        <article>...</article>
    </section>
</main>

Semantyczne znaczniki HTML5 zostały wprowadzone, aby nadać strukturze dokumentu głębsze znaczenie, wykraczające poza czysto wizualne formatowanie. Używanie odpowiednich znaczników, takich jak header, nav, main, article, section, aside i footer, pozwala przeglądarkom i czytnikom ekranu lepiej zrozumieć układ i hierarchię treści. Dla osób niewidomych korzystających z technologii asystujących semantyczny HTML umożliwia efektywną nawigację po stronie przez przeskakiwanie między sekcjami, co drastycznie poprawia dostępność.

Z perspektywy SEO wyszukiwarki przypisują większą wagę treściom umieszczonym w semantycznych znacznikach, szczególnie w main i article, co może pozytywnie wpłynąć na pozycję strony w wynikach wyszukiwania. Ponadto semantyczny kod jest łatwiejszy w utrzymaniu dla zespołów developerskich, ponieważ intencja każdego elementu jest jasna z jego nazwy. W praktyce zaleca się unikanie nadużywania div i span na rzecz dedykowanych znaczników semantycznych, co czyni kod bardziej wyrazistym i profesjonalnym.

4Hiperłącza (<a>) i Osadzanie Obrazów (<img>)

Hiperłącza i osadzanie obrazów - fundamenty treści internetowej

Hiperłącza (linki) tworzone za pomocą znacznika <a> (od ang. anchor - kotwa) są jednym z najważniejszych elementów HTML. Pozwalają one użytkownikom nawigować między stronami, dokumentami i zasobami w Internecie. Kluczowym atrybutem jest tutaj href (Hypertext Reference), który określa adres URL docelowy. Atrybut target="_blank" powoduje otwarcie linku w nowej karcie przeglądarki.

Obrazy w HTML tworzymy za pomocą znacznika <img>. Atrybut src (source - źródło) wskazuje ścieżkę lub adres URL do pliku graficznego. Atrybut alt (alternative text) jest niezwykle istotny z kilku powodów: wyświetla się jako tekst zastępczy, gdy obraz nie może zostać załadowany, jest odczytywany przez czytniki ekranu dla osób niewidomych, a także pomaga wyszukiwarkom zrozumieć zawartość obrazu (SEO). Dodatkowo można określić wymiary za pomocą atrybutów width i height, co zapobiega "przeskakiwaniu" układu strony podczas ładowania obrazów.

<a href="/strona" target="_blank">Nowa Karta</a>
<img src="zdjecie.jpg" alt="Opis dla SEO i niewidomych" width="300">

Hiperłącza są kwintesencją internetu jako połączonej sieci dokumentów, a znacznik a z atrybutem href stanowi podstawowy mechanizm nawigacji między stronami. Atrybut target="_blank" jest powszechnie używany do otwierania linków w nowej karcie, co jest szczególnie przydatne dla linków zewnętrznych, aby użytkownik nie opuszczał całkowicie bieżącej witryny. Warto jednak pamiętać o dodaniu atrybutu rel="noopener noreferrer" dla linków z target="_blank", aby zapobiec potencjalnym lukom bezpieczeństwa związanym z dostępem do obiektu window.opener.

Obrazy wnoszą ogromną wartość wizualną i informacyjną do stron internetowych, ale ich nieprawidłowe użycie może znacząco pogorszyć wydajność i dostępność. Atrybut alt jest nie tylko wymogiem dostępności dla czytników ekranu, ale także kluczowym elementem SEO, ponieważ wyszukiwarki analizują tekst alternatywny podczas indeksowania obrazów. Atrybuty width i height pomagają przeglądarce zarezerwować miejsce dla obrazu przed jego załadowaniem, co zapobiega niepożądanym przesunięciom układu strony podczas ładowania i poprawia Cumulative Layout Shift (CLS).

5Organizacja Treści: Listy i Tabele

Strukturalne organizowanie treści na stronie

Listy i tabele to podstawowe elementy służące do uporządkowanego prezentowania informacji na stronie internetowej. Pozwalają one na logiczne pogrupowanie danych, co znacząco ułatwia ich odczytywanie i przyswajanie przez użytkowników.

Listy nieuporządkowane (<ul> - unordered list) służą do prezentacji elementów, których kolejność nie ma znaczenia. Każdy punkt listy tworzymy za pomocą znacznika <li> (list item). Przykłady zastosowania: lista składników, lista funkcji programu, wypunktowania.

Listy uporządkowane (<ol> - ordered list) stosujemy, gdy kolejność elementów jest istotna. Przykłady: instrukcje krok po kroku, rankingi, numerowane procedury. Domyślnie elementy są numerowane cyframi, ale można to zmienić w CSS.

Tabele (<table>) służą do prezentacji danych tabelarycznych. Prawidłowa struktura tabeli obejmuje: <thead> (nagłówek tabeli z <th>), <tbody> (ciało tabeli z danymi w <td>), <tr> (wiersz tabeli). Tabele powinny być używane wyłącznie do danych tabelarycznych, nie do tworzenia układu strony.

<ul>
    <li>Punkt 1</li>
    <li>Punkt 2</li>
</ul>
<table>
    <thead><tr><th>Klucz</th></tr></thead>
    <tbody><tr><td>Wartość</td></tr></tbody>
</table>

Listy HTML są podstawowym narzędziem do grupowania powiązanych elementów w czytelną strukturę, a ich prawidłowe stosowanie znacząco poprawia dostępność i organizację treści. Lista nieuporządkowana ul jest używana tam, gdzie kolejność elementów nie ma znaczenia, na przykład przy wypunktowaniu cech produktu lub elementów menu. Lista uporządkowana ol jest właściwym wyborem dla instrukcji krok po kroku, rankingów i procedur, gdzie sekwencja ma kluczowe znaczenie dla zrozumienia treści.

Tabele HTML powinny być używane wyłącznie do prezentacji danych tabelarycznych, nigdy do tworzenia układu strony, co było niestety powszechną praktyką we wczesnych latach internetu. Prawidłowa struktura tabeli obejmuje elementy thead, tbody i tfoot, które grupują odpowiednio nagłówki, dane właściwe i podsumowanie. Znacznik th w thead definiuje nagłówki kolumn, które są domyślnie wyświetlane pogrubione i wyśrodkowane, a także są odpowiednio interpretowane przez czytniki ekranu dla osób niewidomych.

6Formularze HTML (<form>): Elementy wejściowe

Formularze HTML - interaktywne zbieranie danych od użytkownika

Formularze HTML są podstawowym sposobem interakcji użytkownika ze stroną internetową. Pozwalają na zbieranie danych wejściowych, takich jak login i hasło, adres e-mail, wiadomości tekstowe, wybory opcji i wiele innych. Formularz tworzymy za pomocą znacznika <form>.

Kluczowe atrybuty formularza to: action określający adres URL, na który zostaną wysłane dane formularza po jego submitowaniu, oraz method definiujący metodę HTTP używaną do wysłania danych (GET umieszcza dane w URL i jest używane do pobierania danych, POST wysyła dane w ciele żądania i jest używane do bezpiecznego przesyłania danych).

Element <input> służy do tworzenia różnych typów pól wejściowych. Najważniejsze typy to: text (zwykłe pole tekstowe), email (waliduje format adresu e-mail), password (ukrywa wprowadzane znaki), number (akceptuje tylko liczby), radio (przyciski radiowe do wyboru jednej opcji), checkbox (pole wyboru do zaznaczania wielu opcji). Każde pole powinno mieć przypisaną etykietę <label> za pomocą atrybutu for, co poprawia dostępność i użyteczność formularza.

<form method="POST" action="/login">
    <label for="u">Użytkownik:</label>
    <input type="text" id="u" name="user" required>
    <button type="submit">Wyślij</button>
</form>

Formularze HTML są podstawowym mechanizmem zbierania danych od użytkownika i stanowią kluczowy element interaktywnych stron internetowych. Atrybut action formularza określa adres URL, na który zostaną wysłane dane po zatwierdzeniu, a atrybut method definiuje metodę HTTP: GET umieszcza dane w adresie URL jako parametry zapytania, co jest odpowiednie dla wyszukiwania i filtrów, natomiast POST przesyła dane w ciele żądania, co jest bezpieczniejsze dla danych wrażliwych i większych zestawów danych.

Różnorodność typów input HTML5 pozwala na tworzenie zaawansowanych interfejsów bez konieczności pisania dodatkowego JavaScriptu. Typ email zapewnia walidację formatu adresu e-mail, type number wyświetla odpowiednią klawiaturę na urządzeniach mobilnych i ogranicza wprowadzane znaki do cyfr, a type password maskuje wprowadzane znaki dla bezpieczeństwa. Element label z atrybutem for powiązanym z id inputa jest kluczowy dla dostępności, ponieważ czytniki ekranu odczytują etykietę, gdy użytkownik skupia się na polu, a także zwiększa obszar klikalny, co ułatwia obsługę formularza.

7Wbudowana Walidacja Formularzy (required, pattern)

Wbudowana walidacja formularzy w HTML5

HTML5 wprowadził zaawansowane mechanizmy walidacji formularzy po stronie klienta, które pozwalają na sprawdzanie poprawności danych jeszcze przed ich wysłaniem do serwera. Dzięki temu użytkownik otrzymuje natychmiastową informację zwrotną o błędach, bez konieczności oczekiwania na odpowiedź serwera.

Najważniejsze atrybuty walidacji to: required oznaczający pole jako obowiązkowe do wypełnienia, minlength i maxlength określające minimalną i maksymalną długość tekstu, min i max dla pól numerycznych, type="email" walidujące format adresu e-mail, type="url" walidujące format adresu URL, oraz type="tel" dostosowujący klawiaturę mobilną do wprowadzania numerów telefonu.

Atrybut pattern pozwala na zdefiniowanie własnego wzorca walidacji za pomocą wyrażeń regularnych (regex). Na przykład pattern="[A-Za-z]{3,}" wymaga co najmniej 3 liter. Atrybut title wyświetla użytkownikowi wskazówkę, gdy walidacja się nie powiedzie. Walidacja HTML5 jest wygodna, ale zawsze powinna być uzupełniona walidacją po stronie serwera dla bezpieczeństwa.

<input type="text" 
    pattern="[A-Za-z]{3,}" 
    title="Wymagane co najmniej 3 litery"
    required>
    
<input type="number" min="18" max="99">

Walidacja po stronie klienta za pomocą atrybutów HTML5 jest pierwszą linią obrony przed nieprawidłowymi danymi i znacząco poprawia doświadczenie użytkownika poprzez natychmiastową informację zwrotną. Atrybut required uniemożliwia wysłanie formularza, dopóki obowiązkowe pole nie zostanie wypełnione, a przeglądarka wyświetla stosowny komunikat błędu. Atrybut pattern umożliwia zdefiniowanie własnego wzorca walidacji za pomocą wyrażeń regularnych, co daje ogromną elastyczność przy sprawdzaniu formatów takich jak kody pocztowe, numery telefonów czy identyfikatory.

Warto pamiętać, że walidacja po stronie klienta jest wygodna, ale nigdy nie powinna być jedyną warstwą zabezpieczeń, ponieważ może być łatwo ominięta przez wyłączenie JavaScriptu lub ręczne wysłanie żądania HTTP. Dlatego każda aplikacja webowa musi implementować walidację także po stronie serwera, gdzie dane są weryfikowane przed przetworzeniem i zapisem do bazy danych. Atrybut title dla elementów z patternem dostarcza użytkownikowi wskazówki dotyczącej oczekiwanego formatu, co jest wyświetlane w dymku podczas walidacji i poprawia użyteczność formularza.

8Osadzanie Kodu JavaScript (<script>)

Metody osadzania kodu JavaScript w dokumencie HTML

JavaScript może być osadzony w dokumencie HTML na dwa główne sposoby: jako zewnętrzny plik (zewnętrzny skrypt) lub jako kod wbudowany bezpośrednio w dokument (inline script). Każde z tych podejść ma swoje zastosowanie i zalety.

Zewnętrzne skrypty są preferowane w większości przypadków, ponieważ pozwalają na oddzielenie kodu JavaScript od HTML, ułatwiając utrzymanie i cache'owanie przez przeglądarkę. Plik JS dołączamy za pomocą <script src="sciezka/do/pliku.js"></script>.

Wydajność ładowania jest kluczowa dla doświadczenia użytkownika. Domyślnie przeglądarka blokuje parsowanie HTML podczas ładowania skryptu. Dlatego zaleca się umieszczanie skryptów na końcu sekcji <body>, aby najpierw załadować i wyświetlić treść strony.

Atrybut defer informuje przeglądarkę, aby pobrać skrypt równolegle z parsowaniem HTML, ale wykonać go dopiero po całkowitym sparsowaniu dokumentu, zachowując kolejność wykonania. Atrybut async pobiera i wykonuje skrypt asynchronicznie, najszybciej jak to możliwe, bez zachowania kolejności - idealny dla niezależnych skryptów analitycznych.

<!-- Zewnętrzny plik JS, nie blokuje renderowania -->
<script src="app.js" defer></script> 
<!-- Wewnętrzny kod JS -->
<script> console.log('Witaj'); </script>

Osadzanie JavaScriptu w dokumencie HTML może odbywać się na kilka sposobów, z których każdy ma wpływ na wydajność i zachowanie strony. Zewnętrzne pliki JS, dołączane przez znacznik script z atrybutem src, są preferowane ze względu na możliwość cache'owania przez przeglądarkę i separację logiki od struktury. Kod wbudowany bezpośrednio w HTML za pomocą tagu script bez atrybutu src jest wygodny dla małych skryptów, ale utrudnia utrzymanie i nie może być cache'owany, co zwiększa objętość przesyłanych danych.

Wydajność ładowania skryptów jest krytyczna dla doświadczenia użytkownika, ponieważ domyślnie przeglądarka blokuje parsowanie HTML podczas pobierania i wykonywania skryptu. Atrybut defer rozwiązuje ten problem, pobierając skrypt równolegle z parsowaniem HTML i wykonując go dopiero po całkowitym sparsowaniu dokumentu, co jest idealne dla skryptów, które muszą manipulować DOM-em. Atrybut async również pobiera skrypt asynchronicznie, ale wykonuje go natychmiast po pobraniu, niezależnie od stanu parsowania, co jest odpowiednie dla niezależnych skryptów analitycznych i reklamowych.

9Zmienne (let) i Stałe (const) - Zasięg Blokowy

Deklarowanie zmiennych i stałych w JavaScript

JavaScript oferuje trzy sposoby deklarowania zmiennych: var, let i const. Wybór odpowiedniego sposobu ma kluczowe znaczenie dla poprawnego działania i czytelności kodu.

const służy do deklarowania stałych - wartości, które nie będą zmieniane w trakcie wykonywania programu. Jest to preferowany wybór, gdy tylko jest to możliwe, ponieważ sygnalizuje innym programistom i kompilatorowi, że wartość ta jest niezmienna. Jeśli zadeklarujesz obiekt z const, nie będziesz mógł przypisać do niego nowego obiektu, ale wciąż możesz modyfikować jego właściwości.

let służy do deklarowania zmiennych, których wartość będzie się zmieniać. let ma zasięg blokowy (block scope) - zmienna jest dostępna tylko w bloku, w którym została zadeklarowana (np. wewnątrz instrukcji if, pętli czy funkcji).

var jest starszym sposobem deklarowania zmiennych, który jest obecnie uznawany za przestarzały. var ma zasięg funkcyjny (nie blokowy), co może prowadzić do nieoczekiwanego zachowania. Zaleca się unikanie var i używanie zamiast niego let lub const.

const PI = 3.14; // Wartość niezmienna
let licznik = 0; // Wartość zmienna
licznik++;

if (true) {
    let x = 10;
    // console.log(x); // Dostępne
}
// console.log(x); // Błąd, poza zasięgiem blokowym

Deklaracja zmiennych w JavaScript przeszła długą ewolucję od var przez let aż do const, a zrozumienie różnic między nimi jest kluczowe dla pisania bezpiecznego i przewidywalnego kodu. Var, pochodzący z ES5, ma zasięg funkcyjny, co oznacza, że zmienna zadeklarowana z var wewnątrz bloku if lub pętli jest dostępna poza tym blokiem, co często prowadzi do subtelnych błędów. Let i const, wprowadzone w ES6, mają zasięg blokowy, co ogranicza widoczność zmiennej do najbliższego bloku kodu wyznaczonego przez nawiasy klamrowe, eliminując całą kategorię potencjalnych problemów.

Const jest preferowanym wyborem dla wartości, które nie powinny być reassignowane, co stanowi formę dokumentacji intencji programisty i zapobiega przypadkowym modyfikacjom. W przypadku obiektów i tablic const nie uniemożliwia modyfikacji ich zawartości - chroni jedynie przed ponownym przypisaniem zmiennej. W nowoczesnym JavaScript zaleca się domyślne używanie const, uciekanie się do let tylko gdy reassignacja jest konieczna, i całkowite unikanie var, co jest standardem w profesjonalnych projektach i frameworkach.

10Typy Danych Prymitywnych (String, Number, Boolean)

Prymitywne typy danych w JavaScript

JavaScript jest językiem z dynamicznym typowaniem, co oznacza, że zmienne mogą przechowywać wartości różnych typów bez jawnej deklaracji typu. Niemniej jednak, każda wartość w JavaScript ma określony typ. JavaScript posiada 8 typów danych, z czego 7 to typy prymitywne.

String reprezentuje ciąg znaków tekstowych. Może być zdefiniowany za pomocą pojedynczych apostrofów ('tekst'), podwójnych cudzysłowów ("tekst") lub backticków (`tekst`). Teksty są używane do przechowywania nazw, wiadomości, adresów URL i wielu innych danych tekstowych.

Number reprezentuje zarówno liczby całkowite, jak i zmiennoprzecinkowe (np. 42, 3.14, -7). JavaScript używa 64-bitowego formatu zmiennoprzecinkowego podwójnej precyzji. Specjalne wartości to Infinity, -Infinity i NaN (Not a Number - wynik nieprawidłowej operacji matematycznej).

Boolean reprezentuje wartość logiczną: true (prawda) lub false (fałsz). Używany głównie w instrukcjach warunkowych do sterowania przepływem programu.

Null to specjalna wartość oznaczająca celowy brak wartości obiektowej. Undefined oznacza zmienną, która została zadeklarowana, ale nie została jeszcze przypisana. Symbol to unikalny, niezmienny identyfikator używany głównie jako klucze właściwości obiektów. BigInt pozwala na reprezentację liczb całkowitych większych niż 2^53-1.

Funkcja typeof zwraca typ danej wartości jako string (np. "string", "number", "boolean").

let nazwa = "Ala";        // string
let wiek = 30;            // number
let jestAktywny = true;   // boolean
let bezWartosci;          // undefined

console.log(typeof nazwa); // "string"

Typy danych w JavaScript dzielą się na prymitywne i złożone, przy czym typy prymitywne są niemutowalne i przechowywane bezpośrednio w zmiennej, podczas gdy obiekty są przechowywane przez referencję. String, Number, Boolean, Null, Undefined, Symbol i BigInt to siedem typów prymitywnych, z których każdy ma swoje specyficzne zastosowanie i zachowanie. Operator typeof jest podstawowym narzędziem do sprawdzania typu wartości w czasie wykonania, choć ma pewne niuanse, takie jak typeof null zwracające "object", co jest historycznym błędem języka.

Number w JavaScript jest implementowany jako 64-bitowa liczba zmiennoprzecinkowa zgodnie ze standardem IEEE 754, co oznacza, że nie ma oddzielnego typu dla liczb całkowitych i zmiennoprzecinkowych. Specjalne wartości Infinity i NaN pojawiają się odpowiednio przy przekroczeniu zakresu liczbowego i przy nieprawidłowych operacjach matematycznych. Symbol wprowadzony w ES6 jest używany do tworzenia unikalnych identyfikatorów, szczególnie przydatnych jako klucze właściwości obiektów, gdzie gwarantuje brak kolizji nazw między różnymi bibliotekami.

11Operatory Porównania (===) i Logiczne

Operatory porównania i operatory logiczne

Operatory porównania służą do porównywania wartości i zwracają wartość boolean (true lub false). JavaScript oferuje dwa typy porównania: ścisłe i luźne.

Operator ścisły (===) porównuje zarówno wartość, jak i typ danych. Jeśli typy są różne, wynik zawsze będzie false. Na przykład: 10 === '10' zwraca false, ponieważ porównujemy liczbę ze stringiem. Operator ścisły jest zdecydowanie zalecany w większości przypadków, ponieważ zapewnia przewidywalne i bezpieczne porównania.

Operator luźny (==) porównuje tylko wartości, automatycznie konwertując typy przed porównaniem. Na przykład: 10 == '10' zwraca true, ponieważ string '10' jest konwertowany na liczbę 10. Ta automatyczna konwersja może prowadzić do nieoczekiwanych wyników i błędów, dlatego zaleca się unikanie operatora luźnego porównania.

Operatory logiczne służą do łączenia lub negowania wartości boolean: && (AND - iloczyn logiczny) zwraca true tylko gdy oba warunki są spełnione, || (OR - suma logiczna) zwraca true gdy przynajmniej jeden warunek jest spełniony, ! (NOT - negacja) odwraca wartość logiczną.

10 == '10';  // true (luzne)
10 === '10'; // false (ścisłe, różne typy)

let a = 5;
if (a > 0 && a < 10) {
    console.log('Między 0 a 10');
}

Operatory porównania w JavaScript są źródłem wielu potencjalnych błędów, szczególnie dla początkujących programistów, ze względu na obecność zarówno ścisłych, jak i luźnych operatorów. Operator ścisłej równości === porównuje zarówno wartość, jak i typ danych, co czyni go przewidywalnym i bezpiecznym wyborem w praktycznie każdej sytuacji. Operator luźnej równości == dokonuje automatycznej konwersji typów przed porównaniem, co może prowadzić do zaskakujących rezultatów, takich jak 0 == false zwracające true, podczas gdy 0 === false zwraca false.

Operatory logiczne &&, || i ! służą do łączenia i negacji warunków, ale w JavaScript mają dodatkową właściwość: zwracają niekoniecznie wartość boolean, a wartość jednego z operandów. Operator && zwraca pierwszy operand, jeśli jest falsy, w przeciwnym razie zwraca drugi operand, co jest wykorzystywane w technice warunkowego wykonania kodu. Operator || zwraca pierwszy prawdziwy operand lub ostatni falsy, co jest powszechnie używane do ustawiania wartości domyślnych, choć od ES2020 lepszym wyborem jest operator nullish coalescing ??, który uwzględnia tylko null i undefined jako wartości domyślne.

12Instrukcje Warunkowe (if/else i switch)

Sterowanie przepływem programu za pomocą warunków

Instrukcje warunkowe pozwalają na wykonywanie różnych bloków kodu w zależności od spełnienia określonych warunków. Są one fundamentem każdego programowania i pozwalają na tworzenie logiki decyzyjnej.

Instrukcja if/else służy do wykonywania kodu na podstawie warunku logicznego. Składnia jest prosta: jeśli warunek jest prawdziwy, wykonaj pierwszy blok kodu, w przeciwnym razie wykonaj kod po else (jeśli istnieje). Można łączyć wiele warunków za pomocą else if. Jest to preferowana instrukcja dla warunków binarnych lub gdy mamy do czynienia z nieliniowymi, złożonymi warunkami.

Instrukcja switch jest alternatywą dla wielokrotnych instrukcji else if, gdy sprawdzamy wartość jednej zmiennej względem wielu możliwych wartości. Switch jest bardziej czytelny i czasami bardziej wydajny. Kluczowe jest użycie instrukcji break na końcu każdego przypadku case - bez niej wykonanie "przedostanie się" do następnego przypadku, co jest często niepożądanym zachowaniem (tzw. fall-through). Przypadek default wykonuje się, gdy żaden z case'ów nie pasuje.

const status = 'nowy';
switch (status) {
    case 'nowy':
        console.log('Do zrobienia');
        break;
    default:
        console.log('Zrobione');
}

Instrukcje warunkowe if, else if i else stanowią podstawowy mechanizm podejmowania decyzji w programie, pozwalając na wykonywanie różnych ścieżek kodu w zależności od spełnienia warunków. Klauzula else if pozwala na sprawdzenie wielu warunków w sposób sekwencyjny, gdzie tylko pierwszy prawdziwy warunek powoduje wykonanie odpowiadającego mu bloku kodu. W przypadku wielu wzajemnie wykluczających się warunków else if jest czytelniejsze niż zagnieżdżone if, które szybko stają się nieczytelne i trudne w utrzymaniu.

Instrukcja switch stanowi alternatywę dla wielokrotnych else if, gdy porównujemy wartość jednej zmiennej z zestawem możliwych wartości stałych. Każdy przypadek case powinien kończyć się instrukcją break, aby zapobiec niezamierzonemu przejściu do następnego przypadku, co jest znane jako fall-through i w większości sytuacji jest niepożądane. Współczesny JavaScript oferuje jeszcze bardziej eleganckie rozwiązanie w postaci obiektów mapujących wartości do funkcji, co bywa czytelniejsze od rozbudowanych konstrukcji switch i jest częstym wzorcem w profesjonalnym kodzie.

13Funkcje: Deklaracja, Wyrażenie i Strzałkowe

Funkcje - wielokrotnie użyteczne bloki kodu

Funkcje to fundamentalne konstrukcje w JavaScript, które pozwalają na organizowanie kodu w wielokrotnie użyteczne bloki. Stosowanie funkcji wpisuje się w zasadę DRY (Don't Repeat Yourself) - raz napisany kod może być wywoływany wielokrotnie w różnych miejscach programu, co ułatwia utrzymanie, testowanie i rozwijanie oprogramowania.

Deklaracja funkcji (Function Declaration) to klasyczny sposób definiowania funkcji: function nazwa(params) { ... }. Taka funkcja jest hoistowana (przenoszona na początek zakresu), co oznacza, że można ją wywołać przed miejscem, w którym została zdefiniowana.

Wyrażenie funkcyjne (Function Expression) przypisuje funkcję do zmiennej: const nazwa = function(params) { ... }. Funkcja ta nie jest hoistowana.

Funkcje strzałkowe (Arrow Functions) to skrótowa składnia wprowadzona w ES6: const nazwa = (params) => { ... } lub dla jednoliniowych: const nazwa = (a, b) => a + b. Mają one krótszą składnię i różnią się zachowaniem this - nie tworzą własnego kontekstu this, dziedzicząc go z otaczającego zakresu.

Instrukcja return służy do zwracania wartości z funkcji. Jeśli funkcja nie ma instrukcji return lub return jest bez wartości, zwraca undefined.

// 1. Deklaracja
function suma(a, b) { return a + b; }

// 2. Strzałkowa (Arrow)
const roznica = (a, b) => a - b;

console.log(suma(1, 2));
console.log(roznica(5, 3));

Funkcje w JavaScript są obiektami pierwszej klasy, co oznacza, że mogą być przypisywane do zmiennych, przekazywane jako argumenty do innych funkcji i zwracane z funkcji, co stanowi fundament programowania funkcyjnego. Deklaracja funkcji za pomocą słowa kluczowego function podlega hoistingowi, czyli funkcja jest przenoszona na początek zakresu przed wykonaniem kodu, co pozwala na wywoływanie jej przed miejscem definicji. Wyrażenie funkcyjne przypisane do zmiennej nie jest hoistowane, ponieważ tylko deklaracja zmiennej jest przenoszona, a nie przypisanie, co ma istotne znaczenie przy organizacji kodu.

Funkcje strzałkowe wprowadzone w ES6 oferują zwięzłą składnię i różnią się od tradycyjnych funkcji przede wszystkim brakiem własnego kontekstu this, który dziedziczą z otaczającego zakresu. Ta cecha czyni je idealnymi do używania jako callbacki w metodach tablicowych, setTimeout i event listenerach, gdzie zachowanie this bywa źródłem frustracji przy użyciu tradycyjnych funkcji. Funkcje strzałkowe z pojedynczym parametrem mogą pominąć nawiasy wokół parametru, a z jednoliniowym ciałem mogą pominąć słowo kluczowe return i nawiasy klamrowe, co prowadzi do bardzo zwięzłego kodu.

14Pętle: for, while, for...of

Pętle - powtarzanie operacji w JavaScript

Pętle pozwalają na wielokrotne wykonywanie tego samego bloku kodu. Są niezbędne do przetwarzania kolekcji danych, automatyzacji powtarzalnych zadań i implementacji logiki iteracyjnej.

Klasyczna pętla for (for (inicjalizacja; warunek; inkrement) { ... }) jest najbardziej uniwersalną pętlą. Składa się z trzech części: inicjalizacji zmiennej licznikowej (wykonuje się raz na początku), warunku kontynuacji (sprawdzany przed każdą iteracją) i inkrementacji (wykonywana po każdej iteracji). Jest idealna, gdy znasz dokładną liczbę powtórzeń lub chcesz mieć pełną kontrolę nad przebiegiem pętli.

Pętla while wykonuje blok kodu tak długo, jak warunek jest prawdziwy. Używamy jej, gdy nie znamy z góry liczby iteracji i chcemy kontynuować dopóki spełniony jest określony warunek (np. dopóki użytkownik nie wprowadzi poprawnych danych). Należy zachować ostrożność, aby nie stworzyć pętli nieskończonej.

Pętla for...of to nowoczesna składnia wprowadzona w ES6, przeznaczona do iteracji po wartościach iterowalnych obiektów (kolekcji), takich jak tablice, stringi, mapy czy zestawy. Jest bardziej czytelna niż klasyczna pętla for, gdy interesują nas wartości, a nie indeksy. Dla tablic można również użyć metod .forEach(), .map() czy .filter().

const imiona = ['Ola', 'Piotr', 'Kasia'];

// Klasyczna iteracja
for (let i = 0; i < imiona.length; i++) {
    console.log(imiona[i]);
}

// Nowoczesna iteracja
for (const imie of imiona) {
    console.log(imie);
}

Pętle w JavaScript umożliwiają wielokrotne wykonywanie tego samego bloku kodu i są niezbędne przy przetwarzaniu kolekcji danych oraz automatyzacji powtarzalnych operacji. Klasyczna pętla for z inicjalizacją, warunkiem i inkrementacją daje pełną kontrolę nad przebiegiem iteracji i jest odpowiednia, gdy znamy liczbę powtórzeń lub potrzebujemy dostępu do indeksu. Pętla while wykonuje kod tak długo, jak warunek pozostaje prawdziwy, co jest przydatne w sytuacjach, gdy liczba iteracji nie jest znana z góry, na przykład przy przetwarzaniu strumienia danych.

Pętla for...of wprowadzona w ES6 umożliwia iterację po wartościach obiektów iterowalnych, takich jak tablice, stringi, mapy i zbiory, oferując czytelniejszą składnię niż klasyczna pętla for. W przeciwieństwie do forEach, pętla for...of obsługuje instrukcje break i continue, co daje większą kontrolę nad przepływem iteracji. Dla obiektów, które nie są domyślnie iterowalne, można użyć metod Object.keys, Object.values lub Object.entries do iteracji po kluczach, wartościach lub parach, co jest standardowym wzorcem w nowoczesnym JavaScript.

15Tablice (Arrays): Podstawowe Metody

Tablice - uporządkowane kolekcje danych

Tablica (Array) to obiekt w JavaScript służący do przechowywania uporządkowanych kolekcji wartości. Może zawierać elementy dowolnych typów - liczby, stringi, obiekty, inne tablice, a nawet funkcje. Tablice są fundamentalnym typem danych używanym praktycznie w każdym programie JavaScript.

Elementy w tablicy są indeksowane od zera - pierwszy element ma index 0, drugi 1, i tak dalej. Dostęp do elementu uzyskujemy za pomocą nawiasów kwadratowych: tablica[0]. Właściwość length zwraca liczbę elementów w tablicy.

Metody modyfikujące tablicę:

  • .push(element) - dodaje element lub elementy na końcu tablicy, zwraca nową długość
  • .pop() - usuwa i zwraca ostatni element tablicy
  • .unshift(element) - dodaje element lub elementy na początku tablicy
  • .shift() - usuwa i zwraca pierwszy element tablicy
  • .splice(index, ileUsunac, elementyDoDodania) - wszechstronna metoda do wstawiania, usuwania i zastępowania elementów
const lista = ['A', 'B'];
lista.push('C'); // ['A', 'B', 'C']
lista.pop();     // ['A', 'B']

console.log(lista[0]); // A (indeksacja od 0)

Tablice JavaScript są dynamicznymi kolekcjami, które mogą przechowywać elementy dowolnych typów i automatycznie dostosowują swój rozmiar do liczby przechowywanych elementów. Indeksowanie od zera jest standardem w większości języków programowania i oznacza, że pierwszy element tablicy ma indeks 0, drugi 1, a ostatni length - 1. Właściwość length jest automatycznie aktualizowana przy dodawaniu i usuwaniu elementów, a jej ustawienie na mniejszą wartość niż aktualna liczba elementów powoduje obcięcie tablicy, co jest przydatne przy implementacji kolekcji o stałym rozmiarze.

Metody modyfikujące tablice, takie jak push, pop, shift i unshift, działają odpowiednio na końcu i początku tablicy, oferując złożoność obliczeniową O(1) dla operacji na końcu i O(n) dla operacji na początku ze względu na konieczność przesunięcia indeksów. Metoda splice jest niezwykle wszechstronna, umożliwiając jednoczesne usuwanie i wstawianie elementów w dowolnym miejscu tablicy, co czyni ją potężnym narzędziem do manipulacji kolekcjami. W nowoczesnym JavaScript preferuje się jednak używanie niemutowalnych metod takich jak toSpliced, toReversed i with, które zwracają nową tablicę zamiast modyfikować oryginał.

16Obiekty (Objects): Właściwości i Metody

Obiekty - struktury danych klucz-wartość

Obiekty są jednym z fundamentalnych typów danych w JavaScript i stanowią podstawę całego języka (JavaScript jest językiem zorientowanym obiektowo). Obiekt to zbiór właściwości (properties), gdzie każda właściwość składa się z nazwy (klucza) i wartości. Wartości mogą być prymitywami lub innymi obiektami, a także funkcjami.

Obiekty służą do grupowania powiązanych danych i funkcjonalności w jedną, spójną strukturę. Na przykład, obiekt reprezentujący użytkownika może zawierać jego imię, wiek, adres e-mail oraz metody do wysyłania wiadomości czy aktualizacji profilu.

Dostęp do właściwości obiektu:

  • Notacja kropkowa: obiekt.wlasciwosc - preferowana, bardziej czytelna
  • Notacja nawiasowa: obiekt['wlasciwosc'] - przydatna, gdy nazwa właściwości jest dynamiczna, zawiera spacje lub znaki specjalne

Właściwości można dodawać, modyfikować i usuwać w dowolnym momencie. Funkcje wewnątrz obiektów nazywamy metodami.

const samochod = {
    marka: 'Ford',
    rok: 2020,
    uruchom: function() { 
        console.log('Wrrrum!'); 
    }
};

console.log(samochod.marka);
samochod['rok'] = 2021;
samochod.uruchom();

Obiekty w JavaScript są kolekcjami par klucz-wartość, gdzie klucze są stringami lub symbolami, a wartości mogą być dowolnego typu, w tym inne obiekty i funkcje. Notacja literałowa obiektu z nawiasami klamrowymi jest najczęściej używanym sposobem tworzenia obiektów, oferując czytelną składnię i możliwość definiowania właściwości oraz metod w jednym miejscu. Dostęp do właściwości za pomocą notacji kropkowej jest preferowany ze względu na czytelność, ale notacja nawiasowa kwadratowego jest niezbędna, gdy nazwa właściwości jest dynamiczna, zawiera spacje lub znaki specjalne.

Metody obiektów to funkcje zdefiniowane jako właściwości, które operują na danych obiektu za pomocą słowa kluczowego this, odnoszącego się do obiektu, na którym metoda została wywołana. W nowoczesnym JavaScript można używać skróconej składni metod, pomijając dwukropek i słowo function, co czyni kod bardziej zwięzłym. Obiekty mogą być rozszerzane o nowe właściwości w dowolnym momencie, a właściwości można usuwać za pomocą operatora delete, co daje dużą elastyczność, ale wymaga ostrożności przy zarządzaniu strukturą danych.

17Funkcyjne Metody Tablic (forEach, map, filter)

Funkcyjne metody tablic do przetwarzania kolekcji

JavaScript oferuje potężne metody tablicowe, które pozwalają na deklaratywne (opisowe) przetwarzanie danych bez imperatywnego (krok po kroku) pisania pętli. Metody te przyjmują jako argument funkcję zwrotną (callback), która jest wykonywana dla każdego elementu tablicy.

.forEach(callback) - wykonuje podaną funkcję dla każdego elementu tablicy. Nie zwraca nowej tablicy, nie modyfikuje oryginalnej. Używamy go, gdy chcemy wykonać pewną akcję dla każdego elementu (np. wypisać do konsoli, dodać do DOM).

.map(callback) - tworzy nową tablicę, w której każdy element jest wynikiem wywołania funkcji callback na odpowiednim elemencie oryginalnej tablicy. Oryginalna tablica pozostaje niezmieniona. Idealny do transformacji danych.

.filter(callback) - tworzy nową tablicę zawierającą tylko te elementy, dla których funkcja callback zwróciła true. Oryginalna tablica pozostaje niezmieniona. Używamy go do wybierania podzbioru elementów spełniających warunek.

Te metody są preferowane nad tradycyjnymi pętlami for, ponieważ są bardziej zwięzłe, czytelne i mniej podatne na błędy (np. nie trzeba ręcznie zarządzać indeksami).

const liczby = [1, 2, 3];

// Zwraca nową tablicę: [2, 4, 6]
const podwojone = liczby.map(x => x * 2);

// Zwraca tablicę tylko parzystych: [2]
const parzyste = liczby.filter(x => x % 2 === 0);

liczby.forEach(x => console.log(x)); // Wykonuje akcję

Funkcyjne metody tablic, takie jak forEach, map i filter, stanowią fundament deklaratywnego stylu programowania w JavaScript, który skupia się na tym, co ma być osiągnięte, a nie na tym, jak to zrobić krok po kroku. Metoda forEach wykonuje podaną funkcję dla każdego elementu tablicy, ale nie zwraca nowej tablicy, co czyni ją odpowiednią do operacji ze skutkami ubocznymi, takich jak zapis do konsoli czy modyfikacja zewnętrznych zmiennych. Metoda map tworzy nową tablicę przez przekształcenie każdego elementu oryginalnej tablicy za pomocą funkcji transformującej, co jest podstawową operacją w przetwarzaniu danych.

Metoda filter zwraca nową tablicę zawierającą tylko elementy spełniające warunek określony w funkcji predykatu, co pozwala na eleganckie odfiltrowanie niepotrzebnych danych. Metody te są preferowane nad tradycyjnymi pętlami for, ponieważ są bardziej ekspresyjne, mniej podatne na błędy związane z zarządzaniem indeksami i często bardziej wydajne dzięki optymalizacjom silnika JavaScript. Dodatkowo łańcuchowanie metod, na przykład array.filter(...).map(...), pozwala na budowanie czytelnych potoków przetwarzania danych, gdzie każdy krok transformacji jest jasno określony i łatwy do modyfikacji.

18Destrukturyzacja Obiektów i Tablic (ES6)

Destrukturyzacja - rozpakowywanie wartości do zmiennych

Destrukturyzacja (Destructuring) to składnia wprowadzona w ES6, która umożliwia wyciąganie wartości z tablic lub właściwości z obiektów i przypisywanie ich do zmiennych w jednej, zwięzłej operacji. Znacząco poprawia czytelność kodu i zmniejsza ilość potrzebnego kodu.

Destrukturyzacja obiektów: Pozwala na wyciągnięcie wybranych właściwości obiektu do zmiennych. Można użyć oryginalnych nazw właściwości lub przypisać do zmiennych o innych nazwach za pomocą dwukropka: const { name: imie, age: wiek } = person. Można również ustawiać wartości domyślne: const { name = 'Anonim' } = person.

Destrukturyzacja tablic: Pozwala na wyciągnięcie elementów tablicy do zmiennych na podstawie ich pozycji. Można pominąć elementy używając przecinków: const [pierwszy, , trzeci] = tablica. Resztę elementów można zebrać za pomocą operatora rest: const [pierwszy, ...reszta] = tablica.

Destrukturyzacja jest szczególnie użyteczna przy pracy z parametrami funkcji, wartościami zwracanymi przez funkcje, przy importowaniu modułów oraz w codziennej pracy z obiektami i tablicami.

const user = { imie: 'Anna', wiek: 25 };
const [a, b, c] = [10, 20, 30];

// Destrukturyzacja obiektu
const { imie, wiek } = user;
console.log(imie); // Anna

// Destrukturyzacja tablicy
console.log(b); // 20

Destrukturyzacja to jedna z najbardziej praktycznych cech ES6, która znacząco redukuje ilość boilerplate kodu przy wyciąganiu danych z obiektów i tablic. Destrukturyzacja obiektów pozwala na wyciągnięcie wybranych właściwości do osobnych zmiennych o tej samej nazwie, co eliminuje potrzebę wielokrotnego odwoływania się do obiektu z notacją kropkową. Możliwość zmiany nazwy zmiennej za pomocą składni dwukropka oraz ustawiania wartości domyślnych czyni destrukturyzację niezwykle elastyczną i odporną na brakujące właściwości.

Destrukturyzacja tablic pozwala na przypisanie elementów do zmiennych na podstawie ich pozycji, co jest szczególnie przydatne przy pracy z funkcjami zwracającymi wiele wartości jako tablicę lub przy parsowaniu danych o stałym formacie. Operator rest (...) w destrukturyzacji zbiera pozostałe elementy w nową tablicę lub obiekt, co jest przydatne przy wyodrębnianiu tylko pierwszych kilku elementów i ignorowaniu reszty. W praktyce destrukturyzacja jest szeroko stosowana przy importowaniu modułów, obsłudze parametrów funkcji, przetwarzaniu odpowiedzi API i wszędzie tam, gdzie potrzebujemy selektywnie wyciągnąć dane ze złożonych struktur.

19Operator Spread (...) i Rest Parameters

Operator Spread i Parametry Rest

Operator spread (...) i parametry rest wyglądają identycznie, ale pełnią przeciwne funkcje. Oba używają trzech kropek, ale w różnych kontekstach.

Operator Spread służy do rozpakowania (spread) elementów iterowalnych (tablic, stringów) lub właściwości obiektów na pojedyncze elementy. Jest używany głównie w trzech scenariuszach:

  • Kopiowanie tablic/obiektów: const kopia = [...oryginal] - tworzy płytką kopię (zagnieżdżone obiekty/tablice są nadal referencjami)
  • Łączenie tablic/obiektów: const polaczone = [...tab1, ...tab2] lub const obiekt = { ...obj1, ...obj2 }
  • Przekazywanie argumentów: func(...tablica) przekaże elementy tablicy jako osobne argumenty

Parametry Rest (params) zbierają ("zbierają" = rest) pozostałe argumenty funkcji do tablicy. Składnia function func(a, b, ...reszta) sprawia, że wszystkie argumenty po a i b trafiają do tablicy reszta. Jest to przydatne przy tworzeniu funkcji z nieznaną liczbą argumentów.

const tab1 = [1, 2];
const tab2 = [...tab1, 3, 4]; // [1, 2, 3, 4]

const obj1 = { a: 1 };
const obj2 = { ...obj1, b: 2 }; // { a: 1, b: 2 }

// Rest (zbieranie argumentów w tablicę)
function logArgs(...args) { 
    console.log(args); 
}

Operator spread i parametry rest, oba używające składni trzech kropek, pełnią przeciwstawne role, co może być mylące dla początkujących programistów. Spread rozpakowuje elementy iterowalnej struktury na pojedyncze elementy, co jest używane przy kopiowaniu tablic i obiektów, łączeniu kolekcji oraz przekazywaniu argumentów do funkcji. Kopiowanie za pomocą spread tworzy płytką kopię, co oznacza, że zagnieżdżone obiekty i tablice są nadal współdzielone między oryginałem a kopią, co jest ważne przy pracy ze złożonymi strukturami danych.

Parametry rest zbierają pozostałe argumenty funkcji w tablicę, umożliwiając tworzenie funkcji przyjmujących zmienną liczbę argumentów bez konieczności używania obiektu arguments. W przeciwieństwie do spread, rest jest używany wyłącznie w kontekście deklaracji funkcji i destrukturyzacji. W nowoczesnym JavaScript te dwa mechanizmy są powszechnie stosowane w praktyce, szczególnie przy pracy z funkcjami wyższego rzędu, komponowaniu funkcji i implementacji wzorców takich jak partial application czy currying.

20Template Literals (Backticks) i Interpolacja

Template Literals - nowoczesne ciągi znaków

Template Literals (szablony literałów) to nowa składnia dla ciągów znaków wprowadzona w ES6, która znacząco ułatwia pracę z tekstem. Zamiast zwykłych cudzysłowów lub apostrofów, używamy backticków (odwróconych apostrofów) `tekst`.

Interpolacja zmiennych i wyrażeń: Za pomocą składni ${zmienna} lub ${wyrazenie} możemy wstawiać wartości zmiennych i wyniki wyrażeń bezpośrednio w środek tekstu. To znacznie czytelniejsze niż konkatenacja (sklejanie) stringów za pomocą operatora +.

Wieloliniowe ciągi znaków: Template literals zachowują białe znaki (spacje, enter), co pozwala na tworzenie wieloliniowych stringów bez użycia znaków nowej linii (\n). Jest to szczególnie przydatne przy generowaniu kodu HTML czy szablonów.

Wyrażenia osadzone: Wewnątrz ${} można umieszczać nie tylko zmienne, ale także dowolne wyrażenia JavaScript, w tym wywołania funkcji czy operacje matematyczne.

const produkt = 'Laptop';
const cena = 3500;

// Interpolacja: Wstawienie zmiennych
const info = `Produkt: ${produkt} kosztuje ${cena} zł.`;

// Wieloliniowy ciąg:
const html = `
    <div class="karta">
        <h1>${produkt}</h1>
    </div>
`;

Template literals zrewolucjonizowały pracę ze stringami w JavaScript, oferując możliwość interpolacji zmiennych, tworzenia wieloliniowych tekstów i osadzania wyrażeń bezpośrednio w literałach stringowych. Interpolacja za pomocą składni ${} jest czytelniejsza i mniej podatna na błędy niż tradycyjna konkatenacja za pomocą operatora +, szczególnie przy łączeniu wielu zmiennych i wyrażeń. Template literals zachowują białe znaki, co pozwala na naturalne formatowanie wieloliniowych stringów bez uciekania się do znaku nowej linii lub łączenia wielu stringów plusem.

Zaawansowane możliwości template literals obejmują tagowane template literals, gdzie funkcja poprzedzająca backticki otrzymuje przetworzone fragmenty stringa i osadzone wartości, co umożliwia tworzenie własnych mini-języków osadzonych w JavaScript. Biblioteki takie jak styled-components w React używają tagowanych template literals do definiowania stylów CSS w JavaScript, co jest przykładem potęgi tej składni. W codziennej pracy template literals są niezastąpione przy generowaniu kodu HTML w JavaScript, formatowaniu wiadomości i wszędzie tam, gdzie potrzebujemy dynamicznie tworzyć tekst z wstawionymi wartościami.

21Wprowadzenie do Klas (Class) i OOP w JS

Klasy - obiektowe programowanie w JavaScript

Klasy w JavaScript, wprowadzone w ES6, to składniowa nadbudowa (syntactic sugar) nad istniejącym w JavaScript mechanizmem prototypów. Umożliwiają one tworzenie obiektów o podobnej strukturze i zachowaniu w sposób bardziej intuicyjny i zbliżony do klasycznych języków obiektowych jak Java czy C++.

Konstruktor (constructor) to specjalna metoda wywoływana automatycznie podczas tworzenia nowego obiektu za pomocą słowa kluczowego new. Służy do inicjalizacji właściwości obiektu. Może przyjmować argumenty, które użytkownik podaje przy tworzeniu instancji.

Metody to funkcje zdefiniowane wewnątrz klasy, które operują na danych obiektu (dostępnych przez this). Mogą być zwykłymi metodami, metodami statycznymi (dostępnymi bez tworzenia instancji) lub metodami akcesorów (getter/setter).

Dziedziczenie pozwala na tworzenie nowych klas na podstawie istniejących za pomocą słowa kluczowego extends. Klasa potomna dziedziczy wszystkie właściwości i metody klasy rodzicielskiej, a może je nadpisywać lub rozszerzać.

Użycie klas znacząco poprawia organizację kodu, czytelność i możliwość wielokrotnego użycia (reusability) w porównaniu z ręcznym tworzeniem obiektów i zarządzaniem prototypami.

class Samochod {
    constructor(marka) {
        this.marka = marka;
    }
    opis() {
        return `To jest ${this.marka}.`;
    }
}

const mojSamochod = new Samochod('Audi');
console.log(mojSamochod.opis());

Klasy w JavaScript, wprowadzone w ES6, stanowią składniową nadbudowę nad istniejącym mechanizmem prototypów, oferując znajomą składnię dla programistów przyzwyczajonych do klasycznych języków obiektowych. Konstruktor klasy to specjalna metoda wywoływana automatycznie przy tworzeniu instancji za pomocą słowa kluczowego new, która służy do inicjalizacji właściwości obiektu. Metody zdefiniowane w klasie są automatycznie dodawane do prototypu, a nie do każdej instancji, co oszczędza pamięć przy tworzeniu wielu obiektów tej samej klasy.

Dziedziczenie za pomocą extends pozwala na tworzenie hierarchii klas, gdzie klasa potomna dziedziczy wszystkie metody i właściwości klasy rodzicielskiej. Słowo kluczowe super w konstruktorze klasy potomnej wywołuje konstruktor rodzica, co jest wymagane przed użyciem this w klasie dziedziczącej. Metody statyczne, definiowane za pomocą static, są wywoływane na samej klasie, a nie na instancjach, i są używane dla funkcji narzędziowych powiązanych z klasą, takich jak fabryki obiektów czy metody parsowania danych.

22Moduły JS (import/export)

ES Modules - modułowy podział kodu JavaScript

ES Modules (ECMAScript Modules) to oficjalny standard modułów w JavaScript, wprowadzony w ES6, który pozwala na logiczny podział kodu na mniejsze, niezależne pliki. Modularność jest kluczowa dla tworzenia dużych, łatwych w utrzymaniu aplikacji.

Korzyści z używania modułów:

  • Organizacja kodu: Każdy moduł ma swoje własne zasięgi zmiennych, co zapobiega konfliktom nazw między plikami
  • Wielokrotne użycie: Napisany raz kod może być importowany w wielu miejscach projektu
  • Łatwiejsze utrzymanie: Zmiany w jednym module nie wpływają na inne (o ile nie zmieniamy eksportowanych interfejsów)
  • Leniwe ładowanie: Moduły mogą być ładowane na żądanie, co poprawia wydajność

Eksportowanie: Służy do udostępniania kodu innym modułom. Można eksportować pojedyncze wartości (export const PI = 3.14;) lub całe sekcje na końcu pliku (export { PI, log };). Możliwy jest też eksport domyślny (export default) - jeden na moduł.

Importowanie: Służy do pobierania wyeksportowanego kodu: import { PI } from './utils.js' lub import domyślny: import cos from './utils.js'.

W pliku HTML moduły ładujemy za pomocą <script type="module">.

// plik utils.js
export const PI = 3.14; 
export function log(msg) { ... }

// plik app.js
import { PI, log } from './utils.js';
console.log(PI);
log('Z modułu');

Moduły ES6 stanowią oficjalny, standardowy system modułów w JavaScript, który zastąpił wcześniejsze rozwiązania takie jak CommonJS używane w Node.js czy AMD używane w RequireJS. Każdy moduł ma swój własny zakres, co oznacza, że zmienne i funkcje zadeklarowane w module nie są domyślnie dostępne poza nim, chyba że zostaną jawnie wyeksportowane. Importowanie za pomocą składni import z nazwanymi eksportami umożliwia selektywne pobieranie tylko tych elementów modułu, które są potrzebne, co wspiera tree-shaking w bundlerach takich jak Webpack.

Eksport domyślny, używający składni export default, pozwala na wyeksportowanie jednej głównej wartości z modułu, która może być zaimportowana pod dowolną nazwą, co jest wygodne dla głównych komponentów lub funkcji biblioteki. W pliku HTML moduły są ładowane za pomocą znacznika script z atrybutem type="module", co powoduje, że skrypt jest domyślnie deferred i ma własny zakres. Modules są kluczowym elementem nowoczesnego JavaScript, umożliwiając tworzenie dobrze zorganizowanych, testowalnych i łatwych w utrzymaniu aplikacji.

23DOM: Document Object Model - Wprowadzenie

Document Object Model - drzewiasta reprezentacja strony

DOM (Document Object Model) to programistyczny interfejs (API) reprezentujący strukturę dokumentu HTML (lub XML) jako drzewo węzłów. Każdy element strony - tagi HTML, atrybuty, tekst - jest reprezentowany jako obiekt w tym drzewie, co umożliwia programistyczny dostęp i manipulację treścią strony.

DOM jest niezależny od języka programowania - choć najczęściej używamy go z JavaScript. Przeglądarka automatycznie tworzy DOM podczas ładowania strony na podstawie kodu HTML. Dzięki DOM możemy:

  • Zmieniać zawartość i strukturę strony bez przeładowania
  • Dodawać, usuwać i modyfikować elementy
  • Reagować na akcje użytkownika
  • Dynamicznie zmieniać style i wygląd elementów
  • Odczytywać i modyfikować atrybuty elementów

Głównym punktem dostępu do DOM jest obiekt document, który reprezentuje cały dokument HTML. Wszystkie operacje na DOM zaczynają się od tego obiektu.

console.log(document); // Zwraca cały obiekt dokumentu
console.log(document.body); // Dostęp do węzła body

Document Object Model to interfejs programistyczny reprezentujący strukturę dokumentu HTML jako drzewo węzłów, gdzie każdy element HTML, atrybut i fragment tekstu jest reprezentowany jako obiekt. DOM jest tworzony automatycznie przez przeglądarkę podczas ładowania strony na podstawie kodu HTML i jest dostępny do manipulacji za pomocą JavaScript. Główny obiekt document stanowi punkt wejścia do całego drzewa DOM, udostępniając metody do wyszukiwania, tworzenia i modyfikowania elementów.

Manipulacja DOM-em jest kluczową umiejętnością front-end developera, ponieważ umożliwia dynamiczne aktualizowanie treści i struktury strony bez konieczności przeładowywania całego dokumentu. Każda zmiana w DOM powoduje repaint i reflow w przeglądarce, co ma wpływ na wydajność, dlatego ważne jest minimalizowanie liczby operacji DOM i grupowanie ich w celu zmniejszenia kosztownych przerysowań. Nowoczesne frameworki takie jak React używają wirtualnego DOM-u do optymalizacji tych operacji, ale zrozumienie natywnego DOM-u pozostaje ważne dla debugowania i tworzenia wydajnych aplikacji.

24Wyszukiwanie Elementów: getElementById()

getElementById - najszybszy sposób dostępu do elementu

document.getElementById(id) jest jedną z najstarszych i najczęściej używanych metod DOM. Pozwala na szybki dostęp do pojedynczego elementu HTML na podstawie jego atrybutu id.

Zasady używania ID:

  • Atrybut id musi być unikalny w całym dokumencie HTML - żadne dwa elementy nie powinny mieć tego samego ID
  • ID jest wrażliwe na wielkość liter (case-sensitive)
  • Nazwa ID powinna zaczynać się od litery i może zawierać litery, cyfry, myślniki i podkreślenia
  • Unikaj znaków specjalnych i spacji w nazwach ID

Wydajność: getElementById jest zoptymalizowana przez przeglądarki jako najszybsza metoda wyszukiwania elementów, ponieważ ID jest z definicji unikalne i przeglądarka buduje dla niego specjalny indeks.

Obsługa błędów: Metoda zwraca null, jeśli element o podanym ID nie istnieje w dokumencie. Zawsze warto sprawdzać, czy element został znaleziony, zanim spróbujemy na nim operować.

const mojDiv = document.getElementById('glowny-kontener');
if (mojDiv) {
    mojDiv.style.backgroundColor = 'yellow';
}

Metoda getElementById jest najszybszym sposobem dostępu do elementu DOM, ponieważ przeglądarka utrzymuje wewnętrzną mapę ID do elementów, co zapewnia stały czas dostępu O(1). Unikalność ID w całym dokumencie jest wymuszana przez specyfikację HTML, choć przeglądarki nie zgłaszają błędu przy duplikatach, zachowując się nieprzewidywalnie. Konwencje nazewnictwa ID zalecają używanie małych liter, myślników do oddzielania słów i unikanie znaków specjalnych, co jest zgodne z zasadami CSS i poprawia czytelność kodu.

Obsługa przypadku, gdy element nie istnieje, jest kluczowa dla robustności kodu, ponieważ getElementById zwraca null dla nieistniejącego ID. Próba dostępu do właściwości na null skutkuje błędem TypeError, który zatrzymuje wykonanie skryptu, dlatego zawsze należy sprawdzać, czy wynik nie jest nullem przed wykonywaniem operacji. W nowoczesnym JavaScript można użyć optional chaining operatora ?. do bezpiecznego dostępu do właściwości, na przykład document.getElementById('id')?.textContent, co automatycznie zwraca undefined dla nieistniejącego elementu.

25Wyszukiwanie Elementów: querySelector/All

querySelector i querySelectorAll - selektory CSS w JavaScript

querySelector() i querySelectorAll() to bardzo elastyczne metody wyszukiwania elementów w DOM, które pozwalają używać pełnej składni selektorów CSS. Są one bardziej uniwersalne niż starsze metody jak getElementById czy getElementsByClassName.

querySelector(selector) zwraca pierwszy element pasujący do podanego selektora CSS lub null, jeśli nie znajdzie żadnego. Na przykład: document.querySelector('.karta') zwróci pierwszy element z klasą "karta".

querySelectorAll(selector) zwraca statyczną NodeList (listę węzłów) zawierającą wszystkie elementy pasujące do selektora. Lista ta nie aktualizuje się automatycznie przy zmianach w DOM - jeśli dodamy nowy element pasujący do selektora, nie pojawi się on w już pobranej liście.

Przykłady selektorów CSS:

  • '.klasa' - elementy z klasą
  • '#id' - element z ID
  • 'tag' - elementy danego typu (np. 'div', 'p')
  • 'div.klasa' - elementy div z klasą
  • 'nav a' - linki wewnątrz nav
  • '[atrybut]' - elementy z atrybutem

NodeList ma metodę .forEach(), co pozwala na łatwą iterację. Można też konwertować ją do tablicy za pomocą Array.from().

// Pierwszy element z klasa 'karta'
const pierwsza = document.querySelector('.karta'); 

// Wszystkie linki wewnątrz menu
const linki = document.querySelectorAll('nav a'); 

// Linki można iterować za pomocą forEach
linki.forEach(a => a.textContent += ' [OK]');

Metody querySelector i querySelectorAll są bardziej elastyczne niż starsze metody wyszukiwania, ponieważ akceptują dowolny selektor CSS, co pozwala na precyzyjne wyszukiwanie elementów według klas, ID, atrybutów, pseudoklas i złożonych relacji. QuerySelector zwraca pierwszy pasujący element lub null, co jest przydatne, gdy spodziewamy się tylko jednego wystąpienia. QuerySelectorAll zwraca statyczną NodeList, która migawkuje stan DOM w momencie wywołania i nie aktualizuje się przy późniejszych zmianach, w przeciwieństwie do żywych kolekcji HTMLCollection.

NodeList zwrócona przez querySelectorAll posiada metodę forEach, co pozwala na bezpośrednią iterację bez konieczności konwersji do tablicy. W przypadku potrzeby użycia innych metod tablicowych, takich jak map czy filter, można łatwo skonwertować NodeList na tablicę za pomocą Array.from lub operatora spread. W praktyce querySelectorAll jest preferowanym sposobem wyszukiwania wielu elementów w nowoczesnym JavaScript, ponieważ oferuje pełną moc selektorów CSS i spójne API, w przeciwieństwie do starszych metod z różnymi typami zwracanych kolekcji.

26Wyszukiwanie Elementów: Inne Metody (className, tagName)

Dedykowane metody wyszukiwania elementów

Oprócz uniwersalnych selektorów CSS, DOM oferuje również dedykowane metody do wyszukiwania elementów, które choć rzadziej używane w nowoczesnym kodzie, to wciąż bywają przydatne i wydajne w określonych sytuacjach.

getElementsByClassName(className) zwraca HTMLCollection (kolekcję HTML) wszystkich elementów posiadających podaną nazwę klasy. Kolekcja ta jest żywa (dynamiczna) - automatycznie aktualizuje się, gdy zmieniamy klasy elementów w DOM.

getElementsByTagName(tagName) zwraca HTMLCollection wszystkich elementów danego typu (np. 'div', 'p', 'a'). Można też wywołać na konkretnym elemencie (np. document.body.getElementsByTagName('p')), aby szukać tylko w jego potomkach.

getElementsByName(name) zwraca kolekcję elementów z atrybutem name o podanej wartości (często używane dla pól formularzy).

Ważna różnica: HTMLCollection (zwracana przez te metody) jest strukturą podobną do tablicy, ale nie ma metody .forEach(). Aby iterować, trzeba użyć pętli for lub konwersji do tablicy: Array.from(kolekcja).forEach(...).

const paragrafy = document.getElementsByTagName('p');
const aktywne = document.getElementsByClassName('active');

// Uwaga: HTMLCollection nie ma metody .forEach!
// Należy użyć: Array.from(paragrafy).forEach(...)

Starsze metody wyszukiwania elementów, takie jak getElementsByClassName i getElementsByTagName, wciąż mają swoje zastosowanie, szczególnie gdy potrzebujemy żywej kolekcji, która automatycznie odzwierciedla zmiany w DOM. Żywa kolekcja HTMLCollection jest przydatna, gdy dynamicznie dodajemy lub usuwamy elementy pasujące do selektora i chcemy, aby kolekcja automatycznie się aktualizowała bez ponownego zapytania. Jednak ta cecha może prowadzić do nieoczekiwanych rezultatów podczas iteracji, jeśli w trakcie pętli modyfikujemy DOM, co jest częstym źródłem błędów.

Głównym ograniczeniem HTMLCollection jest brak metody forEach i innych metod tablicowych, co wymaga dodatkowych kroków przy iteracji. Konwersja do tablicy za pomocą Array.from lub pętla for z indeksem są najczęstszymi rozwiązaniami. W praktyce zaleca się używanie querySelectorAll zamiast starszych metod, chyba że istnieje konkretna potrzeba korzystania z żywej kolekcji, ponieważ offeruje ona bogatsze API i pełną moc selektorów CSS, co czyni kod bardziej spójnym i czytelnym.

27Modyfikacja Treści: innerHTML vs textContent

innerHTML vs textContent - modyfikacja zawartości elementu

Dostęp do zawartości elementów DOM jest podstawową operacją w dynamicznych stronach internetowych. JavaScript oferuje kilka sposobów odczytu i modyfikacji treści, z których najważniejsze to innerHTML i textContent.

textContent reprezentuje czystą zawartość tekstową elementu. Po ustawieniu wartości, cały przekazany tekst jest traktowany dosłownie jako tekst - nawet jeśli zawiera tagi HTML, zostaną one wyświetlone jako zwykły tekst, nie jako elementy HTML. Jest to bezpieczniejsze rozwiązanie, ponieważ eliminuje ryzyko ataków XSS (Cross-Site Scripting), gdzie atakujący mógłby wstrzyknąć złośliwy kod JavaScript poprzez formularze lub URL.

innerHTML reprezentuje zawartość HTML elementu, włącznie z tagami. Po ustawieniu wartości, przeglądarka parsuje i renderuje kod HTML. Jest to wygodne do wstawiania fragmentów HTML, ale wymaga ostrożności - nigdy nie używaj innerHTML z danymi pochodzącymi od użytkownika bez uprzedniego "oczyszczenia" (sanitization), ponieważ otwiera to drzwi do ataków XSS.

Wskazówka: Jeśli chcesz tylko wyświetlić lub zmienić tekst, używaj textContent. Jeśli musisz wstawić kod HTML, używaj innerHTML, ale zawsze kontroluj źródło danych.

const div = document.getElementById('msg');

// Bezpieczne
div.textContent = "Witaj!"; 

// Renderuje tag (potencjalnie niebezpieczne)
div.innerHTML = "<b>Witaj!</b>"; 

// Pobranie zawartości
const html = div.innerHTML;

Wybór między innerHTML a textContent ma istotne implikacje dla bezpieczeństwa i wydajności aplikacji webowych. innerHTML parsuje przekazany string jako HTML, co pozwala na wstawianie złożonych struktur, ale otwiera drzwi do ataków XSS, jeśli dane pochodzą od użytkownika lub z niepewnego źródła. textContent interpretuje wszystko jako czysty tekst, automatycznie escape'ując znaki HTML, co czyni go bezpiecznym wyborem do wyświetlania danych użytkownika i zapobiega wstrzykiwaniu kodu.

Z perspektywy wydajności innerHTML jest wolniejszy niż textContent, ponieważ wymaga parsowania HTML i potencjalnie kosztownego przerysowania DOM. Dla prostych operacji tekstowych textContent jest znacznie szybszy i bezpieczniejszy. Biblioteka DOMPurify może być używana do bezpiecznego oczyszczania HTML przed użyciem innerHTML, gdy konieczne jest wstawianie treści HTML z niepewnych źródeł. W nowoczesnych aplikacjach rzadko używa się bezpośrednio innerHTML, preferując frameworki z bezpiecznymi mechanizmami renderowania szablonów.

28Zarządzanie Atrybutami (get, set, remove)

Zarządzanie atrybutami HTML elementów

Atrybuty HTML to dodatkowe wartości konfiguracyjne elementów, takie jak src dla obrazów, href dla linków, id dla identyfikatorów czy atrybuty niestandardowe data-* do przechowywania dodatkowych informacji. JavaScript oferuje standardowe metody do manipulacji atrybutami.

getAttribute(nazwa) służy do odczytu wartości atrybutu. Zwraca null, jeśli atrybut nie istnieje. Na przykład: element.getAttribute('href') zwróci adres URL linka.

setAttribute(nazwa, wartosc) ustawia wartość atrybutu. Jeśli atrybut już istnieje, jego wartość zostanie nadpisana. Jeśli nie istnieje, zostanie utworzony. Przykład: img.setAttribute('src', 'nowy-obrazek.jpg').

removeAttribute(nazwa) usuwa atrybut całkowicie z elementu. Jest to preferowane nad ustawianiem pustej wartości.

Atrybuty niestandardowe data-*: Atrybuty z prefiksem data- są specjalnie przeznaczone do przechowywania niestandardowych danych w elemencie HTML, które mogą być później odczytane przez JavaScript. Np. <div data-user-id="123">.

const img = document.querySelector('img');

img.setAttribute('src', 'nowy.jpg');
const aktualnySrc = img.getAttribute('src');

img.removeAttribute('width'); // Usuń atrybut width

Manipulacja atrybutami HTML za pomocą metod getAttribute, setAttribute i removeAttribute daje programiście pełną kontrolę nad właściwościami elementów. Atrybuty niestandardowe data-* są szczególnie przydatne do przechowywania dodatkowych danych w elementach HTML, które mogą być później odczytane przez JavaScript za pomocą właściwości dataset. Na przykład element z atrybutem data-user-id="123" ma dostępne document.getElementById('el').dataset.userId, które automatycznie konwertuje myślniki na camelCase.

W nowoczesnym JavaScript często preferuje się bezpośredni dostęp do właściwości DOM zamiast używania metod getAttribute i setAttribute, ponieważ są one szybsze i bardziej czytelne. Jednak metody atrybutów są niezbędne w sytuacjach, gdy potrzebujemy pracować z atrybutami, które nie mają odpowiadających właściwości DOM, lub gdy chcemy odczytać oryginalną wartość atrybutu HTML przed jego modyfikacją przez JavaScript. Różnica między wartością atrybutu a wartością właściwości jest subtelna, ale ważna przy pracy z formularzami i linkami.

29Zarządzanie Klasami CSS (classList)

classList - wygodna manipulacja klasami CSS

Właściwość classList elementu DOM zwraca obiekt DOMTokenList, który reprezentuje listę klas CSS elementu. Obiekt ten udostępnia wygodne metody do manipulowania klasami, znacznie łatwiejsze niż ręczne przetwarzanie atrybutu class jako stringa.

.add('klasa1', 'klasa2', ...) dodaje jedną lub więcej klas do elementu. Jeśli klasa już istnieje, nie zostanie dodana ponownie.

.remove('klasa1', 'klasa2', ...) usuwa jedną lub więcej klas z elementu. Jeśli klasa nie istnieje, nic się nie dzieje (nie ma błędu).

.toggle('klasa') jest niezwykle użyteczną metodą - dodaje klasę, jeśli nie istnieje, lub usuwa ją, jeśli już istnieje. Jest to idealne do implementacji przełączników (np. otwieranie/zamykanie menu, włączanie/wyłączanie trybu ciemnego).

.contains('klasa') zwraca true, jeśli element posiada podaną klasę, lub false w przeciwnym razie. Używamy tego do sprawdzania stanu.

.replace('staraKlasa', 'nowaKlasa') zastępuje jedną klasę drugą.

.length zwraca liczbę klas przypisanych do elementu.

const przycisk = document.getElementById('btn');

przycisk.classList.add('aktywny');
przycisk.classList.toggle('ukryty');

if (przycisk.classList.contains('aktywny')) {
    console.log('Przycisk jest aktywny.');
}

classList to nowoczesne API do manipulacji klasami CSS, które eliminuje konieczność ręcznego przetwarzania stringa klasy za pomocą operacji na stringach. Metoda add dodaje klasy bez ryzyka duplikacji, remove usuwa klasy bez zgłaszania błędu, jeśli klasa nie istnieje, a toggle elegancko przełącza klasę między stanem obecnym i nieobecnym. Contains umożliwia sprawdzenie obecności klasy, co jest przydatne przy implementacji logiki warunkowej opartej na stanie wizualnym elementu.

W porównaniu do starszego podejścia z manipulacją atrybutem className za pomocą stringów, classList jest bardziej czytelne, mniej podatne na błędy i oferuje bogatsze API. Metoda replace pozwala na zastąpienie jednej klasy drugą w jednej operacji, co jest przydatne przy przełączaniu między stylami, na przykład między trybem jasnym i ciemnym. classList jest wspierany we wszystkich nowoczesnych przeglądarkach i jest standardem w profesjonalnym web developmentzie, zastępując przestarzałe techniki manipulacji klasami.

30Bezpośrednia Modyfikacja Stylu (Inline)

Bezpośrednia modyfikacja stylów inline

Każdy element DOM posiada właściwość style, która reprezentuje atrybut style elementu HTML (style inline). Umożliwia ona bezpośrednie ustawianie stylów CSS dla konkretnego elementu poprzez JavaScript.

Składnia właściwości: Nazwy właściwości CSS są konwertowane na notację camelCase w JavaScript. Na przykład: background-color staje się backgroundColor, font-size staje się fontSize, margin-top staje się marginTop. Jest to konieczne, ponieważ myślniki nie są dozwolone w nazwach właściwości obiektów JavaScript.

Wartości: Wartości stylów zawsze podajemy jako stringi, włącznie z jednostkami (np. '10px', 'red', 'bold').

Usuwanie stylów: Aby usunąć styl inline i przywrócić domyślne style (z CSS), wystarczy ustawić pusty string: element.style.backgroundColor = ''.

Kiedy używać: Style inline są przydatne do dynamicznych, niewielkich zmian wyglądu (np. animacje, reakcje na hover). Dla większych i stałych modyfikacji zaleca się używanie klas CSS i manipulację nimi przez classList.

const div = document.getElementById('msg');

// Zmiana koloru tła i marginesu
div.style.backgroundColor = 'navy';
div.style.marginTop = '10px';

// Usuń styl (wraca do stylów z CSS)
div.style.backgroundColor = '';

Modyfikacja stylów inline za pomocą właściwości style elementu DOM pozwala na dynamiczne zmiany wyglądu w odpowiedzi na interakcje użytkownika lub zmiany stanu aplikacji. Konwersja nazw właściwości CSS z kebab-case na camelCase jest konieczna ze względu na ograniczenia składni JavaScript, gdzie myślniki są interpretowane jako operator odejmowania. Ustawianie stylów inline ma najwyższy priorytet w kaskadzie CSS, nadpisując style z klas i ID, co czyni tę metodę potężną, ale wymagającą ostrożności.

Mimo swojej mocy, style inline powinny być używane oszczędnie, preferując manipulację klasami CSS za pomocą classList dla większych zmian wyglądu. Style inline są idealne dla dynamicznych wartości, które nie są znane w czasie projektowania, takich jak pozycje elementów obliczane w runtime czy kolory generowane losowo. Dla animacji i przejść CSS, preferowanym rozwiązaniem jest używanie klas CSS w połączeniu z właściwościami transition i animation, co jest bardziej wydajne niż modyfikacja stylów inline w każdej klatce animacji.

31Tworzenie i Wstawianie Nowych Elementów (createElement)

Dynamiczne tworzenie i wstawianie elementów DOM

JavaScript umożliwia tworzenie nowych elementów HTML "w locie" i wstawianie ich do drzewa DOM. Jest to podstawowa technika używana przy dynamicznych interfejsach użytkownika, gdzie zawartość strony zmienia się bez przeładowania.

Sekwencja tworzenia elementu:

  1. createElement(tagName) - tworzy nowy węzeł elementu. Na tym etapie element istnieje tylko w pamięci JavaScript, nie jest jeszcze częścią strony.
  2. Konfiguracja - ustawiamy atrybuty (setAttribute, classList), treść (textContent, innerHTML) i ewentualne inne właściwości
  3. Wstawienie do DOM - używamy metod appendChild(), insertBefore() lub prepend()

appendChild(element) dodaje element na końcu listy dzieci rodzica.

insertBefore(element, referencyjny) wstawia element przed określonym elementem potomnym.

remove() usuwa element z DOM.

cloneNode(deep) tworzy kopię elementu. Parametr true kopiuje również wszystkie dzieci.

const nowyP = document.createElement('p');
nowyP.textContent = 'Utworzono dynamicznie.';
nowyP.classList.add('info');

const kontener = document.getElementById('glowny');
// Wstaw na koniec kontenera
kontener.appendChild(nowyP);

// Usuwanie elementu
nowyP.remove();

Dynamiczne tworzenie elementów za pomocą createElement i wstawianie ich do DOM jest podstawową techniką przy budowie interaktywnych interfejsów użytkownika. Proces tworzenia elementu składa się z trzech etapów: utworzenia elementu w pamięci za pomocą createElement, skonfigurowania jego właściwości i atrybutów, a następnie wstawienia do drzewa DOM. Dopiero po wstawieniu element staje się widoczny dla użytkownika i podlega renderowaniu przez przeglądarkę, co daje programiście pełną kontrolę nad kolejnością i momentem pojawienia się treści.

Metoda appendChild dodaje element na koniec listy dzieci rodzica, podczas gdy insertBefore pozwala na wstawienie w określone miejsce. Nowoczesne metody prepend, append, before, after i replaceWith oferują bardziej elastyczne opcje wstawiania, w tym możliwość dodawania wielu elementów i tekstów jednocześnie. Metoda remove usuwa element z DOM, a cloneNode z parametrem true tworzy głęboką kopię wraz z wszystkimi dziećmi, co jest przydatne przy tworzeniu szablonów i powtarzalnych komponentów.

32Model Zdarzeń: Nasłuchiwanie (addEventListener)

addEventListener - nasłuchiwanie zdarzeń w DOM

Zdarzenia (events) to sygnały generowane przez przeglądarkę lub użytkownika, takie jak kliknięcia myszy, naciśnięcia klawiszy, ładowanie strony czy zmiany w formularzach. Aby reagować na te zdarzenia, używamy metody addEventListener().

Składnia: element.addEventListener(typZdarzenia, funkcjaObslugujaca, opcje)

Zalety addEventListener:

  • Pozwala na dodanie wielu nasłuchiwaczy do tego samego elementu i zdarzenia - wszystkie zostaną wywołane
  • Umożliwia usuwanie nasłuchiwaczy za pomocą removeEventListener()
  • Daje kontrolę nad fazą propagacji (capturing lub bubbling)
  • Działa na dowolnym obiekcie EventTarget, nie tylko na elementach DOM

Funkcja obsługująca może być zwykłą funkcją, funkcją strzałkową lub referencją do nazwanej funkcji. Pamiętaj, że do usunięcia nasłuchiwacza za pomocą removeEventListener musisz przekazać tę samą referencję funkcji.

const btn = document.getElementById('clicker');

btn.addEventListener('click', function() {
    console.log('Kliknięto!');
});

// Usuwanie nasłuchu
btn.removeEventListener('click', mojaFunkcja);

Model zdarzeń w DOM jest jednym z najważniejszych aspektów interaktywnego JavaScriptu, umożliwiającym reakcję na działania użytkownika i zdarzenia systemowe. Metoda addEventListener jest preferowanym sposobem rejestracji nasłuchiwaczy, ponieważ pozwala na dodanie wielu handlerów dla tego samego zdarzenia na jednym elemencie, co nie jest możliwe przy użyciu starszej metody onEvent. Trzeci parametr addEventListener może być obiektem opcji, który umożliwia ustawienie flag takich jak once dla jednorazowego wykonania, passive dla optymalizacji wydajności scrolla i signal do grupowego usuwania nasłuchiwaczy.

Usuwanie nasłuchiwaczy za pomocą removeEventListener jest ważne dla zapobiegania wyciekom pamięci, szczególnie w aplikacjach jednostronicowych, gdzie komponenty są dynamicznie tworzone i niszczone. Aby skutecznie usunąć nasłuchiwacz, należy przekazać tę samą referencję funkcji, co oznacza, że nie można użyć anonimowej funkcji, jeśli planujemy jej późniejsze usunięcie. W praktyce często używa się nazwanych funkcji lub referencji do funkcji strzałkowych przechowywanych w zmiennych, co umożliwia czyste sprzątanie przy odmontowywaniu komponentów.

33Obiekt Event: e.target, preventDefault()

Obiekt Event - informacje o zdarzeniu

Każda funkcja obsługująca zdarzenie (callback) otrzymuje automatycznie obiekt Event jako pierwszy argument. Obiekt ten zawiera mnóstwo przydatnych informacji o zdarzeniu, które właśnie wystąpiło. Konwencjonalnie nazywamy go e, event lub evt.

Najważniejsze właściwości obiektu Event:

  • e.target - element HTML, który wygenerował zdarzenie (np. kliknięty przycisk)
  • e.currentTarget - element, na którym nasłuchujemy zdarzenia (dla bubbling - może być inny niż target)
  • e.type - typ zdarzenia jako string (np. "click", "keydown")
  • e.key - dla zdarzeń klawiatury: kod naciśniętego klawisza
  • e.clientX / e.clientY - współrzędne kursora myszy względem okna przeglądarki

Najważniejsze metody:

  • e.preventDefault() - blokuje domyślną akcję przeglądarki (np. wysłanie formularza, przeładowanie strony po kliknięciu linku)
  • e.stopPropagation() - zatrzymuje propagację zdarzenia (bubbling/capturing) w drzewie DOM
  • e.stopImmediatePropagation() - zatrzymuje propagację i blokuje inne nasłuchiwacze na tym samym elemencie
document.querySelector('a').addEventListener('click', (e) => {
    e.preventDefault();
    console.log('Przejście zablokowane!');
});

Obiekt Event jest automatycznie przekazywany do funkcji obsługującej zdarzenie i zawiera bogaty zestaw informacji o okolicznościach wystąpienia zdarzenia. Właściwość target wskazuje na element, który wygenerował zdarzenie, podczas gdy currentTarget wskazuje na element, na którym zarejestrowano nasłuchiwacz, co jest szczególnie ważne przy delegacji zdarzeń. W przypadku zdarzeń myszy właściwości clientX i clientY podają współrzędne kursora względem okna przeglądarki, a pageX i pageY względem całego dokumentu, co ma znaczenie przy przewijaniu strony.

Metoda preventDefault jest jednym z najczęściej używanych narzędzi w obsłudze zdarzeń, pozwalającym na blokowanie domyślnych akcji przeglądarki, takich jak przesłanie formularza, nawigacja po kliknięciu linku czy otwarcie menu kontekstowego. Metoda stopPropagation zatrzymuje dalszą propagację zdarzenia w drzewie DOM, co jest przydatne przy precyzyjnym kontrolowaniu, które elementy mają reagować na zdarzenie. W praktyce te dwie metody są często używane razem, szczególnie przy implementacji niestandardowych interakcji i zapobieganiu niepożądanym zachowaniom przeglądarki.

34Typy Zdarzeń: Mysz (click, mouseover, contextmenu)

Zdarzenia myszy - interakcje z kursorem

Zdarzenia myszy pozwalają reagować na akcje użytkownika wykonywane za pomocą myszy lub touchpada. Są one fundamentem interaktywności większości stron internetowych.

Podstawowe zdarzenia myszy:

  • click - pojedyncze kliknięcie lewym przyciskiem myszy
  • dblclick - podwójne kliknięcie (double click)
  • mousedown - przycisk myszy został naciśnięty (niezależnie od tego, który)
  • mouseup - przycisk myszy został zwolniony
  • mouseover (lub mouseenter) - kursor myszy najechał na element
  • mouseout (lub mouseleave) - kursor myszy opuścił element
  • mousemove - kursor myszy porusza się nad elementem (bardzo częste zdarzenie!)
  • contextmenu - kliknięcie prawym przyciskiem myszy (menu kontekstowe)

Różnica między mouseover/mouseout a mouseenter/mouseleave: Zdarzenia z "enter/leave" nie propagują (nie bubbling), natomiast "over/out" propagują do rodziców. Dla zagnieżdżonych elementów wybór ma znaczenie.

const div = document.getElementById('box');
div.addEventListener('mouseover', () => {
    div.style.backgroundColor = 'red';
});
div.addEventListener('mouseout', () => {
    div.style.backgroundColor = 'initial';
});

Zdarzenia myszy stanowią podstawowy mechanizm interakcji użytkownika z interfejsem graficznym i są niezbędne przy tworzeniu elementów takich jak przyciski, menu rozwijane, przeciąganie elementów czy interaktywne wykresy. Zdarzenie click jest najczęściej używane i generowane zarówno po kliknięciu lewym przyciskiem, jak i po aktywacji elementu za pomocą klawiatury, co zapewnia dostępność. Zdarzenia mouseenter i mouseleave są preferowane nad mouseover i mouseout dla efektów hover, ponieważ nie propagują i nie są wywoływane przy przechodzeniu między elementami potomnymi, co eliminuje niepożądane migotanie.

Zdarzenie contextmenu jest wywoływane przy kliknięciu prawym przyciskiem myszy i domyślnie otwiera menu kontekstowe przeglądarki. Poprzez wywołanie preventDefault na tym zdarzeniu można zastąpić domyślne menu własnym, niestandardowym menu kontekstowym, co jest popularne w zaawansowanych aplikacjach webowych. Zdarzenie mousemove jest generowane bardzo często podczas ruchu myszy i należy używać go rozsądnie, aby nie przeciążyć głównego wątku, szczególnie przy skomplikowanych obliczeniach w handlerze.

35Typy Zdarzeń: Klawiatura (keydown, keyup)

Zdarzenia klawiatury - reagowanie na naciśnięcia klawiszy

Zdarzenia klawiatury pozwalają reagować na interakcje użytkownika z klawiaturą. Są niezbędne przy tworzeniu skrótów klawiszowych, walidacji formularzy w czasie rzeczywistym, gier przeglądarkowych i wielu innych interaktywnych funkcji.

Podstawowe zdarzenia klawiatury:

  • keydown - klawisz został naciśnięty w dół (generowane przy każdym naciśnięciu, również przy przytrzymaniu)
  • keyup - klawisz został zwolniony (generowane raz przy puszczeniu)
  • keypress - (przestarzałe, nie używać) - generowane dla znaków drukowalnych

Dostęp do informacji o klawiszu:

  • e.key - zwraca wartość klawisza jako string (np. 'a', 'Enter', 'ArrowUp', 'Escape'). Zalecane do użycia.
  • e.code - zwraca kod fizycznego klawisza na klawiaturze (np. 'KeyA', 'Enter') - nie zależy od układu klawiatury
  • e.ctrlKey, e.shiftKey, e.altKey - flagi wskazujące, czy modyfikatory były wciśnięte

Przykładowe wartości e.key: 'a' (litera), '1' (cyfra), 'Enter', 'Escape', 'ArrowUp', 'ArrowDown', ' ' (spacja), 'F1' (klawisz funkcyjny).

document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
        console.log('Wciśnięto ESC.');
    }
});

Zdarzenia klawiatury umożliwiają tworzenie zaawansowanych interfejsów obsługiwanych za pomocą klawiatury, co jest kluczowe dla dostępności i wydajności pracy zaawansowanych użytkowników. Zdarzenie keydown jest generowane przy każdym naciśnięciu klawisza i powtarza się przy przytrzymaniu, co pozwala na implementację ciągłych akcji, takich jak poruszanie postacią w grze. Zdarzenie keyup jest generowane jednorazowo przy zwolnieniu klawisza, co jest przydatne do wykrywania, kiedy użytkownik przestał naciskać dany klawisz.

Właściwość e.key zwraca wartość znaku wygenerowanego przez naciśnięcie klawisza, na przykład 'a', 'Enter', 'ArrowUp', co jest zalecane do większości zastosowań ze względu na czytelność. Właściwość e.code zwraca fizyczny kod klawisza niezależnie od układu klawiatury, na przykład 'KeyA' dla klawisza, na którym w układzie QWERTY znajduje się litera A, co jest przydatne w grach i aplikacjach, gdzie ważne jest fizyczne położenie klawiszy. Modyfikatory ctrlKey, shiftKey, altKey i metaKey pozwalają na wykrywanie kombinacji klawiszy, co jest podstawą skrótów klawiszowych.

36Typy Zdarzeń: Formularze (submit, input, change)

Zdarzenia formularzy - interakcja z inputami

Formularze HTML są podstawowym sposobem zbierania danych od użytkownika. JavaScript oferuje specjalne zdarzenia związane z formularzami i ich polami wejściowymi, które pozwalają na walidację, przetwarzanie i reagowanie na zmiany w czasie rzeczywistym.

submit - generowane, gdy formularz zostaje wysłany (np. przez kliknięcie przycisku submit lub naciśnięcie Enter w polu tekstowym). Zawsze używaj e.preventDefault(), jeśli chcesz obsłużyć formularz w JavaScript bez przeładowania strony.

input - generowane za każdym razem, gdy wartość pola się zmienia (dla inputów tekstowych: przy każdym wpisaniu/usunięciu znaku). Idealne do wyszukiwania na żywo, liczników znaków, autozapisu.

change - generowane, gdy wartość pola zostanie zmieniona i pole straci fokus (dla pól tekstowych). Dla elementów select, checkbox i radio - generowane natychmiast po zmianie wartości.

focus / blur - generowane, gdy pole otrzymuje / traci fokus. Przydatne do podświetlania aktywnych pól, walidacji przy utracie fokusu.

reset - generowane po zresetowaniu formularza (przycisk type="reset").

const input = document.querySelector('input');

input.addEventListener('input', (e) => {
    console.log(e.target.value); // Wartość pola w czasie rzeczywistym
});

document.querySelector('form').addEventListener('submit', (e) => {
    e.preventDefault();
    console.log('Formularz zweryfikowany.');
});

Zdarzenia formularzy są kluczowe dla tworzenia interaktywnych i responsywnych formularzy, które dostarczają użytkownikowi informacji zwrotnej w czasie rzeczywistym. Zdarzenie submit jest najważniejszym zdarzeniem formularza, ponieważ umożliwia przechwycenie próby wysłania danych i wykonanie własnej logiki walidacji oraz przetwarzania przed faktycznym wysłaniem żądania do serwera. Wywołanie preventDefault na zdarzeniu submit jest standardową praktyką w aplikacjach jednostronicowych, gdzie komunikacja z serwerem odbywa się przez Fetch API bez przeładowywania strony.

Zdarzenie input jest generowane przy każdej zmianie wartości pola, włączając wklejanie i wycinanie tekstu, co czyni je idealnym do implementacji wyszukiwania na żywo, autouzupełniania i walidacji w czasie rzeczywistym. Zdarzenie change różni się od input tym, że jest wywoływane dopiero po utracie fokusu przez pole, co jest odpowiednie dla walidacji, która nie wymaga natychmiastowej reakcji. Zdarzenia focus i blur są używane do zarządzania stanem fokusu, co jest ważne dla dostępności i implementacji zaawansowanych interfejsów, takich jak rozwijane listy sugestii.

37Propagacja Zdarzeń: Bubbling i Capturing

Propagacja zdarzeń - fazy capturing i bubbling

Gdy zdarzenie występuje na elemencie, nie ogranicza się ono tylko do tego jednego elementu. Zdarzenie "podróżuje" przez drzewo DOM w dwóch fazach, co nazywamy propagacją zdarzeń (event propagation).

Faza Capturing (przechwytywania): Zdarzenie rozpoczyna swoją podróż od korzenia dokumentu (window/document) i schodzi w dół drzewa DOM aż do elementu docelowego. Ta faza jest rzadziej używana.

Faza Bubbling (bańki mydlane): Po dotarciu do elementu docelowego, zdarzenie "podnosi się" z powrotem do góry drzewa, przechodząc przez wszystkich przodków elementu docelowego aż do korzenia dokumentu. Ta faza jest domyślnie używana przez addEventListener().

Trzeci parametr addEventListener: Trzeci argument metody addEventListener kontroluje fazę. Domyślnie false (bubbling). Ustawienie na true używa fazy capturing.

e.stopPropagation() - zatrzymuje dalszą propagację zdarzenia (zarówno w capturing, jak i bubbling). Zdarzenie nie dotrze do kolejnych przodków/potombów.

Praktyczne zastosowanie: Zrozumienie propagacji jest kluczowe dla delegacji zdarzeń - wydajnej techniki obsługi zdarzeń dla wielu elementów jednocześnie.

// Zatrzymanie propagacji (powstrzymuje bubbling)
const child = document.getElementById('child');
child.addEventListener('click', (e) => {
    console.log('Kliknięto dziecko');
    e.stopPropagation(); // Zdarzenie nie pójdzie do rodzica
});

Propagacja zdarzeń w DOM odbywa się w trzech fazach: capturing, target i bubbling, co daje programiście elastyczność w określaniu, na którym etapie zdarzenie ma być obsłużone. Domyślnie nasłuchiwacze są wykonywane w fazie bubbling, co oznacza, że zdarzenie najpierw dociera do elementu docelowego, a następnie wędruje w górę drzewa DOM do korzenia dokumentu. Zrozumienie propagacji jest kluczowe dla debugowania nieoczekiwanego zachowania, gdy wiele zagnieżdżonych elementów ma nasłuchiwacze na to samo zdarzenie.

Delegacja zdarzeń opiera się na wykorzystaniu bubblingu do obsługi zdarzeń dla wielu elementów za pomocą jednego nasłuchiwacza na wspólnym rodzicu. Ta technika jest szczególnie wydajna dla długich list i dynamicznie dodawanych elementów, ponieważ nie wymaga rejestrowania nasłuchiwacza dla każdego elementu z osobna. W praktyce delegacja zdarzeń jest szeroko stosowana w frameworkach takich jak jQuery i React, a także w czystym JavaScript do obsługi list, tabel i menu, gdzie liczba elementów może być duża lub zmienna w czasie.

38Delegacja Zdarzeń: Efektywność i Dynamika

Delegacja zdarzeń - efektywna obsługa wielu elementów

Delegacja zdarzeń to wzorzec projektowy polegający na przypisaniu jednego nasłuchiwacza zdarzeń do wspólnego przodka (rodzica) zamiast do każdego elementu z osobna. Technika ta wykorzystuje zjawisko bubbling - zdarzenie "wędruje" od elementu docelowego do góry drzewa DOM.

Zasada działania: Przypisujemy nasłuchiwacz do rodzica (np. całej listy), a następnie w handlerze sprawdzamy e.target, aby określić, który konkretnie element został kliknięty (lub inaczej zdarzony).

Korzyści z delegacji zdarzeń:

  • Wydajność: Jeden nasłuchiwacz zamiast setek lub tysięcy (ważne przy długich listach)
  • Mniej pamięci: Mniej event listenerów = mniej zużytej pamięci
  • Dynamika: Działa automatycznie dla elementów dodanych do DOM po zarejestrowaniu nasłuchiwacza
  • Łatwiejsze usuwanie: Wystarczy usunąć jeden nasłuchiwacz

Przykład: Zamiast dodawać click listener do każdego przycisku "Usuń" w liście, dodajemy jeden listener do kontenera listy i sprawdzamy e.target.

Ograniczenia: Nie wszystkie zdarzenia bubblingują (np. focus, blur, scroll). Dla nich używamy capturing lub innych technik.

const lista = document.getElementById('lista');

lista.addEventListener('click', (e) => {
    if (e.target.tagName === 'LI') {
        console.log(`Kliknięto: ${e.target.textContent}`);
    }
});

Delegacja zdarzeń to jedna z najważniejszych technik optymalizacyjnych w JavaScript, która polega na wykorzystaniu propagacji zdarzeń do obsługi wielu elementów za pomocą jednego nasłuchiwacza. Zamiast dodawać nasłuchiwacz do każdego przycisku w liście, dodajemy jeden nasłuchiwacz do całej listy i sprawdzamy e.target, aby określić, który konkretnie przycisk został kliknięty. Ta technika nie tylko oszczędza pamięć, ale także upraszcza kod i ułatwia utrzymanie, ponieważ zmiana logiki obsługi wymaga modyfikacji tylko jednego miejsca.

Główną zaletą delegacji jest automatyczna obsługa dynamicznie dodawanych elementów bez konieczności rejestrowania nowych nasłuchiwaczy. Jeśli dodamy nowy element do listy po inicjalizacji, będzie on automatycznie obsługiwany przez istniejący nasłuchiwacz na rodzicu, ponieważ zdarzenie z nowego elementu będzie bubble'ować do rodzica. W praktyce delegację zdarzeń stosuje się wszędzie tam, gdzie mamy dynamiczne listy, tabele z możliwością dodawania wierszy, menu z podpozycjami ładowanymi asynchronicznie i wszelkie interfejsy, gdzie liczba elementów jest zmienna w czasie.

39Asynchroniczność: Dlaczego jest potrzebna?

Asynchroniczność w JavaScript - dlaczego jest niezbędna

JavaScript jest językiem jednowątkowym (single-threaded), co oznacza, że w danym momencie może wykonywać tylko jedną operację. Gdybyśmy czekali na zakończenie długiej operacji (np. pobieranie danych z serwera), cała strona "zawiesiłaby się" i nie byłaby responsywna.

Operacje asynchroniczne pozwalają na "zlecenie" czasochłonnych zadań (np. żądania sieciowe, odczytywanie plików) i kontynuowanie wykonywania innego kodu w oczekiwaniu na ich wynik. Gdy operacja się zakończy, JavaScript automatycznie wywoła odpowiednią funkcję z wynikiem.

Mechanizmy asynchroniczności w JavaScript:

  • Callbacks (funkcje zwrotne) - najstarszy mechanizm, funkcja przekazywana jako argument
  • Promises (Obietnice) - nowoczesny mechanizm z lepszą obsługą błędów i łańcuchowaniem
  • async/await - "syntactic sugar" nad Promises, pozwalający pisać kod asynchroniczny jak synchroniczny
  • setTimeout/setInterval - funkcje do opóźniania lub powtarzania kodu

Zrozumienie asynchroniczności jest kluczowe dla każdego front-end developera, ponieważ większość operacji w nowoczesnych aplikacjach (komunikacja z API, animacje, interakcje) jest asynchroniczna.

// Przykładowa asynchroniczna funkcja
setTimeout(() => {
    console.log('Po 2 sekundach');
}, 2000); 

console.log('Wykonuje się natychmiast');

Asynchroniczność w JavaScript jest konieczna ze względu na jednowątkową naturę języka, gdzie główny wątek wykonuje kod, renderuje stronę i obsługuje zdarzenia użytkownika. Gdyby długotrwałe operacje, takie jak pobieranie danych z serwera czy odczyt dużych plików, były wykonywane synchronicznie, cała strona zamarłaby na czas ich trwania, co drastycznie pogorszyłoby doświadczenie użytkownika. JavaScript rozwiązuje ten problem za pomocą pętli zdarzeń event loop, która umożliwia wykonywanie kodu asynchronicznego bez blokowania głównego wątku.

Zrozumienie pętli zdarzeń jest kluczowe dla pisania wydajnego i przewidywalnego kodu JavaScript. Pętla zdarzeń cyklicznie sprawdza stos wywołań, kolejki mikro-tasków i makro-tasków, wykonując zadania w odpowiedniej kolejności. Promise i MutationObserver są przetwarzane w kolejce mikro-tasków, która ma wyższy priorytet niż kolejka makro-tasków zawierająca setTimeout, setInterval i zdarzenia DOM. Ta hierarchia ma praktyczne implikacje dla kolejności wykonywania kodu i może prowadzić do subtelnych błędów, jeśli nie jest w pełni zrozumiana.

40Funkcje Zwrotne (Callbacks) i Callback Hell

Callbacks - najstarszy wzorzec asynchroniczności

Callback (funkcja zwrotna) to funkcja przekazywana jako argument do innej funkcji, która wywoła ją (wykona callback) po zakończeniu pewnej operacji lub wystąpieniu określonego zdarzenia. Był to historycznie pierwszy i przez długi czas jedyny mechanizm obsługi kodu asynchronicznego w JavaScript.

Przykład użycia: Funkcja setTimeout(callback, czas) przyjmuje callback i wywołuje go po określonym czasie. Funkcja addEventListener(event, callback) wywołuje callback, gdy wystąpi określone zdarzenie.

Problemy z callbackami:

  • Callback Hell (Piekło Callbacków): Gdy mamy wiele zależnych operacji asynchronicznych, callbacki zagnieżdżają się coraz głębiej, tworząc nieczytelny, trudny w utrzymaniu kod
  • Obsługa błędów: Trudno spójnie obsłużyć błędy w wielu poziomach zagnieżdżenia
  • Inwersja kontroli: Przekazujemy kontrolę nad wykonaniem naszego kodu do innej funkcji

Ze względu na te problemy, nowoczesny JavaScript preferuje używanie Promises i async/await zamiast callbacków, choć callbacki są wciąż używane w niektórych API i bibliotekach.

pobierzUzytkownika(id, (user) => {
    pobierzPosty(user.id, (posts) => {
        // ... więcej zagnieżdżenia
        wyswietl(posts);
    });
});

Funkcje zwrotne callbacki były pierwszym mechanizmem obsługi asynchroniczności w JavaScript i wciąż są używane w niektórych API, takich jak addEventListener, setTimeout czy fs.readFile w Node.js. Podstawowa idea jest prosta: przekazujemy funkcję jako argument do innej funkcji, która wywołuje ją po zakończeniu operacji asynchronicznej. Problemy pojawiają się przy zagnieżdżaniu wielu zależnych operacji asynchronicznych, co prowadzi do tzw. piekła callbacków callback hell, gdzie kod staje się głęboko zagnieżdżony i praktycznie nieczytelny.

Piekło callbacków objawia się jako trójkątny kształt kodu, gdzie każdy kolejny callback jest wcięty głębiej, a obsługa błędów jest rozproszona i niespójna. Dodatkowo przekazywanie kontroli nad wykonaniem kodu do innej funkcji, zwane inwersją kontroli, utrudnia debugowanie i testowanie. Rozwiązaniem tych problemów są Promise, które oferują płaską strukturę łańcuchową, scentralizowaną obsługę błędów przez catch i możliwość komponowania niezależnych operacji asynchronicznych za pomocą Promise.all i Promise.race.

41Promises (Obietnice): Stany i Metody (.then, .catch)

Promises (Obietnice) - nowoczesna asynchroniczność

Promise (Obietnica) to obiekt reprezentujący przyszłe zakończenie (lub niepowodzenie) operacji asynchronicznej oraz jej wynik. Promises zostały wprowadzone w ES6 jako rozwiązanie problemów z callbackami.

Trzy stany Promises:

  • Pending (Oczekujące): Stan początkowy - operacja jest w toku, wynik nie jest jeszcze znany
  • Fulfilled (Spełnione): Operacja zakończyła się sukcesem, wynik jest dostępny
  • Rejected (Odrzucone): Operacja zakończyła się błędem, przyczyna błędu jest dostępna

Tworzenie Promise: Obiekt Promise przyjmuje funkcję executor z dwoma parametrami: resolve (funkcja do wywołania w przypadku sukcesu) i reject (funkcja do wywołania w przypadku błędu).

Metody obsługi:

  • .then(onFulfilled, onRejected) - wywołuje odpowiednią funkcję w zależności od stanu
  • .catch(onRejected) - obsługuje błędy (odpowiednik .then(null, onRejected))
  • .finally(onFinally) - wywoływany zawsze, niezależnie od wyniku
const promise = new Promise((resolve, reject) => {
    // resolve w przypadku sukcesu, reject w przypadku błędu
    if (true) resolve('Dane gotowe');
    else reject('Błąd');
});

promise.then(data => {
    console.log(data); // Sukces
}).catch(error => {
    console.error(error); // Błąd
});

Promise reprezentują wartość, która może być dostępna teraz, w przyszłości lub nigdy, oferując eleganckie API do obsługi operacji asynchronicznych. Trzy stany Promise: pending, fulfilled i rejected, precyzyjnie opisują cykl życia operacji asynchronicznej, a metody then, catch i finally umożliwiają czyste i przewidywalne reagowanie na każdy z tych stanów. Promise są domyślnie eagerly evaluated, co oznacza, że rozpoczynają wykonywanie natychmiast po utworzeniu, a nie po wywołaniu then.

Łańcuchowanie Promise za pomocą then pozwala na sekwencyjne wykonywanie operacji asynchronicznych w czytelny i liniowy sposób, gdzie każdy then otrzymuje wynik poprzedniego i może zwrócić nową wartość lub Promise. Metoda catch na końcu łańcucha przechwytuje każdy błąd, który wystąpił w dowolnym miejscu łańcucha, co eliminuje potrzebę obsługi błędów w każdym then z osobna. Promise.all umożliwia równoległe wykonanie wielu niezależnych operacji i poczekanie na wszystkie wyniki, podczas gdy Promise.race zwraca wynik pierwszej zakończonej operacji, co jest przydatne przy implementacji timeoutów.

42Łańcuchowanie Obietnic (Promise Chaining)

Łańcuchowanie Promise (Promise Chaining)

Jedną z największych zalet Promises jest możliwość ich łańcuchowego wywoływania. Metoda .then() zawsze zwraca nową Obietnicę (nawet jeśli nie zwrócisz jej jawnie, JavaScript automatycznie "opakowuje" wartość w Promise), co pozwala na sekwencyjne wykonywanie operacji asynchronicznych w czysty, liniowy sposób.

Zasada działania: Wynik poprzedniego .then() staje się argumentem następnego. Jeśli funkcja w .then() zwróci wartość, następny .then() otrzyma tę wartość. Jeśli zwróci Promise, następny .then() poczeka na jej rozwiązanie.

Przykład praktyczny: Pobranie użytkownika z API, a następnie użycie jego ID do pobrania listy postów. Dzięki łańcuchowaniu kod jest czytelny i łatwy w utrzymaniu.

Obsługa błędów: W łańcuchu Promise, błąd w dowolnym miejscu przechodzi do najbliższego .catch() poniżej. Jeśli .catch() zwróci wartość lub Promise, łańcuch jest kontynuowany normalnie.

Promise.all() - statyczna metoda pozwalająca na równoległe wykonywanie wielu Promise i poczekanie na wszystkie wyniki.

fetch('/users/1')
    .then(res => res.json()) // 1. Zwraca Promise
    .then(user => fetch(`/posts/${user.id}`)) // 2. Zwraca Promise
    .then(res => res.json())
    .then(posts => {
        console.log(posts); // Dane po wszystkich krokach
    });

Łańcuchowanie Promise jest jedną z najważniejszych zalet tego mechanizmu, umożliwiającą budowanie czytelnych sekwencji operacji asynchronicznych bez zagnieżdżania callbacków. Każde wywołanie then zwraca nowego Promise, który rozwiązuje się z wartością zwróconą przez funkcję callback, co pozwala na naturalne przekazywanie danych między kolejnymi krokami. Jeśli funkcja callback zwróci Promise, następny then poczeka na jego rozwiązanie, co umożliwia płynne łączenie niezależnych operacji asynchronicznych.

Promise.all jest niezbędnym narzędziem przy wykonywaniu wielu niezależnych operacji równolegle, takich jak pobieranie danych z wielu endpointów API jednocześnie, co znacząco przyspiesza działanie aplikacji w porównaniu do sekwencyjnego wykonywania. Promise.allSettled różni się od Promise.all tym, że nie przerywa działania przy pierwszym błędzie, ale czeka na zakończenie wszystkich Promise i zwraca ich statusy, co jest przydatne, gdy chcemy poznać wyniki wszystkich operacji niezależnie od błędów. Promise.any zwraca pierwszy spełniony Promise, ignorując odrzucone, co jest przydatne przy implementacji fallbacków.

43Fetch API: Pobieranie Danych (GET Request)

Fetch API - nowoczesny standard komunikacji HTTP

Fetch API to wbudowany w przeglądarki interfejs do wykonywania żądań HTTP. Zastąpił starsze XMLHttpRequest i jest obecnie standardowym sposobem komunikacji z API w JavaScript. Fetch zwraca Promise, co idealnie integruje się z nowoczesnym, asynchronicznym kodem.

Podstawowe użycie: fetch(url) wykonuje żądanie GET pod podany adres URL i zwraca Promise, który rozwiązuje się z obiektem Response.

Dwuetapowe przetwarzanie: Obiekt Response zawiera informacje o odpowiedzi HTTP (status, nagłówki), ale nie zawiera jeszcze danych. Aby otrzymać dane, musisz wywołać odpowiednią metodę na Response:

  • .json() - parsuj dane jako JSON (zwraca Promise)
  • .text() - pobierz jako surowy tekst
  • .blob() - pobierz jako dane binarne (np. obrazki)
  • .formData() - parsuj jako dane formularza

Walidacja odpowiedzi: Metoda fetch() nie zrzuca błędu przy statusach 404 czy 500 - Promise zawsze się "spełnia". Dlatego zawsze sprawdzaj res.ok lub status w kodzie.

fetch('https://api.example.com/data')
    .then(res => {
        if (!res.ok) throw new Error('Status: ' + res.status);
        return res.json();
    })
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.error(err);
    });

Fetch API zastąpił starsze XMLHttpRequest jako standardowy sposób wykonywania żądań HTTP w przeglądarkach, oferując prostsze API oparte na Promise. Podstawowe wywołanie fetch z samym URL-em wykonuje żądanie GET i zwraca Promise, który rozwiązuje się z obiektem Response. Obiekt Response zawiera informacje o statusie odpowiedzi, nagłówkach i metodach do odczytu ciała odpowiedzi w różnych formatach, takich jak json, text, blob i formData.

Obsługa błędów w Fetch API wymaga uwagi, ponieważ Promise jest odrzucany tylko w przypadku błędów sieciowych, a nie dla statusów HTTP 4xx i 5xx. Dlatego konieczne jest ręczne sprawdzanie właściwości ok lub status na obiekcie Response i rzucanie wyjątku w przypadku niepowodzenia. W praktyce często tworzy się funkcję pomocniczą wrapFetch, która automatyzuje walidację odpowiedzi i parsowanie JSON, co eliminuje powtarzalny kod i zapewnia spójną obsługę błędów w całej aplikacji.

44Fetch API: Wysyłanie Danych (POST Request)

Wysyłanie danych za pomocą Fetch API - metoda POST

Żądanie POST służy do wysyłania danych do serwera - tworzenia nowych zasobów, logowania użytkowników, wysyłania formularzy. W Fetch API drugi argument (obiekt opcji) pozwala na pełną konfigurację żądania.

Obowiązkowe opcje dla POST:

  • method: 'POST' - określa metodę HTTP (może być też PUT, PATCH, DELETE)
  • headers - obiekt z nagłówkami żądania. Kluczowy nagłówek to 'Content-Type' informujący serwer o formacie danych (np. 'application/json' dla JSON)
  • body - dane do wysłania. Muszą być stringiem - dla JSON używamy JSON.stringify(obiekt)

Dane formularza: Zamiast JSON można też wysyłać dane formularza w formacie multipart/form-data lub application/x-www-form-urlencoded używając obiektu FormData.

Bezpieczeństwo: Nigdy nie wysyłaj wrażliwych danych (hasła, tokeny) w URL (metoda GET). Zawsze używaj POST i HTTPS. Pamiętaj o walidacji danych zarówno po stronie klienta, jak i serwera.

const dane = { tytul: 'Nowy post' };

fetch('/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(dane) // Konwersja obiektu na JSON
})
.then(res => res.json())
.then(nowyPost => console.log(nowyPost));

Wysyłanie danych za pomocą metody POST w Fetch API wymaga podania drugiego argumentu z obiektem opcji, który zawiera metodę, nagłówki i ciało żądania. Nagłówek Content-Type jest kluczowy dla serwera, ponieważ informuje go o formacie przesyłanych danych: application/json dla JSON, multipart/form-data dla plików i application/x-www-form-urlencoded dla tradycyjnych formularzy. Ciało żądania musi być stringiem dla JSON, co wymaga użycia JSON.stringify do konwersji obiektu JavaScript.

Wysyłanie formularzy z plikami wymaga użycia obiektu FormData, który automatycznie ustawia nagłówek Content-Type na multipart/form-data z odpowiednim boundary. Fetch API obsługuje także inne metody HTTP, takie jak PUT do aktualizacji zasobów, PATCH do częściowej aktualizacji i DELETE do usuwania, co czyni go kompletnym narzędziem do budowania klientów REST API. W praktycznych aplikacjach często tworzy się abstrakcje nad Fetch API, które dodają obsługę tokenów autoryzacyjnych, automatyczne odświeżanie tokenów i centralną obsługę błędów.

45Async/Await: Syntactic Sugar dla Promises

Async/Await - synchroniczna składnia dla asynchroniczności

Async/await to składnia wprowadzona w ES2017 (ES8), która stanowi "syntactic sugar" (uproszczenie składniowe) nad Promises. Pozwala ona pisać kod asynchroniczny w sposób wyglądający i zachowujący się jak synchroniczny kod, co znacząco poprawia czytelność i łatwość utrzymania.

Słowo kluczowe async: Umieszczone przed deklaracją funkcji (async function lub async () =>) oznacza, że funkcja zawsze zwraca Promise. Jeśli jawnie zwrócisz wartość, zostanie ona automatycznie "opakowana" w Promise.

Słowo kluczowe await: Może być użyte tylko wewnątrz funkcji async. Powoduje ono "pauzę" wykonywania funkcji do czasu rozwiązania Promise stojącego po prawej stronie. Kod "czeka" na wynik, ale nie blokuje głównego wątku JavaScript - inne operacje mogą się wykonywać w tle.

Obsługa błędów: Służy blok try...catch...finally - znany z kodu synchronicznego. Błędy z dowolnego await w bloku try zostaną złapane przez catch.

Zalety: Kod jest czytelniejszy, łatwiejszy w debugowaniu, eliminuje "callback hell" i łańcuchy .then().

async function pobierzUsera(id) {
    try {
        const res = await fetch(`/users/${id}`);
        if (!res.ok) throw new Error('Błąd HTTP');
        const user = await res.json();
        return user;
    } catch (err) {
        console.error("Błąd fetch:", err);
    }
}

Async i await, wprowadzone w ES2017, stanowią przełom w pisaniu kodu asynchronicznego, umożliwiając pisanie operacji asynchronicznych w sposób wyglądający jak zwykły, synchroniczny kod. Słowo kluczowe async przed deklaracją funkcji sprawia, że funkcja zawsze zwraca Promise, nawet jeśli jawnie zwrócimy wartość prostą, która zostanie automatycznie opakowana. Słowo kluczowe await wstrzymuje wykonywanie funkcji asynchronicznej do momentu rozwiązania Promise, nie blokując przy tym głównego wątku JavaScript.

Obsługa błędów w async/await odbywa się za pomocą tradycyjnego try...catch, co jest znacznie bardziej czytelne niż łańcuchy then z catch na końcu. W praktyce async/await wyeliminowało większość przypadków użycia surowych Promise w kodzie produkcyjnym, prowadząc do bardziej liniowego i zrozumiałego kodu. Należy jednak pamiętać, że await można używać tylko wewnątrz funkcji async, a zapomnienie o tym skutkuje błędem składniowym, oraz że równoległe wykonywanie niezależnych operacji wymaga użycia Promise.all zamiast sekwencyjnego await.

46Lokalny Magazyn Danych: localStorage

localStorage - trwałe przechowywanie danych w przeglądarce

localStorage to wbudowany w przeglądarki interfejs Web Storage API, który umożliwia trwałe przechowywanie danych po stronie klienta (w przeglądarce użytkownika). Dane zapisane w localStorage pozostają tam dopóty, dopóki użytkownik ich nie usunie (np. poprzez wyczyszczenie danych strony w ustawieniach przeglądarki) lub programowo.

Charakterystyka localStorage:

  • Pojemność: około 5-10 MB (znacznie więcej niż cookies)
  • Dane przechowywane jako stringi (klucz-wartość)
  • Dane są dostępne między sesjami (nie wygasają)
  • Dane są domenowo izolowane - każda strona ma dostęp tylko do swoich danych
  • Dane nie są wysyłane z każdym żądaniem do serwera (w przeciwieństwie do cookies)

Typowe zastosowania: Zapamiętywanie preferencji użytkownika (motyw ciemny/jasny), stan aplikacji, zawartość koszyka, tymczasowe dane formularza, cache'owanie danych.

Alternatywy: sessionStorage (dane tylko dla jednej sesji), IndexedDB (bazodanowe rozwiązanie dla dużych ilości danych), Cookies (dane wysyłane do serwera).

// Zapisz (obiekt trzeba stringify)
localStorage.setItem('motyw', 'ciemny');

// Odczytaj
const motyw = localStorage.getItem('motyw');
console.log(motyw);

// Usuń
localStorage.removeItem('motyw');

localStorage to podstawowy mechanizm przechowywania danych po stronie klienta w przeglądarce, oferujący pojemność około 5-10 MB na domenę. Dane w localStorage są przechowywane jako pary klucz-wartość, gdzie zarówno klucze, jak i wartości muszą być stringami, co wymaga użycia JSON.stringify przy zapisie obiektów i JSON.parse przy odczycie. localStorage jest synchroniczny i blokujący, co oznacza, że operacje odczytu i zapisu są wykonywane natychmiast na głównym wątku, ale w praktyce nie stanowi to problemu ze względu na małe rozmiary danych.

Dane w localStorage są trwałe i pozostają dostępne między sesjami przeglądarki, co odróżnia go od sessionStorage, który jest czyszczony po zamknięciu karty. W przeciwieństwie do cookies, dane z localStorage nie są automatycznie wysyłane z każdym żądaniem HTTP, co zmniejsza ilość przesyłanych danych i poprawia wydajność. Typowe zastosowania localStorage obejmują zapamiętywanie preferencji użytkownika, takich jak motyw kolorystyczny, dane koszyka zakupowego w sklepach internetowych oraz cache'owanie odpowiedzi API dla poprawy wydajności aplikacji offline.

47JSON: Serializacja i Deserializacja

JSON - uniwersalny format wymiany danych

JSON (JavaScript Object Notation) to lekki, tekstowy format wymiany danych, który jest niezależny od języka programowania. Mimo nazwy, JSON jest formatem uniwersalnym i jest używany praktycznie wszędzie - w API webowych, plikach konfiguracyjnych, bazach danych.

Składnia JSON: JSON obsługuje podstawowe typy danych: stringi (w cudzysłowach), liczby, wartości boolean, null, tablice (w nawiasach kwadratowych) i obiekty (w nawiasach klamrowych). Klucze muszą być stringami (w cudzysłowach).

Serializacja (JSON.stringify): Konwertuje obiekt JavaScript na string JSON. Jest to niezbędne przy: wysyłaniu danych przez Fetch API, zapisywaniu obiektów w localStorage, przesyłaniu danych między serwerami.

Deserializacja (JSON.parse): Konwertuje string JSON z powrotem na obiekt JavaScript. Konieczne przy: odczytywaniu danych z localStorage, parsowaniu odpowiedzi API.

Uwaga: JSON.parse na nieprawidłowym JSON rzuci błąd SyntaxError - zawsze obsługuj to w try...catch!

const userObj = { id: 1, name: 'Adam' };

// Serializacja
const userJSON = JSON.stringify(userObj); 

// Zapisz string
localStorage.setItem('user', userJSON);

// Deserializacja
const user = JSON.parse(localStorage.getItem('user'));
console.log(user.name);

JSON stał się de facto standardem wymiany danych w web developmentie, wypierając XML ze względu na swoją lekkość, czytelność i natywne wsparcie w JavaScript. Serializacja za pomocą JSON.stringify konwertuje obiekt JavaScript na string JSON, automatycznie pomijając właściwości o wartości undefined i funkcje, które nie są prawidłowymi wartościami JSON. Deserializacja za pomocą JSON.parse przekształca string JSON z powrotem w obiekt JavaScript, rzucając SyntaxError w przypadku nieprawidłowego formatu, co należy obsłużyć w try...catch.

Drugi parametr JSON.stringify może być funkcją transformującą lub tablicą dozwolonych właściwości, co pozwala na kontrolowanie, które właściwości obiektu zostaną zserializowane. JSON.parse akceptuje drugi parametr w postaci funkcji reviver, która może transformować wartości podczas deserializacji, na przykład konwertować stringi ISO dat na obiekty Date. W praktyce JSON jest używany nie tylko w komunikacji z API, ale także do głębokiego kopiowania obiektów za pomocą JSON.parse(JSON.stringify(obj)), choć ta metoda ma ograniczenia związane z pomijaniem funkcji i symboli.

48Timery: setTimeout() i setInterval()

Timery - opóźnianie i powtarzanie wykonania kodu

JavaScript udostępnia funkcje do planowania wykonania kodu w przyszłości: jednorazowego opóźnienia (setTimeout) lub powtarzania (setInterval). Są to fundamentalne narzędzia do tworzenia animacji, odświeżania danych, obsługi czasu i wielu innych zastosowań.

setTimeout(func, delay, arg1, arg2, ...):

  • Planuje wykonanie funkcji func po upływie delay milisekund
  • Zwraca ID timera, którego można użyć do anulowania w clearTimeout()
  • Dodatkowe argumenty (arg1, arg2, ...) są przekazywane do funkcji
  • Czas jest minimalny - przeglądarka nie gwarantuje dokładności

setInterval(func, delay, arg1, arg2, ...):

  • Planuje cykliczne wykonywanie funkcji co delay milisekund
  • Zwraca ID interwału do anulowania w clearInterval()
  • Wykonuje się dopóki nie zostanie jawnie zatrzymany

Zatrzymywanie timerów: Zawsze używaj clearTimeout(id) lub clearInterval(id), gdy timer nie jest już potrzebny. W przeciwnym razie może dojść do wycieków pamięci i niepożądanego wykonania kodu.

requestAnimationFrame: Dla animacji preferuj requestAnimationFrame zamiast setInterval - jest zoptymalizowany pod kątem płynności i wydajności.

const timerID = setTimeout(() => {
    console.log('Jednorazowe opóźnienie');
}, 1500);

// Anulowanie timera przed wykonaniem
clearTimeout(timerID);

// Interwał
let intervalID = setInterval(() => {
    console.log('Co sekundę...');
}, 1000);
clearInterval(intervalID);

Timery setTimeout i setInterval są podstawowymi narzędziami do planowania wykonania kodu w przyszłości, wykorzystywanymi w animacjach, odświeżaniu danych, debouncingu i wielu innych zastosowaniach. setTimeout wykonuje funkcję jednorazowo po określonym opóźnieniu, a zwracane ID timera umożliwia anulowanie za pomocą clearTimeout przed wykonaniem. Rzeczywisty czas wykonania może być dłuższy niż podane opóźnienie ze względu na działanie pętli zdarzeń i inne operacje w kolejce.

setInterval wykonuje funkcję cyklicznie co określony interwał, ale należy go używać ostrożnie, ponieważ może prowadzić do problemów z wydajnością, jeśli funkcja wykonuje się dłużej niż interwał. W praktyce często preferuje się rekurencyjny setTimeout zamiast setInterval, ponieważ gwarantuje stałe opóźnienie między zakończeniem poprzedniego wykonania a rozpoczęciem następnego. Dla animacji zaleca się używanie requestAnimationFrame zamiast timerów, ponieważ jest zsynchronizowany z cyklem odświeżania monitora i automatycznie pauzuje, gdy karta jest nieaktywna, co oszczędza zasoby systemowe.

49Geolocation API: Pamiętaj o Zgodzie Użytkownika

Geolocation API - dostęp do lokalizacji użytkownika

Geolocation API to interfejs przeglądarki umożliwiający aplikacjom internetowym dostęp do informacji o położeniu geograficznym urządzenia użytkownika. Jest to niezwykle przydatne API przy tworzeniu aplikacji związanych z lokalizacją - map, nawigacji, serwisów informacyjnych, systemów rekomendacji opartych na lokalizacji.

Wymóg zgody użytkownika: Ze względów bezpieczeństwa i prywatności, użytkownik musi wyraźnie wyrazić zgodę na udostępnienie lokalizacji. Przeglądarka wyświetli stosowny komunikat z prośbą o zgodę. Użytkownik może odmówić lub wycofać zgodę w każdej chwili.

Metody Geolocation API:

  • getCurrentPosition(callback) - pobiera aktualną pozycję jeden raz
  • watchPosition(callback) - ciągle monitoruje pozycję i wywołuje callback przy każdej zmianie
  • clearWatch(id) - zatrzymuje monitorowanie pozycji

Obiekt Position: Zawiera współrzędne (latitude, longitude, altitude), dokładność (accuracy), prędkość, kierunek i znacznik czasu.

Obsługa błędów: Drugi parametr callback obsługuje błędy (PERMISSION_DENIED, POSITION_UNAVAILABLE, TIMEOUT).

if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(
        (position) => {
            console.log('Lat:', position.coords.latitude);
            console.log('Lon:', position.coords.longitude);
        },
        (error) => {
            console.error('Błąd Geolokacji:', error.code);
        }
    );
} else {
    console.log("Geolokalizacja nie jest wspierana.");
}

Geolocation API umożliwia aplikacjom webowym dostęp do informacji o położeniu geograficznym urządzenia użytkownika, co otwiera drzwi do tworzenia aplikacji związanych z lokalizacją, takich jak mapy, nawigacja, serwisy rekomendacji miejsc i systemy śledzenia przesyłek. Ze względów bezpieczeństwa przeglądarka zawsze pyta użytkownika o zgodę przed udostępnieniem lokalizacji, a użytkownik może odmówić lub wycofać zgodę w dowolnym momencie. Aplikacje powinny być zaprojektowane tak, aby działały również bez dostępu do lokalizacji, oferując alternatywne metody wprowadzania danych.

Metoda getCurrentPosition pobiera aktualną pozycję jednorazowo, podczas gdy watchPosition umożliwia ciągłe monitorowanie zmian położenia, co jest przydatne w aplikacjach nawigacyjnych. Obiekt pozycji zawiera współrzędne geograficzne z dokładnością wyrażoną w metrach, a także opcjonalne dane takie jak wysokość, prędkość i kierunek. W praktyce Geolocation API jest często używane w połączeniu z bibliotekami mapowymi takimi jak Leaflet lub Google Maps API do wizualizacji pozycji na mapie, co wymaga dodatkowego ładowania skryptów i kluczy API.

50Podsumowanie: Ścieżka Rozwoju (Frontend)

Ścieżka rozwoju front-end developera

Ukończenie tego kursu stanowi solidną podstawę do rozpoczęcia pracy jako front-end developer. HTML, CSS i JavaScript to fundamenty, na których opiera się cały współczesny web development. Jednak aby stać się profesjonalistą, potrzebujesz ciągłego rozwoju i nauki kolejnych technologii.

Polecane kierunki dalszego rozwoju:

  1. Zaawansowany CSS: Flexbox i Grid Layout do tworzenia responsywnych układów, animacje CSS, preprocesory (SASS/SCSS), metodologie (BEM, SMACSS)
  2. Narzędzia deweloperskie: Bundlery (Vite, Webpack, Parcel), lintery i formattery (ESLint, Prettier), systemy kontroli wersji (Git)
  3. Frameworki i biblioteki: React, Vue.js lub Angular do tworzenia interaktywnych aplikacji jednostronicowych (SPA)
  4. TypeScript: Typowany nadzbiór JavaScriptu, który znacząco poprawia jakość i bezpieczeństwo kodu
  5. Testowanie: Testy jednostkowe (Jest, Vitest), testy komponentów (React Testing Library), testy E2E (Cypress, Playwright)
  6. Backend basics: Podstawy Node.js, Express, bazy danych - zrozumienie pełnego stosu technologicznego

Rada praktyczna: Najlepszą metodą nauki jest praktyka. Buduj projekty, eksperymentuj, ucz się na błędach i nie bój się wyzwań!

HTML, CSS, JS to dopiero początek! Kontynuuj budowanie i eksperymentowanie, aby opanować nowoczesny web development.

Ścieżka rozwoju front-end developera nie kończy się na opanowaniu podstaw HTML, CSS i JavaScript, ale wymaga ciągłego uczenia się i śledzenia zmian w dynamicznym ekosystemie web developmentu. Po opanowaniu podstaw warto skupić się na zaawansowanym CSS, w tym Flexbox i Grid Layout, które umożliwiają tworzenie złożonych, responsywnych układów bez uciekania się do frameworków. Animacje CSS z użyciem właściwości transition i animation pozwalają na tworzenie płynnych interakcji bez obciążania głównego wątku JavaScript.

Wybór frameworka front-endowego, takiego jak React, Vue czy Angular, jest naturalnym kolejnym krokiem, który znacząco przyspiesza tworzenie zaawansowanych aplikacji. TypeScript, jako typowany nadzbiór JavaScript, staje się standardem w profesjonalnych projektach, oferując lepsze wsparcie narzędziowe, autouzupełnianie i wykrywanie błędów na etapie kompilacji. Testowanie z użyciem Jest, React Testing Library i Playwright zapewnia jakość i stabilność kodu, a znajomość podstaw Node.js i Express pozwala na zrozumienie pełnego stosu technologicznego i efektywniejszą współpracę z backend developerami.