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.
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}: <${tagName}${id}>
</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} (<${element.tagName.toLowerCase()}${element.id ? ' id="' + element.id + '"' : ''}>)</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- 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ść
- 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ść
- 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ść
- 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ć
- 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>";