JavaScript 19.11

Klasy

Konstruktor a klasa - klasy zostały dodane późno, w 2015, ale nie różnią się zbytnio od konstruktora. Dlaczego potrzeba klas? możemy pisać wszystko za pomocą prototypów, ale używając klas to będzie czytelniejsze. W klasach jest funkcja kontruktor, ktora pomaga tworzyć wiele obiektów. W klasie już nie mówimy o prototypie, tylko o konstruktorze i o instancjach klasy.

class + nazwa klasy, nazwa w PascalCase

constructor(name, email){ this.name=name; this.email=email; this.createdAt=new Date();} metoda wywoływana automatycznie przy tworzeniu nowej instancji

this jest w kontekście każdego powołanego obiektu czyli każdej nowej instancji

metody klasy definiujemy bez słowa kluczowego function. każda instancja ma dostęp do metod które zostały zdefiniowane.

właściwości instancji które tworzymy - tworzymy w konstruktorze przypisując poprzez this.name

KLASY NIE SĄ HOISTOWANE, MUSZĄ BYĆ ZDEFINIOWANE INACZEJ BĘDZIE BŁĄD

Jeśli tworzymy dużo obiektów to w konstruktor jesteśmy w stanie dopisać wartości domyślne.

Ścisłe porównanie obiektów da true bo te obiekty są utworzone jako klasa i dzielą właściwości ale nie dzielą wartości.

 // DEFINICJA KLASY
        // Słowo kluczowe 'class' rozpoczyna definicję
        // Nazwa klasy konwencjonalnie w PascalCase
        class User {
            // KONSTRUKTOR - wywoływany automatycznie przy tworzeniu instancji
            // Służy do inicjalizacji właściwości obiektu
            constructor(name, email) {
                // 'this' odnosi się do tworzonej instancji
                // Przypisujemy parametry do właściwości instancji
                this.name = name;
                this.email = email;
                this.createdAt = new Date(); // Możemy też przypisać wartości domyślne
                this.isActive = false
            }

            // METODA INSTANCJI
            // Nie używamy słowa kluczowego 'function'
            // Dostępna dla każdej instancji klasy
            greet() {
                // Metoda ma dostęp do 'this' i właściwości instancji
                return `Cześć, jestem ${this.name}`;
            }

            // Kolejna metoda instancji
            getInfo() {
                return `${this.name} (${this.email})`;
            }
        }

        // TWORZENIE INSTANCJI
        // Operator 'new' tworzy nowy obiekt i wywołuje konstruktor
        const user1 = new User("Jan", "[email protected]");
        const user2 = new User("Anna", "[email protected]");

        // UŻYWANIE INSTANCJI
        const output = document.getElementById('output');
        output.innerHTML = `
            <h3>Instancja 1:</h3>
            <p>${user1.greet()}</p>
            <p>${user1.getInfo()}</p>
            
            <h3>Instancja 2:</h3>
            <p>${user2.greet()}</p>
            <p>${user2.getInfo()}</p>
            
            <h3>Ważne:</h3>
            <p>Każda instancja ma własne wartości właściwości</p>
            <p>Ale wszystkie instancje współdzielą te same metody (efektywność pamięci)</p>
        `;

        // Sprawdzenie że metody są współdzielone (z prototypu)
        console.log('user1.greet === user2.greet:', user1.greet === user2.greet); // true!

Metody statyczne

metoda publiczna można wywołać w dowolny sposób

metoda statyczna nie można wywołać z zewnątrz, nie ma dostępu do this, metody statyczne są robione na klasie nie na obiekcie, konstrukt niezależny od instancji obiektu np. fabryka obiektów - gotowy obiekt z przypisanymi wartościami.

Możemy w ten sam sposób tworzyć pola.

class Product {
            // Konstruktor inicjalizuje właściwości instancji
            constructor(name, price) {
                this.name = name;
                this.price = price;
            }

            // METODA INSTANCJI
            // Działa na konkretnym obiekcie, ma dostęp do 'this'
            // Wywołujemy: obiekt.metodaInstancji()
            getFormattedPrice() {
                return `${this.price.toFixed(2)} PLN`;
            }

            // Kolejna metoda instancji
            applyDiscount(percentage) {
                // Modyfikuje właściwości tej konkretnej instancji
                this.price = this.price * (1 - percentage / 100);
                return this; // Zwracamy 'this' dla method chaining
            }

            // METODA STATYCZNA
            // Należy do klasy, nie do instancji
            // Nie ma dostępu do 'this' w kontekście instancji
            // Wywołujemy: NazwaKlasy.metodaStatyczna()
            static compareByPrice(product1, product2) {
                // Porównuje dwa produkty - nie potrzebuje 'this'

                // Math.random() to jest metoda statyczna wbudowana w obiekt Math

                return product1.price - product2.price;
            }

            // Metoda statyczna jako fabryka obiektów
            // Tworzy instancję z domyślnymi wartościami
            static createSampleProduct() {
                return new Product("Produkt przykładowy", 99.99);
            }

            // Metoda statyczna walidująca dane
            static isValidPrice(price) {
                return typeof price === 'number' && price > 0;
            }
        }

        // UŻYCIE METOD INSTANCJI
        const laptop = new Product("Laptop", 3500);
        const mouse = new Product("Mysz", 120);
        
        // Wywołujemy metody na instancjach
        const laptopPrice = laptop.getFormattedPrice();
        laptop.applyDiscount(10); // Metoda zwraca 'this', można chainować

        // UŻYCIE METOD STATYCZNYCH
        // Wywołujemy bezpośrednio na klasie, nie na instancji
        const products = [laptop, mouse];
        products.sort(Product.compareByPrice); // Sortowanie używa metody statycznej

        const sample = Product.createSampleProduct(); // Fabryka
        const isValid = Product.isValidPrice(100); // Walidacja

        // Wyświetlenie wyników
        const output = document.getElementById('output');
        output.innerHTML = `
            <h3>Metody instancji:</h3>
            <p>Laptop: ${laptop.name} - ${laptop.getFormattedPrice()}</p>
            <p>Mysz: ${mouse.name} - ${mouse.getFormattedPrice()}</p>
            
            <h3>Metody statyczne:</h3>
            <p>Produkt przykładowy: ${sample.name} - ${sample.getFormattedPrice()}</p>
            <p>Czy cena 100 jest poprawna? ${isValid}</p>
            <p>Produkty posortowane: ${products.map(p => p.name).join(', ')}</p>
        `;

Dziedziczenie

działa tak jak wcześniej

Można stworzyć klasę która rozszerza klasę bazową.

Class Car extends Vehicle { constructor, brand, model, doors){

super (brand, model) wywołuje konstruktor klasy bazowej, przekazujemy parametry do konstruktora Vehicle, SUPER musi byc zadeklarowane przed użyciem this, dopiero jak zadeklarujemy właściwości można uzyc this. }

// KLASA BAZOWA (nadklasa, rodzic)
        // Definiuje wspólną funkcjonalność dla wszystkich pojazdów
        class Vehicle {
            constructor(brand, model) {
                this.brand = brand;
                this.model = model;
                this.speed = 0;
            }

            // Metoda klasy bazowej
            accelerate(amount) {
                this.speed += amount;
                return `${this.brand} ${this.model} przyspiesza do ${this.speed} km/h`;
            }

            // Metoda do przesłonięcia w klasach potomnych
            getType() {
                return "Pojazd";
            }
        }

        // KLASA POTOMNA (podklasa, dziecko)
        // 'extends' wskazuje klasę bazową
        class Car extends Vehicle {
            constructor(brand, model, doors) {
                // SUPER - wywołanie konstruktora klasy bazowej
                // MUSI być przed użyciem 'this'!
                super(brand, model); // Przekazujemy parametry do konstruktora Vehicle
                
                // Dopiero teraz możemy używać 'this'
                // Dodajemy własne właściwości specyficzne dla samochodu
                this.doors = doors;
            }

            // PRZESŁONIĘCIE METODY (override)
            // Zastępujemy metodę z klasy bazowej
            getType() {
                return "Samochód";
            }

            // NOWA METODA specyficzna dla Car
            getDetails() {
                return `${this.brand} ${this.model} (${this.doors} drzwi)`;
            }
        }

        // KLASA POTOMNA drugiego poziomu
        class ElectricCar extends Car {
            constructor(brand, model, doors, batteryCapacity) {
                // Super wywołuje konstruktor Car
                super(brand, model, doors);
                this.batteryCapacity = batteryCapacity;
                this.batteryLevel = 100;
            }

            // Rozszerzamy metodę z klasy bazowej
            accelerate(amount) {
                // Wywołujemy oryginalną metodę używając super
                const result = super.accelerate(amount);
                // Dodajemy własną logikę
                this.batteryLevel -= amount * 0.1;
                return `${result} (Bateria: ${this.batteryLevel.toFixed(1)}%)`;
            }

            // Nowa metoda tylko dla ElectricCar
            charge() {
                this.batteryLevel = 100;
                return "Bateria naładowana do 100%";
            }
        }

        // TWORZENIE INSTANCJI
        const vehicle = new Vehicle("Generic", "V1");
        const car = new Car("Toyota", "Corolla", 4);
        const tesla = new ElectricCar("Tesla", "Model 3", 4, 75);

        // UŻYWANIE DZIEDZICZENIA
        const output = document.getElementById('output');
        output.innerHTML = `
            <h3>Vehicle (klasa bazowa):</h3>
            <p>${vehicle.accelerate(50)}</p>
            <p>Typ: ${vehicle.getType()}</p>
            
            <h3>Car (dziedziczy po Vehicle):</h3>
            <p>${car.accelerate(80)}</p>
            <p>Typ: ${car.getType()}</p>
            <p>Szczegóły: ${car.getDetails()}</p>
            
            <h3>ElectricCar (dziedziczy po Car):</h3>
            <p>${tesla.accelerate(100)}</p>
            <p>${tesla.charge()}</p>
            <p>Typ: ${tesla.getType()}</p>
            <p>Szczegóły: ${tesla.getDetails()}</p>
        `;

Prywatne/Publiczne pola

Aby chronić wewnętrzną implementacje klasy aby ją chronić:

enkapsulacja pola prywatne które poza klasą nie są dostępne, tworzymy je za pomocą #nazwazmiennej. Wymaga też metod prywatnych też definiowanych za pomocą #. Enkapsulacja chroni przez dostępem z zewnątrz, nadzorowany dostęp do właściwości wrażliwych jak np. pole saldo, pin. Aby modyfikować takie pole trzeba użyć metody , nie można jej bezpośrednio wywołać.

W metodach publicznych które są wewnątrz klasy możemy wywołać prywatne pole.

class BankAccount {
            // POLA PUBLICZNE - dostępne z zewnątrz
            owner;      // Deklaracja pola publicznego
            accountNumber;

            // POLA PRYWATNE - poprzedzone #
            // Niedostępne spoza klasy!
            #balance;   // Saldo - dane wrażliwe
            #pin;       // PIN - absolutnie prywatny

            constructor(owner, accountNumber, initialBalance, pin) {
                this.owner = owner;
                this.accountNumber = accountNumber;
                this.#balance = initialBalance;  // Inicjalizacja prywatnego pola
                this.#pin = pin;
            }

            // METODA PRYWATNA - tylko do użytku wewnętrznego
            #validatePin(inputPin) {
                return inputPin === this.#pin;
            }

            // PUBLICZNY GETTER - kontrolowany dostęp do odczytu
            // Nie ujawniamy bezpośrednio #balance
            getBalance(pin) {
                // Walidacja przed dostępem do danych
                if (!this.#validatePin(pin)) {
                    return "Nieprawidłowy PIN";
                }
                return `Saldo: ${this.#balance.toFixed(2)} PLN`;
            }

            // PUBLICZNA METODA z walidacją
            deposit(amount) {
                // Kontrola poprawności danych
                if (amount <= 0) {
                    return "Kwota musi być większa od 0";
                }
                // Bezpieczna modyfikacja prywatnego pola
                this.#balance += amount;
                return `Wpłacono ${amount} PLN. ${this.getBalance(this.#pin)}`;
            }


            setPin(newPin, oldPin) {
                if (!this.#validatePin(oldPin)) {
                    return "Nieprawidłowy stary PIN";
                }
                this.#pin = newPin;
                return "PIN został zmieniony pomyślnie";
            } 

            // PUBLICZNA METODA z walidacją i autoryzacją
            withdraw(amount, pin) {
                // Weryfikacja PIN
                if (!this.#validatePin(pin)) {
                    return "Nieprawidłowy PIN";
                }
                // Walidacja kwoty
                if (amount <= 0) {
                    return "Kwota musi być większa od 0";
                }
                // Sprawdzenie dostępnych środków
                if (amount > this.#balance) {
                    return "Niewystarczające środki";
                }
                // Wykonanie operacji
                this.#balance -= amount;
                return `Wypłacono ${amount} PLN. ${this.getBalance(pin)}`;
            }
        }

        // UŻYCIE ENKAPSULACJI
        const account = new BankAccount("Jan Kowalski", "123456789", 1000, "1234");


        account.pin = "0000"; // Próba zmiany prywatnego pola z zewnątrz (nie działa)
        account.setPin("0000", "1234"); // Zmiana PIN przez publiczną metodę z walidacją

        const output = document.getElementById('output');
        let results = `<h3>Konto: ${account.owner}</h3>`;
        results += `<p>Numer: ${account.accountNumber}</p>`;
        
        // Bezpieczny dostęp przez publiczne metody
        results += `<p>${account.getBalance("1234")}</p>`;
        results += `<p>${account.deposit(500)}</p>`;
        results += `<p>${account.withdraw(200, "1234")}</p>`;
        
        // Próba nieprawidłowego dostępu
        results += `<p>${account.withdraw(200, "0000")}</p>`;
        results += `<p>${account.withdraw(-100, "1234")}</p>`;
        
        results += `<h3>Próba bezpośredniego dostępu:</h3>`;
        results += `<p>account.#balance powoduje błąd składni!</p>`;
        results += `<p>Pola prywatne są rzeczywiście niedostępne z zewnątrz</p>`;
        
        output.innerHTML = results;

        // Próba dostępu do prywatnego pola (odkomentuj aby zobaczyć błąd)
        // console.log(account.#balance); // SyntaxError!

JSON JavaScript Object Notation

Metoda powszechnie stosowana do wymiany danych. Standard komunikacji aplikacji webowych. JSON mimo ze wywodzi się z JS, to stał się niezależnym formatem do wymiany danych.

// OBIEKT JAVASCRIPT DO SERIALIZACJI
        const user = {
            id: 1,
            name: "Jan Kowalski",
            email: "[email protected]",
            age: 30,
            isActive: true,
            roles: ["user", "admin"],
            address: {
                city: "Warszawa",
                country: "Polska"
            },
            createdAt: new Date("2024-01-15")
        };

        // JSON.STRINGIFY - serializacja (obiekt -> string)
        // Zamienia obiekt JavaScript w tekstową reprezentację JSON
        const jsonString = JSON.stringify(user);
        
        // WYNIK: string w formacie JSON
        // Uwaga: Data została zamieniona na string ISO
        console.log("Typ wyniku:", typeof jsonString); // "string"
        console.log("JSON string:", jsonString);

        // JSON.STRINGIFY z wcięciami dla czytelności
        // Trzeci parametr określa liczbę spacji wcięcia
        const prettyJson = JSON.stringify(user, null, 2);

        // JSON.PARSE - deserializacja (string -> obiekt)
        // Zamienia string JSON z powrotem w obiekt JavaScript
        const parsedUser = JSON.parse(jsonString);
        
        console.log("Typ wyniku:", typeof parsedUser); // "object"
        console.log("Odtworzony obiekt:", parsedUser);

        // WAŻNE: Data nie jest już obiektem Date!
        console.log("Typ createdAt:", typeof parsedUser.createdAt); // "string"
        console.log("Czy to Date?", parsedUser.createdAt instanceof Date); // false

        // PRZYPADKI SPECJALNE podczas stringify
        const specialCases = {
            normalValue: "tekst",
            numberValue: 42,
            nullValue: null,
            functionValue: function() { return "test"; },  // Zostanie zignorowana!
            undefinedValue: undefined,  // Zostanie zignorowana!
            dateValue: new Date(),  // Zamieni się na string ISO
            symbolValue: Symbol("test")  // Zostanie zignorowana!
        };

        const specialJson = JSON.stringify(specialCases);
        console.log("Special cases:", specialJson);
        // Funkcje, undefined, Symbol znikają!

        // PRAKTYCZNE ZASTOSOWANIE: localStorage
        // localStorage przechowuje tylko stringi
        // Musimy używać JSON.stringify/parse
        localStorage.setItem('user', JSON.stringify(user));
        const storedUser = JSON.parse(localStorage.getItem('user'));

        // Wyświetlenie wyników
        const output = document.getElementById('output');
        output.innerHTML = `
            <h3>Oryginalny obiekt:</h3>
            <pre>${JSON.stringify(user, null, 2)}</pre>
            
            <h3>Po JSON.stringify (string):</h3>
            <pre>${jsonString}</pre>
            
            <h3>Po JSON.parse (obiekt):</h3>
            <pre>${JSON.stringify(parsedUser, null, 2)}</pre>
            
            <h3>Uwagi:</h3>
            <p>- Data jest teraz stringiem, nie obiektem Date</p>
            <p>- Funkcje, undefined, Symbol zostały usunięte</p>
            <p>- Struktura zagnieżdżona została zachowana</p>
        `;

Serializacja - proces zamiany obiektów JS w string, w pliki tekstowe, jeśli chcemy serializować (zamienić w JSON) const jsonString = JSON.stringfy(nazwa obiektu). Wynik to nic innego jak string który zawiera wszystkie elementy obiektu z pewnymi obostrzeniami. Nie wszystkie elementy da się serializowac: date, metody obiektu, wartości undefined, symbole ani referencje cykliczne <- ignorowane lub powodują błąd. Data idzie do formatu ISO ciąg znaków.

Deserializacja - proces odwrotny do w.w, const jsonParsed = JSON.parse (nazwapliku), może wyrzucić błąd składni jeżeli nasz json jest niepoprawny. Struktura zagnieżdżona zostaje zachowana po deserialializacji.

JSONPlaceholder - Free Fake REST API

Funkcje do serializacji/deserializacji

replacer - pozwala na zmianę pewnych właściwości, może być tablicą kluczy do uwzględnienie, albo funkcją która transformuje każda wartość przed serializacją, można konwertować wartości na inne, filtrowanie danych wrażliwych jeśli nie chce by cos wpadło do JSON, konwersja formatu typów, wpisuje wartosci na "białą listę"

reviver - drugi element JSON.parse, drugi parametr, można np. za jego pomocą odtworzyć obiekt date, możemy też walidować dane, jeśli są one niepoprawne możemy przerwać lub zmienić je na jakaś domyślna wartość, pozwala przywracać typy które zostały utracone podczas deserializacji, KLUCZ I WARTOSC JAKO ARGUMENTY.

Undefined usuwa całą właściwość, jak intencjonalnie zwrócimy undefined w return

 // Obiekt z danymi użytkownika zawierający wrażliwe informacje
        const userData = {
            id: 1,
            username: "jkowalski",
            email: "[email protected]",
            password: "tajneHaslo123",  // Wrażliwe!
            creditCard: "1234-5678-9012-3456",  // Wrażliwe!
            role: "admin",
            lastLogin: new Date("2024-11-15T10:30:00"),
            profile: {
                firstName: "Jan",
                lastName: "Kowalski",
                age: 30
            }
        };

        // REPLACER JAKO TABLICA
        // Określa które właściwości uwzględnić
        const allowedFields = ['id', 'username', 'email', 'role'];
        const filteredJson = JSON.stringify(userData, allowedFields, 2);
        console.log("Filtrowany JSON:", filteredJson);

        // REPLACER JAKO FUNKCJA
        // Wywoływana dla każdej właściwości (klucz, wartość)
        function secureReplacer(key, value) {
            // Pierwszy wywołanie: key = "", value = cały obiekt
            // Kolejne wywołania: każda właściwość
            
            // Filtrowanie wrażliwych danych
            if (key === 'password' || key === 'creditCard') {
                return undefined;  // Usuń tę właściwość
            }
            
            // Transformacja daty na timestamp
            if (value instanceof Date) {
                return {
                    _type: 'Date',  // Dodajemy metadane o typie
                    value: value.toISOString()
                };
            }
            
            // Zwracamy wartość bez zmian dla pozostałych przypadków
            return value;
        }

        const secureJson = JSON.stringify(userData, secureReplacer, 2);
        console.log("Bezpieczny JSON:", secureJson);

        // REVIVER FUNCTION
        // Odtwarza typy i waliduje dane podczas parse
        function smartReviver(key, value) {
            // Odtwarzanie obiektów Date
            if (value && typeof value === 'object' && value._type === 'Date') {
                return new Date(value.value);  // Przywracamy obiekt Date
            }
            
            // Walidacja email (prosty przykład)
            if (key === 'email' && typeof value === 'string') {
                if (!value.includes('@')) {
                    console.warn(`Niepoprawny email: ${value}`);
                    return null;  // Lub rzuć błąd
                }
            }
            
            // Transformacja: wielkie litery dla role
            if (key === 'role' && typeof value === 'string') {
                return value.toUpperCase();
            }
            
            return value;
        }

        // Deserializacja z reviverem
        const restoredData = JSON.parse(secureJson, smartReviver);
        console.log("Odtworzone dane:", restoredData);
        console.log("Typ lastLogin:", restoredData.lastLogin instanceof Date); // true!

        // PRAKTYCZNY PRZYKŁAD: System logów
        class LogEntry {
            constructor(level, message, timestamp) {
                this.level = level;
                this.message = message;
                this.timestamp = timestamp || new Date();
            }

            // Własna metoda serializacji
            toJSON() {
                return {
                    _type: 'LogEntry',
                    level: this.level,
                    message: this.message,
                    timestamp: this.timestamp.toISOString()
                };
            }

            // Statyczna metoda deserializacji
            static fromJSON(data) {
                return new LogEntry(
                    data.level,
                    data.message,
                    new Date(data.timestamp)
                );
            }
        }

        const log = new LogEntry('INFO', 'Aplikacja uruchomiona');
        const logJson = JSON.stringify(log);
        
        // Reviver odtwarzający LogEntry
        const logReviver = (key, value) => {
            if (value && value._type === 'LogEntry') {
                return LogEntry.fromJSON(value);
            }
            return value;
        };

        const restoredLog = JSON.parse(logJson, logReviver);

        // Wyświetlenie wyników
        const output = document.getElementById('output');
        output.innerHTML = `
            <h3>Oryginalny obiekt (z hasłem):</h3>
            <pre>${JSON.stringify(userData, null, 2)}</pre>
            
            <h3>Po replacer (bez hasła, z metadanymi Date):</h3>
            <pre>${secureJson}</pre>
            
            <h3>Po reviver (odtworzona Date, ROLE uppercase):</h3>
            <pre>${JSON.stringify(restoredData, null, 2)}</pre>
            <p>lastLogin jest Date? ${restoredData.lastLogin instanceof Date}</p>
            
            <h3>LogEntry:</h3>
            <p>Czy odtworzony log jest instancją LogEntry? ${restoredLog instanceof LogEntry}</p>
        `;

Kiedy obiekt ma sam do siebie referencje, cykliczna referencja, referencja zwrotna JSON nie potrafi tego obsłużyć, jest wtedy type error.

Śledzenie przetworzonych obiektów WeakSet(), jak za jego pomocą znajdziemy problematyczne referencje to replacerem je usuwamy.

Są gotowe biblioteki do konwersji JSON np circularJSON

// PRZYKŁAD 1: Prosta referencja cykliczna
        const person = {
            name: "Jan",
            age: 30
        };
        
        // Obiekt wskazuje sam na siebie
        person.self = person;

        // To spowoduje błąd!
        try {
            const json = JSON.stringify(person);
        } catch (error) {
            console.error("Błąd:", error.message);
            // "Converting circular structure to JSON"
        }

        // PRZYKŁAD 2: Pośrednia referencja cykliczna
        const user = {
            name: "Anna"
        };
        
        const post = {
            title: "Mój post",
            author: user  // Post wskazuje na użytkownika
        };
        
        user.posts = [post];  // Użytkownik wskazuje na post -> CYKL!

        try {
            JSON.stringify(user);
        } catch (error) {
            console.error("Cykl user -> post -> user:", error.message);
        }

        // ROZWIĄZANIE 1: Replacer z WeakSet
        // WeakSet śledzi już przetworzone obiekty
        function getCircularReplacer() {
            // WeakSet przechowuje referencje do obiektów
            // Nie zapobiega garbage collection
            const seen = new WeakSet();
            
            return (key, value) => {
                // Sprawdzamy tylko obiekty (nie prymitywy)
                if (typeof value === "object" && value !== null) {
                    // Czy już widzieliśmy ten obiekt?
                    if (seen.has(value)) {
                        // Pomijamy - to referencja cykliczna
                        return "[Circular Reference]";
                    }
                    // Dodajemy do "widzianych"
                    seen.add(value);
                }
                return value;
            };
        }

        // Teraz działa!
        const safeJson = JSON.stringify(person, getCircularReplacer(), 2);
        console.log("Bezpieczna serializacja:", safeJson);

        // ROZWIĄZANIE 2: Selektywne pomijanie właściwości
        function createSafeReplacer(skipKeys = []) {
            const seen = new WeakSet();
            
            return (key, value) => {
                // Pomijamy wybrane klucze
                if (skipKeys.includes(key)) {
                    return undefined;
                }
                
                if (typeof value === "object" && value !== null) {
                    if (seen.has(value)) {
                        return "[Circular]";
                    }
                    seen.add(value);
                }
                return value;
            };
        }

        // Pomijamy właściwość 'self' która powoduje cykl
        const jsonWithoutSelf = JSON.stringify(
            person,
            createSafeReplacer(['self']),
            2
        );

        // ROZWIĄZANIE 3: Ręczne usuwanie cykli
        function removeCircularReferences(obj, seen = new WeakSet()) {
            // Dla prymitywów zwracamy wartość bez zmian
            if (typeof obj !== 'object' || obj === null) {
                return obj;
            }
            
            // Sprawdzamy czy już widzieliśmy ten obiekt
            if (seen.has(obj)) {
                return '[Circular]';
            }
            
            // Dodajemy do widzianych
            seen.add(obj);
            
            // Tworzymy nowy obiekt/tablicę bez cykli
            if (Array.isArray(obj)) {
                return obj.map(item => removeCircularReferences(item, seen));
            }
            
            const result = {};
            for (const [key, value] of Object.entries(obj)) {
                result[key] = removeCircularReferences(value, seen);
            }
            
            return result;
        }

        const cleanPerson = removeCircularReferences(person);
        const cleanJson = JSON.stringify(cleanPerson, null, 2);

        // PRZYKŁAD 4: Struktura drzewiasta (częsty przypadek)
        class TreeNode {
            constructor(value) {
                this.value = value;
                this.children = [];
                this.parent = null;  // Potencjalne źródło cykli!
            }

            addChild(node) {
                node.parent = this;  // Tworzy cykl: parent -> child -> parent
                this.children.push(node);
            }

            // Metoda toJSON ignorująca parent (zapobiega cyklom)
            toJSON() {
                return {
                    value: this.value,
                    children: this.children  // Nie uwzględniamy parent
                };
            }
        }

        const root = new TreeNode("root");
        const child1 = new TreeNode("child1");
        const child2 = new TreeNode("child2");
        
        root.addChild(child1);
        root.addChild(child2);

        // Działa dzięki własnej metodzie toJSON
        const treeJson = JSON.stringify(root, null, 2);

        // Wyświetlenie wyników
        const output = document.getElementById('output');
        output.innerHTML = `
            <h3>Problem: Obiekt z self-referencją</h3>
            <p>person.self = person powoduje cykl</p>
            
            <h3>Rozwiązanie 1: WeakSet replacer</h3>
            <pre>${safeJson}</pre>
            
            <h3>Rozwiązanie 2: Pomijanie kluczy</h3>
            <pre>${jsonWithoutSelf}</pre>
            
            <h3>Rozwiązanie 3: Ręczne czyszczenie</h3>
            <pre>${cleanJson}</pre>
            
            <h3>Rozwiązanie 4: Własna toJSON()</h3>
            <pre>${treeJson}</pre>
            
            <p><strong>Wskazówka:</strong> WeakSet nie zapobiega garbage collection</p>
        `;

JSON SCHEMA standard definiowana struktur i elementów walidacji. pola, formaty, ograniczenia wartości, niestandardowe patterny, używa się tworząc JSON, tworząc API do nich. Opis jak wygląda struktura danych. To nasz dokument, ale tak się sklada ze tez jest w formacie JSON

// DEFINICJA JSON SCHEMA
        // Opisuje strukturę i reguły dla obiektu User
        const userSchema = {
            // Wersja specyfikacji schema
            $schema: "http://json-schema.org/draft-07/schema#",
            
            // Identyfikator schema
            $id: "http://example.com/user.schema.json",
            
            // Tytuł i opis
            title: "User",
            description: "Schemat użytkownika aplikacji",
            
            // Typ główny: obiekt
            type: "object",
            
            // Definicje właściwości
            properties: {
                id: {
                    type: "integer",
                    description: "Unikalny identyfikator",
                    minimum: 1  // Musi być >= 1
                },
                username: {
                    type: "string",
                    minLength: 3,  // Min 3 znaki
                    maxLength: 20,  // Max 20 znaków
                    pattern: "^[a-zA-Z0-9_]+$"  // Tylko alfanumeryczne i _
                },
                email: {
                    type: "string",
                    format: "email"  // Musi być poprawnym emailem
                },
                age: {
                    type: "integer",
                    minimum: 18,  // Pełnoletni
                    maximum: 120
                },
                isActive: {
                    type: "boolean"
                },
                roles: {
                    type: "array",
                    items: {
                        type: "string",
                        enum: ["user", "admin", "moderator"]  // Tylko te wartości
                    },
                    minItems: 1,  // Przynajmniej jedna rola
                    uniqueItems: true  // Bez duplikatów
                },
                address: {
                    type: "object",
                    properties: {
                        street: { type: "string" },
                        city: { type: "string" },
                        postalCode: {
                            type: "string",
                            pattern: "^\\d{2}-\\d{3}$"  // Format: 00-000
                        }
                    },
                    required: ["city"]  // Miasto jest wymagane
                }
            },
            
            // Wymagane pola (top level)
            required: ["id", "username", "email"],
            
            // Dodatkowe właściwości nie są dozwolone
            additionalProperties: false
        };

        // PROSTY WALIDATOR (manualny, bez bibliotek)
        // W prawdziwej aplikacji użyj biblioteki jak 'ajv'
        function validateBasic(data, schema) {
            const errors = [];

            // Sprawdzenie typu
            if (schema.type && typeof data !== schema.type) {
                errors.push(`Niepoprawny typ. Oczekiwano: ${schema.type}`);
                return { valid: false, errors };
            }

            // Sprawdzenie wymaganych pól
            if (schema.required && schema.type === 'object') {
                for (const field of schema.required) {
                    if (!(field in data)) {
                        errors.push(`Brakuje wymaganego pola: ${field}`);
                    }
                }
            }

            // Walidacja właściwości obiektu
            if (schema.properties && schema.type === 'object') {
                for (const [key, propSchema] of Object.entries(schema.properties)) {
                    const value = data[key];
                    
                    if (value === undefined) continue;
                    
                    // Sprawdzenie typu właściwości
                    const actualType = Array.isArray(value) ? 'array' : typeof value;
                    if (actualType !== propSchema.type) {
                        errors.push(`${key}: niepoprawny typ. Oczekiwano ${propSchema.type}, otrzymano ${actualType}`);
                        continue;
                    }
                    
                    // String: minLength, maxLength, pattern
                    if (propSchema.type === 'string') {
                        if (propSchema.minLength && value.length < propSchema.minLength) {
                            errors.push(`${key}: za krótki (min: ${propSchema.minLength})`);
                        }
                        if (propSchema.maxLength && value.length > propSchema.maxLength) {
                            errors.push(`${key}: za długi (max: ${propSchema.maxLength})`);
                        }
                        if (propSchema.pattern) {
                            const regex = new RegExp(propSchema.pattern);
                            if (!regex.test(value)) {
                                errors.push(`${key}: nie pasuje do wzorca ${propSchema.pattern}`);
                            }
                        }
                    }
                    
                    // Number/Integer: minimum, maximum
                    if (propSchema.type === 'integer' || propSchema.type === 'number') {
                        if (propSchema.minimum !== undefined && value < propSchema.minimum) {
                            errors.push(`${key}: za mała wartość (min: ${propSchema.minimum})`);
                        }
                        if (propSchema.maximum !== undefined && value > propSchema.maximum) {
                            errors.push(`${key}: za duża wartość (max: ${propSchema.maximum})`);
                        }
                    }
                }
            }

            return {
                valid: errors.length === 0,
                errors
            };
        }

        // TESTOWANIE WALIDACJI
        
        // Poprawny użytkownik
        const validUser = {
            id: 1,
            username: "jkowalski",
            email: "[email protected]",
            age: 30,
            isActive: true,
            roles: ["user", "admin"],
            address: {
                city: "Warszawa",
                postalCode: "00-001"
            }
        };

        // Niepoprawny użytkownik (wiele błędów)
        const invalidUser = {
            id: -1,  // Za małe
            username: "ab",  // Za krótkie
            email: "niepoprawny-email",  // Nie sprawdzimy formatu w prostym walidatorze
            age: 15,  // Za młody
            isActive: "tak",  // Niepoprawny typ
            roles: [],  // Za mało elementów
            address: {
                postalCode: "000001"  // Niepoprawny format
            }  // Brak wymaganego city
        };

        const result1 = validateBasic(validUser, userSchema);
        const result2 = validateBasic(invalidUser, userSchema);

        // Wyświetlenie wyników
        const output = document.getElementById('output');
        output.innerHTML = `
            <h3>Definicja Schema:</h3>
            <pre>${JSON.stringify(userSchema, null, 2)}</pre>
            
            <h3>Test 1: Poprawny użytkownik</h3>
            <p>Walidacja: ${result1.valid ? '✓ PASSED' : '✗ FAILED'}</p>
            ${result1.errors.length > 0 ? `<ul>${result1.errors.map(e => `<li>${e}</li>`).join('')}</ul>` : ''}
            
            <h3>Test 2: Niepoprawny użytkownik</h3>
            <p>Walidacja: ${result2.valid ? '✓ PASSED' : '✗ FAILED'}</p>
            <ul>${result2.errors.map(e => `<li>${e}</li>`).join('')}</ul>
            
            <p><strong>Uwaga:</strong> To uproszczony walidator. W produkcji użyj biblioteki jak 'ajv'</p>

Walidacja danych w JSON

nie ufać json który przychodzi do nas, zawsze trzeba to sprawdzić. Jest różnica między walidacją danych czy dane sa poprawne a sanityzacją danych. Sanityzacja nawet z poprawnych ciągów znaków wewnątrz naszego kodu mogą zadziałać złośliwe. Np. przy konwersji zostaną sparsowane w coś co nam zaszkodzi.

1 : sprawdzić typy: isString, isNumber, isObjectmożna

2: niepoprawne dane: email, kod pocztowy, czy podane wartości zgadzają sie z kluczem, też hasła

3: klasa: jakie pola walidujemy w jaki sposób, w jaki sposób je waliduje, co robimy z obsługą błędów i w jaki sposób

4: obiektów : czy konkretne wartości się w nich znajdują, są odpowiedniego formatu, długości itd. np. dane formularza do rejestracji

Używać .? optional chaining, ?? nulish coalescing

// POZIOM 1: Walidacja typu
        function isString(value) {
            return typeof value === 'string';
        }

        function isNumber(value) {
            return typeof value === 'number' && !isNaN(value);
        }

        function isObject(value) {
            return typeof value === 'object' && value !== null && !Array.isArray(value);
        }

        // POZIOM 2: Walidacja formatu
        const validators = {
            email: (value) => {
                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                return emailRegex.test(value);
            },
            
            phone: (value) => {
                // Polski numer telefonu: 9 cyfr lub +48 xxx xxx xxx
                const phoneRegex = /^(\+48)?[0-9]{9}$/;
                return phoneRegex.test(value.replace(/\s/g, ''));
            },
            
            postalCode: (value) => {
                // Polski kod pocztowy: XX-XXX
                return /^\d{2}-\d{3}$/.test(value);
            },
            
            url: (value) => {
                try {
                    new URL(value);
                    return true;
                } catch {
                    return false;
                }
            },
            
            strongPassword: (value) => {
                // Min 8 znaków, jedna wielka, jedna mała, jedna cyfra
                return value.length >= 8 &&
                       /[A-Z]/.test(value) &&
                       /[a-z]/.test(value) &&
                       /[0-9]/.test(value);
            }
        };

        // POZIOM 3: Klasa walidatora z chainable API
        class Validator {
            constructor(value, fieldName = 'Field') {
                this.value = value;
                this.fieldName = fieldName;
                this.errors = [];
            }

            required() {
                if (this.value === undefined || this.value === null || this.value === '') {
                    this.errors.push(`${this.fieldName} jest wymagane`);
                }
                return this;
            }

            type(expectedType) {
                const actualType = Array.isArray(this.value) ? 'array' : typeof this.value;
                if (actualType !== expectedType) {
                    this.errors.push(`${this.fieldName} musi być typu ${expectedType}`);
                }
                return this;
            }

            minLength(min) {
                if (typeof this.value === 'string' && this.value.length < min) {
                    this.errors.push(`${this.fieldName} musi mieć co najmniej ${min} znaków`);
                }
                return this;
            }

            maxLength(max) {
                if (typeof this.value === 'string' && this.value.length > max) {
                    this.errors.push(`${this.fieldName} nie może być dłuższe niż ${max} znaków`);
                }
                return this;
            }

            range(min, max) {
                if (typeof this.value === 'number') {
                    if (this.value < min || this.value > max) {
                        this.errors.push(`${this.fieldName} musi być między ${min} a ${max}`);
                    }
                }
                return this;
            }

            pattern(regex, message) {
                if (typeof this.value === 'string' && !regex.test(this.value)) {
                    this.errors.push(message || `${this.fieldName} ma niepoprawny format`);
                }
                return this;
            }

            email() {
                return this.pattern(
                    /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
                    `${this.fieldName} musi być poprawnym adresem email`
                );
            }

            custom(fn, message) {
                if (!fn(this.value)) {
                    this.errors.push(message || `${this.fieldName} nie przeszedł walidacji`);
                }
                return this;
            }

            isValid() {
                return this.errors.length === 0;
            }

            getErrors() {
                return this.errors;
            }
        }

        // POZIOM 4: Walidator obiektów
        class ObjectValidator {
            constructor() {
                this.rules = {};
            }

            field(name, validatorFn) {
                this.rules[name] = validatorFn;
                return this;
            }

            validate(obj) {
                const errors = {};
                let isValid = true;

                for (const [fieldName, validatorFn] of Object.entries(this.rules)) {
                    const value = obj[fieldName];
                    const validator = new Validator(value, fieldName);
                    validatorFn(validator);

                    if (!validator.isValid()) {
                        errors[fieldName] = validator.getErrors();
                        isValid = false;
                    }
                }

                return { isValid, errors };
            }
        }

        // PRZYKŁAD UŻYCIA: Walidacja formularza rejestracji
        const registrationValidator = new ObjectValidator()
            .field('username', v => v
                .required()
                .type('string')
                .minLength(3)
                .maxLength(20)
                .pattern(/^[a-zA-Z0-9_]+$/, 'Tylko litery, cyfry i podkreślniki')
            )
            .field('email', v => v
                .required()
                .type('string')
                .email()
            )
            .field('password', v => v
                .required()
                .type('string')
                .minLength(8)
                .custom(
                    val => /[A-Z]/.test(val) && /[a-z]/.test(val) && /[0-9]/.test(val),
                    'Hasło musi zawierać wielką literę, małą literę i cyfrę'
                )
            )
            .field('age', v => v
                .required()
                .type('number')
                .range(18, 120)
            )
            .field('phone', v => v
                .type('string')
                .pattern(/^\+?[0-9]{9,}$/, 'Niepoprawny numer telefonu')
            );

        // Test 1: Poprawne dane
        const validData = {
            username: "jkowalski",
            email: "[email protected]",
            password: "Haslo123",
            age: 25,
            phone: "+48123456789"
        };

        // Test 2: Niepoprawne dane
        const invalidData = {
            username: "ab",  // Za krótkie
            email: "niepoprawny",  // Zły format
            password: "haslo",  // Brak wielkiej litery i cyfry
            age: 15,  // Za młody
            phone: "abc"  // Niepoprawny format
        };

        const result1 = registrationValidator.validate(validData);
        const result2 = registrationValidator.validate(invalidData);

        // Wyświetlenie wyników
        const output = document.getElementById('output');
        output.innerHTML = `
            <h3>Test 1: Poprawne dane</h3>
            <p>Status: ${result1.isValid ? '✓ Walidacja przeszła' : '✗ Błędy walidacji'}</p>
            ${!result1.isValid ? `<pre>${JSON.stringify(result1.errors, null, 2)}</pre>` : ''}
            
            <h3>Test 2: Niepoprawne dane</h3>
            <p>Status: ${result2.isValid ? '✓ Walidacja przeszła' : '✗ Błędy walidacji'}</p>
            <pre>${JSON.stringify(result2.errors, null, 2)}</pre>
            
            <h3>Proste walidatory formatu:</h3>
            <ul>
                <li>Email "[email protected]": ${validators.email('[email protected]') ? '✓' : '✗'}</li>
                <li>Email "niepoprawny": ${validators.email('niepoprawny') ? '✓' : '✗'}</li>
                <li>Telefon "123456789": ${validators.phone('123456789') ? '✓' : '✗'}</li>
                <li>Kod "00-123": ${validators.postalCode('00-123') ? '✓' : '✗'}</li>
                <li>Hasło "Haslo123": ${validators.strongPassword('Haslo123') ? '✓' : '✗'}</li>
            </ul>
        `;
    </script>

Wprowadzenie do DOM

Na podstawie tagów tworzy strukturę, ustawia je hierarchiczne tak jak one funkcjonują. To co jest w pamięci to nie HTML, to DOM Document Object Model, struktura odzwierciedlająca dokument ale nie jest nim z tożsama. HTML to statyczna forma, pozwala na zdefiniowanie struktury i elo, a DOM jest żywy tzn skoro on funkcjonuje w pamięci a nie pliku to możemy go zmodyfikować w dowolnym momencie. Programowy interfejs razem z metodami, API przez które możemy modyfikować nasz obiekt. Drzewo obiektów.

na górze znajduje się węzeł dokument i zawsze tam będzie, pozostałe węzły będą po kolei, możemy odwoływać się do nich po id lub innych selektorach. ten dokument ma zazwyczaj jedno dziecko, którym jest html, a html ma dwójkę dzieci head body. Jak coś modyfikujemy to się automatycznie zmienia. HIERARCHIA

typy węzłów są odpowiednio renderowane w przeglądarce i do odpowiednich typów są dodawane metody dzięki którym możemy wchodzić w interakcje z węzłami, Białe znaki takie jak sapcje, nowe linie, przerwy między tagami tworzą również węzły i są to węzły typu text.

element, text, comment, document głowne typy węzłów

Node - klasa która ma cały szereg atrybutów, właściwości i metod do jej obsługi

Każdy węzeł poza dokument ma rodzica dostępnego przez parent node, parent element, węzły mogą mieć wiele dzieci childNodes (wszystkie węzły zależne od zdefiniowanego węzła tzn również wszystkie puste znaki, znaki nowej linii), children (tylko elementy), pierwsze i ostatnie dzieci jest możliwe do selekcji, metody do wykorzystania gdy np. dodajemy elementy do listy. Rodzeństwo sibling również pozwala na wykorzystanie metod nextSibling, previousSibling.

&0 oznaczony w konsoli ostatnio zaznaczany element, jeśli klikam na jakimś elemencie to dolar zero się pokazuje

 <!-- To jest komentarz HTML -->
        <p id="paragraph">
            To jest <strong>tekst</strong> w paragrafie
        </p>
    </div>
    
    <div id="output"></div>
    <button id="analyzeBtn">Analizuj typy węzłów</button>

    <script>
        // STAŁE DEFINUJĄCE TYPY WĘZŁÓW
        // Dostępne jako właściwości Node
        console.log("Typy węzłów:");
        console.log("Element:", Node.ELEMENT_NODE); // 1
        console.log("Text:", Node.TEXT_NODE); // 3
        console.log("Comment:", Node.COMMENT_NODE); // 8
        console.log("Document:", Node.DOCUMENT_NODE); // 9
        
        const analyzeBtn = document.getElementById('analyzeBtn');
        const output = document.getElementById('output');
        
        analyzeBtn.addEventListener('click', function() {
            const container = document.getElementById('container');
            
            // RÓŻNICA między childNodes a children
            // childNodes - WSZYSTKIE węzły (włącznie z tekstem i komentarzami)
            const allNodes = container.childNodes;
            console.log("Wszystkie węzły (childNodes):", allNodes);
            
            // children - TYLKO węzły elementów
            const elementNodes = container.children;
            console.log("Tylko elementy (children):", elementNodes);
            
            // Funkcja do analizy węzła
            function getNodeInfo(node, index) {
                let typeeName;
                let icon;
                
                switch(node.nodeType) {
                    case Node.ELEMENT_NODE:
                        typeName = 'ELEMENT_NODE (1)';
                        icon = '📦';
                        break;
                    case Node.TEXT_NODE:
                        typeName = 'TEXT_NODE (3)';
                        icon = '📝';
                        break;
                    case Node.COMMENT_NODE:
                        typeName = 'COMMENT_NODE (8)';
                        icon = '💬';
                        break;
                    default:
                        typeName = `UNKNOWN (${node.nodeType})`;
                        icon = '❓';
                }
                
                // Zawartość węzła (skrócona)
                let content = '';
                if (node.nodeType === Node.TEXT_NODE) {
                    const text = node.textContent.trim();
                    content = text ? `"${text.substring(0, 30)}${text.length > 30 ? '...' : ''}"` : '(puste białe znaki)';
                } else if (node.nodeType === Node.COMMENT_NODE) {
                    content = `"${node.textContent}"`;
                } else if (node.nodeType === Node.ELEMENT_NODE) {
                    const tagName = node.tagName.toLowerCase();
                    const id = node.id ? ` id="${node.id}"` : '';
                    content = `<${tagName}${id}>`;
                }
                
                return {
                    index,
                    icon,
                    typeName,
                    nodeName: node.nodeName,
                    content,
                    hasChildren: node.childNodes.length > 0,
                    childrenCount: node.childNodes.length
                };
            }
            
            // Analiza wszystkich węzłów
            let html = '<h2>Analiza węzłów w container:</h2>';
            html += '<h3>Wszystkie węzły (childNodes):</h3>';
            html += '<div style="background: #f5f5f5; padding: 10px;">';
            
            allNodes.forEach((node, index) => {
                const info = getNodeInfo(node, index);
                html += `
                    <div style="background: white; margin: 5px; padding: 10px; border-left: 3px solid #2196f3;">
                        <strong>${info.icon} Węzeł ${info.index}:</strong> ${info.typeName}<br>
                        <strong>nodeName:</strong> ${info.nodeName}<br>
                        <strong>Zawartość:</strong> ${info.content}<br>
                        <strong>Dzieci:</strong> ${info.childrenCount}
                    </div>
                `;
            });
            
            html += '</div>';
            
            // Pokazanie tylko elementów
            html += '<h3>Tylko elementy (children):</h3>';
            html += '<div style="background: #e8f5e9; padding: 10px;">';
            
            Array.from(elementNodes).forEach((element, index) => {
                const tagName = element.tagName.toLowerCase();
                const id = element.id ? ` id="${element.id}"` : '';
                html += `
                    <div style="background: white; margin: 5px; padding: 10px;">
                        📦 Element ${index}: &lt;${tagName}${id}&gt;
                    </div>
                `;
            });
            
            html += '</div>';
            
            // PRAKTYCZNY PRZYKŁAD: Filtrowanie węzłów według typu
            html += '<h3>Węzły pogrupowane według typu:</h3>';
            
            const elements = [];
            const texts = [];
            const comments = [];
            
            allNodes.forEach(node => {
                switch(node.nodeType) {
                    case Node.ELEMENT_NODE:
                        elements.push(node);
                        break;
                    case Node.TEXT_NODE:
                        if (node.textContent.trim()) { // Tylko niepuste teksty
                            texts.push(node);
                        }
                        break;
                    case Node.COMMENT_NODE:
                        comments.push(node);
                        break;
                }
            });
            
            html += `
                <div style="background: #fff3e0; padding: 10px;">
                    <p><strong>📦 Elementy:</strong> ${elements.length}</p>
                    <p><strong>📝 Węzły tekstowe (niepuste):</strong> ${texts.length}</p>
                    <p><strong>💬 Komentarze:</strong> ${comments.length}</p>
                </div>
            `;
            
            // Pokazanie węzłów tekstowych (często niewidocznych dla początkujących)
            html += '<h3>Szczegóły węzłów tekstowych:</h3>';
            html += '<div style="background: #fce4ec; padding: 10px;">';
            
            allNodes.forEach((node, index) => {
                if (node.nodeType === Node.TEXT_NODE) {
                    const text = node.textContent;
                    const trimmed = text.trim();
                    html += `
                        <div style="background: white; margin: 5px; padding: 10px;">
                            <strong>Węzeł ${index}:</strong><br>
                            Surowa zawartość: "${text}"<br>
                            Oczyszczona: "${trimmed}"<br>
                            ${!trimmed ? '<em>To są tylko białe znaki między tagami!</em>' : ''}
                        </div>
                    `;
                }
            });
            
            html += '</div>';
            
            output.innerHTML = html;
        });
        
        // PRAKTYCZNE ZASTOSOWANIE: Iteracja tylko po elementach
        const paragraph = document.getElementById('paragraph');
        
        // PROBLEM: childNodes zawiera także text nodes
        console.log("=== childNodes (wszystkie węzły) ===");
        paragraph.childNodes.forEach((node, index) => {
            console.log(`${index}: ${node.nodeName} (type: ${node.nodeType})`);
        });
        
        // ROZWIĄZANIE: używamy children (tylko elementy)
        console.log("=== children (tylko elementy) ===");
        Array.from(paragraph.children).forEach((element, index) => {
            console.log(`${index}: ${element.tagName}`);
        });
        
        // Lub filtrujemy według typu
        console.log("=== Filtrowanie według typu ===");
        const onlyElements = Array.from(paragraph.childNodes)
            .filter(node => node.nodeType === Node.ELEMENT_NODE);
        console.log("Tylko elementy:", onlyElements);
const item2 = document.getElementById('item2');
        const list = document.getElementById('list');
        
        // RODZIC (Parent)
        // parentNode - zwraca rodzica (dowolny węzeł)
        // parentElement - zwraca rodzica (tylko jeśli to element)
        console.log("=== RODZIC ===");
        console.log("parentNode:", item2.parentNode); // <ul>
        console.log("parentElement:", item2.parentElement); // <ul>
        // Zazwyczaj są identyczne, różnica tylko dla document.documentElement
        console.log("Rodzic <html>:", document.documentElement.parentNode); // document
        console.log("ParentElement <html>:", document.documentElement.parentElement); // null
        
        // DZIECI (Children)
        console.log("=== DZIECI ===");
        // childNodes - WSZYSTKIE węzły (włącznie z tekstem)
        console.log("childNodes list:", list.childNodes);
        // Zawiera text nodes (białe znaki między <li>)!
        
        // children - TYLKO elementy
        console.log("children list:", list.children);
        // HTMLCollection z trzema <li>
        
        // Pierwsze i ostatnie dziecko
        console.log("firstChild:", list.firstChild); // Text node (białe znaki)!
        console.log("firstElementChild:", list.firstElementChild); // <li id="item1">
        console.log("lastChild:", list.lastChild); // Text node
        console.log("lastElementChild:", list.lastElementChild); // <li id="item3">
        
        // RODZEŃSTWO (Siblings)
        console.log("=== RODZEŃSTWO ===");
        // previousSibling/nextSibling - włącznie z text nodes
        console.log("previousSibling item2:", item2.previousSibling); // Text node!
        console.log("nextSibling item2:", item2.nextSibling); // Text node!
        
        // previousElementSibling/nextElementSibling - tylko elementy
        console.log("previousElementSibling item2:", item2.previousElementSibling); // <li id="item1">
        console.log("nextElementSibling item2:", item2.nextElementSibling); // <li id="item3">
        
        // WIZUALIZACJA RELACJI
        const showRelationsBtn = document.getElementById('showRelationsBtn');
        const output = document.getElementById('output');
        
        showRelationsBtn.addEventListener('click', function() {
            function describeRelations(element, name) {
                const parent = element.parentElement;
                const firstChild = element.firstElementChild;
                const lastChild = element.lastElementChild;
                const prevSibling = element.previousElementSibling;
                const nextSibling = element.nextElementSibling;
                const childrenCount = element.children.length;
                
                return `
                    <div style="background: #e1f5fe; padding: 15px; margin: 10px 0; border-radius: 5px;">
                        <h3>${name} (&lt;${element.tagName.toLowerCase()}${element.id ? ' id="' + element.id + '"' : ''}&gt;)</h3>
                        <ul style="list-style: none; padding-left: 0;">
                            <li>↑ <strong>Rodzic:</strong> ${parent ? `<${parent.tagName}#${parent.id || 'no-id'}>` : 'BRAK'}</li>
                            <li>👶 <strong>Liczba dzieci:</strong> ${childrenCount}</li>
                            <li>👶 <strong>Pierwsze dziecko:</strong> ${firstChild ? `<${firstChild.tagName}#${firstChild.id || 'no-id'}>` : 'BRAK'}</li>
                            <li>👶 <strong>Ostatnie dziecko:</strong> ${lastChild ? `<${lastChild.tagName}#${lastChild.id || 'no-id'}>` : 'BRAK'}</li>
                            <li>← <strong>Poprzednie rodzeństwo:</strong> ${prevSibling ? `<${prevSibling.tagName}#${prevSibling.id || 'no-id'}>` : 'BRAK'}</li>
                            <li>→ <strong>Następne rodzeństwo:</strong> ${nextSibling ? `<${nextSibling.tagName}#${nextSibling.id || 'no-id'}>` : 'BRAK'}</li>
                        </ul>
                    </div>
                `;
            }
            
            output.innerHTML = `
                <h2>Analiza relacji:</h2>
                ${describeRelations(list, 'Lista (rodzic)')}
                ${describeRelations(item1, 'Element 1 (pierwsze dziecko)')}
                ${describeRelations(item2, 'Element 2 (środkowy)')}
                ${describeRelations(item3, 'Element 3 (ostatnie dziecko)')}
            `;
        });
        
        // PRAKTYCZNE ZASTOSOWANIA
        
        // 1. Nawigacja do rodzica określonego typu
        function findAncestor(element, tagName) {
            let current = element.parentElement;
            tagName = tagName.toUpperCase();
            
            while (current) {
                if (current.tagName === tagName) {
                    return current;
                }
                current = current.parentElement;
            }
            
            return null;
        }
        
        const ancestorUL = findAncestor(item2, 'UL');
        console.log("Przodek typu UL dla item2:", ancestorUL);
        
        // 2. Pobieranie wszystkich rodzeństwa
        function getAllSiblings(element) {
            const siblings = [];
            let sibling = element.parentElement.firstElementChild;
            
            while (sibling) {
                if (sibling !== element) {
                    siblings.push(sibling);
                }
                sibling = sibling.nextElementSibling;
            }
            
            return siblings;
        }
        
        const siblingsOfItem2 = getAllSiblings(item2);
        console.log("Wszystkie rodzeństwo item2:", siblingsOfItem2);
        
        // 3. Sprawdzenie czy element jest dzieckiem innego elementu
        function isChildOf(child, parent) {
            let current = child.parentElement;
            
            while (current) {
                if (current === parent) {
                    return true;
                }
                current = current.parentElement;
            }
            
            return false;
        }
        
        console.log("Czy item2 jest dzieckiem list?", isChildOf(item2, list)); // true
        console.log("Czy item2 jest dzieckiem body?", isChildOf(item2, document.body)); // true
        
        // 4. Pobieranie indeksu dziecka
        function getChildIndex(element) {
            let index = 0;
            let sibling = element.previousElementSibling;
            
            while (sibling) {
                index++;
                sibling = sibling.previousElementSibling;
            }
            
            return index;
        }
        
        console.log("Indeks item1:", getChildIndex(item1)); // 0
        console.log("Indeks item2:", getChildIndex(item2)); // 1
        console.log("Indeks item3:", getChildIndex(item3)); // 2
  1. getElementById- najłatwiej i najszybciej, ZWRACA 1 ELEMENT, lub null jak nie ma, musimy tworzyc unikalne id wtedy możemy podać id i łatwo się możemy dostać, dostępne tylko na document nie na pojedynczych elementach, nie bierze # przed nazwa, inaczej niż w css, NIE JEST TO WPROST TABLICA, MUSIMY TO SKONWERTOWAĆ, ma tylko długość
  2. getElementsByClassName - zwraca żywa (tzn. ona się zmienia za każdym razem, aktualizowana, jeśli zmienia się DOM) kolekcje wszystkich elementów z dana klasa. To może powodować problemy - np usuwamy wiele elementów w pętli a operacje na drzewie DOM są dość kosztowne, to usuwając pojedynczo w pętli to tak kolekcja sie od nowa tworzy. Warto rozważyć usuniecie większego kawałka hierarchicznie np. usunąć wszystkie elementy z listy, usunąć jakis kawałek kodu to wsadzi go np w div i taki div wyrzucić zamiast np elementy po kolei. NIE JEST TO WPROST TABLICA, MUSIMY TO SKONWERTOWAĆ, ma tylko długość
  3. getElementsByTagName - działa j.w. żywa kolekcja wszystkich elementów tagu listy, paragrafy itd możemy do nich dotrzeć, pokazywać ukrywać modyfikować, NIE JEST TO WPROST TABLICA, MUSIMY TO SKONWERTOWAĆ, ma tylko długość
  4. querySelector zwraca pojedynczy element, idzie przez drzewo DOM i jak znajdzie pasujący element spełniający warunek to ten pierwszy element zostanie zwrócony a ten selektor się zatrzyma. Używamy składni jak w css #container do id, klasy .content, tagu 'p', możemy użyć dowolnych selektorów css tak jak tworzyliśmy style możemy sięgnąć do id clasy taga div p, konkretny atrybut, sub klasy, DOWOLNY WARUNEK; ZWRACA PSEUDO TABLICE, jest for each ale nie ma metod filter, map, sort trzeba skonwertować do tablicy; ma długość, kolekcja sama nie aktualizuje się -> nie może być const musi być let by można było go aktualizować
  5. querySelectorAll zwraca wszystkie pasujące elementy, kolekcja elementów, statyczny, nie aktualizuje się sam, musimy wywołać na nowo by się zaktualizowała lista, można od razu użyć, ZWRACA PSEUDO TABLICE, jest for each ale nie ma metod filter, map, sort trzeba skonwertować do tablicy, ma długość, kolekcja sama nie aktualizuje się ->nie może być const musi być let by można było go aktualizować, elastyczny bezpieczny najlepszy dla grup obiektów

querySelector... są wolniejsze, niż getElement.. ale tak długo jak nie budujemy ogromnego dokumentu to ta różnica nie ma faktycznego poważnego znaczenia

trueArray = Array.from(document.querySelectorAll(.content'); robi tablice prawdziwa z metodami tablicowymi

trueArray2 = [...document.querySelectorAll(.'content')]; tworzenie tablicy przez spread operator

<!-- Elementy do selekcji -->
    <div id="main-content">
        <p class="text">Paragraf 1</p>
        <p class="text highlight">Paragraf 2</p>
        <p class="text">Paragraf 3</p>
        <span class="text">Span 1</span>
        <span>Span 2</span>
    </div>
    
    <div id="output"></div>
    <button id="demoBtn">Demonstracja metod</button>

    <script>
        // 1. getElementById - zwraca POJEDYNCZY element lub null
        // Najszybsza metoda selekcji
        // Dostępna TYLKO na document (nie na pojedynczych elementach)
        
        const mainContent = document.getElementById('main-content');
        console.log("getElementById('main-content'):", mainContent);
        // Zwraca: <div id="main-content">...</div>
        
        const nonExistent = document.getElementById('nie-istnieje');
        console.log("Nieistniejący element:", nonExistent);
        // Zwraca: null
        
        // WAŻNE: Nie używamy # przed ID (to nie CSS!)
        // document.getElementById('#main-content'); // ŹLE!
        // document.getElementById('main-content');  // DOBRZE!
        
        // 2. getElementsByClassName - zwraca HTMLCollection
        // Można wywołać na document lub dowolnym elemencie
        // Zwraca LIVE collection
        
        const textElements = document.getElementsByClassName('text');
        console.log("getElementsByClassName('text'):", textElements);
        console.log("Liczba elementów:", textElements.length); // 4
        console.log("Typ:", textElements.constructor.name); // HTMLCollection
        
        // Można zawęzić wyszukiwanie do konkretnego elementu
        const textsInMain = mainContent.getElementsByClassName('text');
        console.log("text tylko w main:", textsInMain.length); // 4
        
        // Wiele klas (element musi mieć WSZYSTKIE)
        const highlighted = document.getElementsByClassName('text highlight');
        console.log("Elementy z text I highlight:", highlighted.length); // 1
        
        // 3. getElementsByTagName - zwraca HTMLCollection
        // Podobnie jak getElementsByClassName
        // Zwraca LIVE collection
        
        const allParagraphs = document.getElementsByTagName('p');
        console.log("getElementsByTagName('p'):", allParagraphs);
        console.log("Liczba paragrafów:", allParagraphs.length); // 3
        
        const allSpans = document.getElementsByTagName('span');
        console.log("Liczba spanów:", allSpans.length); // 2
        
        // Specjalny case: wszystkie elementy
        const allElements = document.getElementsByTagName('*');
        console.log("WSZYSTKIE elementy:", allElements.length);
        
        // DEMONSTRACJA LIVE COLLECTION
        const demoBtn = document.getElementById('demoBtn');
        const output = document.getElementById('output');
        
        demoBtn.addEventListener('click', function() {
            let html = '<h2>Demonstracja metod selekcji:</h2>';
            
            // Pokazanie HTMLCollection
            html += '<h3>1. getElementsByClassName (LIVE collection):</h3>';
            const textColl = document.getElementsByClassName('text');
            html += `<p>Znaleziono ${textColl.length} elementów z klasą 'text':</p><ul>`;
            
            for (let i = 0; i < textColl.length; i++) {
                html += `<li>${textColl[i].tagName}: ${textColl[i].textContent}</li>`;
            }
            html += '</ul>';
            
            // Pokazanie że to LIVE collection
            html += '<p><strong>Test LIVE collection:</strong></p>';
            html += `<p>Długość przed dodaniem: ${textColl.length}</p>`;
            
            // Dodajemy nowy element z klasą 'text'
            const newP = document.createElement('p');
            newP.className = 'text';
            newP.textContent = 'Nowy paragraf!';
            mainContent.appendChild(newP);
            
            html += `<p>Długość PO dodaniu: ${textColl.length}</p>`;
            html += '<p>Kolekcja automatycznie się zaktualizowała!</p>';
            
            // getElementsByTagName
            html += '<h3>2. getElementsByTagName:</h3>';
            const pElements = document.getElementsByTagName('p');
            html += `<p>Liczba paragrafów: ${pElements.length}</p>`;
            
            const spanElements = document.getElementsByTagName('span');
            html += `<p>Liczba spanów: ${spanElements.length}</p>`;
            
            // getElementById
            html += '<h3>3. getElementById:</h3>';
            const mainById = document.getElementById('main-content');
            html += `<p>Element: ${mainById.tagName}</p>`;
            html += `<p>ID: ${mainById.id}</p>`;
            html += `<p>Liczba dzieci: ${mainById.children.length}</p>`;
            
            // CZĘSTE PROBLEMY
            html += '<h3>Częste problemy i rozwiązania:</h3>';
            
            // Problem 1: HTMLCollection nie jest tablicą
            html += '<h4>Problem 1: HTMLCollection nie jest tablicą</h4>';
            html += '<pre>';
            html += 'const texts = document.getElementsByClassName("text");\n';
            html += '// texts.map() // ❌ BŁĄD! Brak metody map\n\n';
            html += '// Rozwiązanie: konwersja do tablicy\n';
            html += 'const textsArray = Array.from(texts);\n';
            html += 'textsArray.map(el => el.textContent); // ✓ Działa!\n\n';
            html += '// Lub spread operator\n';
            html += 'const textsArray2 = [...texts];\n';
            html += '</pre>';
            
            // Problem 2: Live collection w pętli usuwającej
            html += '<h4>Problem 2: LIVE collection podczas usuwania</h4>';
            html += '<pre>';
            html += '// ❌ BŁĄD - pomija co drugi element!\n';
            html += 'for (let i = 0; i < collection.length; i++) {\n';
            html += '    collection[i].remove(); // Kolekcja się skraca!\n';
            html += '}\n\n';
            html += '// ✓ Rozwiązanie 1: iteruj od tyłu\n';
            html += 'for (let i = collection.length - 1; i >= 0; i--) {\n';
            html += '    collection[i].remove();\n';
            html += '}\n\n';
            html += '// ✓ Rozwiązanie 2: konwertuj do tablicy\n';
            html += 'Array.from(collection).forEach(el => el.remove());\n';
            html += '</pre>';
            
            output.innerHTML = html;
        });
        
        // PRAKTYCZNE PRZYKŁADY
        
        // Przykład 1: Zmiana wszystkich elementów o danej klasie
        function changeAllByClass(className, newText) {
            const elements = document.getElementsByClassName(className);
            // Konwersja do tablicy dla bezpieczeństwa
            Array.from(elements).forEach(el => {
                el.textContent = newText;
            });
        }
        
        // Przykład 2: Liczenie elementów danego typu
        function countElementsOfType(tagName) {
            return document.getElementsByTagName(tagName).length;
        }
        
        console.log("Liczba divów:", countElementsOfType('div'));
        console.log("Liczba paragrafów:", countElementsOfType('p'));
        
        // Przykład 3: Pobieranie tekstu ze wszystkich paragrafów
        function getAllParagraphTexts() {
            const paragraphs = document.getElementsByTagName('p');
            return Array.from(paragraphs).map(p => p.textContent);
        }
        
        console.log("Teksty paragrafów:", getAllParagraphTexts());

Modyfikacje:

const contentDiv = document.getElementById('content')

contentDiv.textContent = "nowa treść"; podmienia to co było w tym div wpisane jako tekst i zmienia na to co zadeklarujemy, usuwa albo pobiera, bo można też zrobić console.log('contentDiv.textContent) wtedy pokaże(pobierze) nam w konsoli to co jest w tym div. To co w txt area wyląduje jest inputem użytkownika, a textcontent pobiera czysty tekst, ignoruje tagi html, nie parsuje html, nie reaguje na sztuczki

contentDiv.innerText ="nowa treść innerText"; - bez css zadziała jak tekst content, ale pobiera tekst i uwzględnia style css z jednym wyjątkiem jeśli w stylu css ustawione jest display:none to to zostanie pominięte

contentDiv.innerHTML ="moja <br> treść"; metoda parsuje i renderuje HTML

const formField= document.getElementById('imie');

console.log(formField.value);

formField.value = "Nowa wartość";

formField.classList.add('important'); dodaje klase np jak źle wypełnisz pole pokazuje sie czerwona ramka

formField.classList.remove('important'); usuwa klase jak dobrze wypełnisz pole to znika ramka

<title>Modyfikacja treści</title>
    <style>
        #content{

            font-size: large;
        }
        .important{
            color: red;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <div id="content" data-type="aaaaa">
        <h2>Witaj w mojej aplikacji!</h2>
        <p>To jest przykładowy paragraf.</p>
    </div>
<form>
<input type="text" placeholder="Imię" id="imie" value="Mariusz Rząsa">
<button  type="button" onclick="zmienTreść()">Zmień treść</button>

</form>

<script>
    const contentDiv = document.getElementById('content');
    //contentDiv.textContent = "nowa treść";
    console.log(contentDiv.textContent);
    // contentDiv.innerText = "Nowa treść innerText"
    console.log(contentDiv.innerText);
    contentDiv.innerHTML = "<strong>moja</strong> <br> treść";

    const formField= document.getElementById('imie');
    console.log(formField.value);
    formField.value = "Nowa wartość";

    formField.classList.add('important');
    formField.classList.remove('important');

    console.log(contentDiv.getAttribute('data-type'));
    contentDiv.setAttribute('data-type', 'bbbb');
    contentDiv.removeAttribute('data-type');


    function zmienTreść(){
        contentDiv.innerHTML = "<em>Zmieniona treść!</em>";