Temat 25 · 2 × 45 min
Błędy, JSON i localStorage
Do tej pory błąd oznaczał czerwony napis w konsoli i zatrzymany skrypt. W prawdziwej aplikacji błędy są normalną częścią pracy: użytkownik wpisze literę zamiast liczby, serwer odpowie śmieciami, pamięć przeglądarki będzie pełna. Ta lekcja uczy trzech rzeczy, które zawsze występują razem: jak przechwycić i zgłosić błąd (try…catch, throw), jak zamienić dane na tekst i z powrotem (JSON) oraz jak zapisać je w przeglądarce, żeby przetrwały odświeżenie strony (localStorage). Razem dają „backend bez backendu” – aplikację, która pamięta stan bez serwera.
Rodzaje błędów i jak je czytać
Gdy silnik nie może wykonać instrukcji, rzuca (throw) wyjątek – obiekt błędu – i przerywa bieżący kod. Jeśli nic go nie przechwyci, trafia do konsoli jako Uncaught …. Wbudowane typy, z którymi spotkasz się najczęściej:
| Typ | Kiedy | Typowy komunikat |
|---|---|---|
SyntaxError | kod nie da się sparsować (brak nawiasu, zły JSON) | Unexpected token '}' |
ReferenceError | użycie nieistniejącej nazwy | cena is not defined |
TypeError | operacja na złym typie (metoda na undefined, zmiana const) | Cannot read properties of null (reading 'value') |
RangeError | wartość poza zakresem | Invalid array length, toFixed() digits argument must be between 0 and 100 |
Komunikat ma trzy części: nazwę typu, opis i stos wywołań (stack trace) – listę funkcji od miejsca błędu do początku, z numerami linii. Czytaj od góry: pierwsza linia stosu to miejsce, gdzie błąd powstał. Cannot read properties of null (reading 'value') niemal zawsze znaczy: getElementById nie znalazł elementu (literówka w id albo skrypt wykonał się przed HTML).
// każdy błąd łapiemy osobno, żeby zobaczyć wszystkie
const proby = [
() => nieistniejaca + 1,
() => null.value,
() => (123.45).toFixed(200),
() => JSON.parse('{ zly json }'),
];
for (const proba of proby) {
try {
proba();
} catch (blad) {
console.log(`${blad.name}: ${blad.message}`);
}
}try…catch…finally
Blok try zawiera kod, który może się nie udać. Gdy w nim poleci wyjątek, wykonanie natychmiast przeskakuje do catch (reszta try jest pomijana), a w zmiennej po catch ląduje obiekt błędu. finally wykonuje się zawsze – po udanym try, po catch, a nawet gdy w środku jest return. Służy do sprzątania: ukrycia spinnera, odblokowania przycisku, zamknięcia połączenia.
Co wypisze ten kod?
function test() {
try {
console.log('A');
return 'wynik';
} catch {
console.log('B');
} finally {
console.log('C');
}
}
console.log(test());function parsujLiczbe(tekst) {
try {
console.log('Próbuję:', tekst);
const liczba = Number(tekst);
if (Number.isNaN(liczba)) throw new Error(`„${tekst}” nie jest liczbą`);
console.log('Udało się:', liczba);
return liczba;
} catch (blad) {
console.log('Przechwycono:', blad.message);
return 0; // wartość awaryjna – program działa dalej
} finally {
console.log('finally – zawsze, np. odblokuj przycisk');
}
}
parsujLiczbe('42');
parsujLiczbe('abc');
console.log('Skrypt dalej działa');Od ES2019 zmienną w catch można pominąć (catch {), jeśli jej nie potrzebujesz. Blok try może mieć samo catch, samo finally lub oba.
Obiekt błędu
Każdy błąd ma name (typ), message (opis) i stack (stos jako napis). Do użytkownika pokazuj tylko message – i to przemyślane; do konsoli (console.error(blad)) cały obiekt, bo wtedy DevTools pokaże klikalny stos.
throw – zgłaszanie własnych błędów
throw rzuca dowolną wartość, ale rzucaj zawsze obiekt Error (lub jego podklasę) – tylko on ma stack, a narzędzia i biblioteki na to liczą. Rzucaj, gdy funkcja dostała dane, z którymi nie może zrobić nic sensownego: zamiast cicho zwracać NaN czy undefined, głośno powiedz, co jest nie tak. Decyzję, jak zareagować (komunikat, wartość domyślna, ponowna próba), zostaw temu, kto wywołał.
function obliczRate(koszt, liczbaRat) {
if (typeof koszt !== 'number' || koszt < 0) {
throw new TypeError('Koszt musi być liczbą nieujemną');
}
if (!Number.isInteger(liczbaRat) || liczbaRat < 1) {
throw new RangeError('Liczba rat musi być całkowita i co najmniej 1');
}
return Math.round((koszt / liczbaRat) * 100) / 100;
}
console.log(obliczRate(900, 4));
for (const [k, r] of [[900, 0], ['900', 3], [100, 3]]) {
try {
console.log(`Rata: ${obliczRate(k, r)} zł`);
} catch (e) {
console.log(`${e.name} → ${e.message}`);
}
}Własne klasy błędów
Gdy aplikacja rozróżnia rodzaje problemów (dane użytkownika vs błąd sieci vs brak uprawnień), definiujesz własną klasę dziedziczącą po Error. W catch sprawdzasz instanceof i reagujesz inaczej na każdą. Składnię class … extends poznasz dokładnie w lekcji o klasach; tu wystarczy wzorzec do skopiowania.
class BladWalidacji extends Error {
constructor(message, pole) {
super(message); // ustawia message i stack
this.name = 'BladWalidacji'; // zamiast domyślnego „Error”
this.pole = pole; // dodatkowa informacja: które pole formularza
}
}
function sprawdzWiek(tekst) {
const wiek = Number(tekst);
if (tekst.trim() === '' || Number.isNaN(wiek)) throw new BladWalidacji('Wpisz liczbę', 'wiek');
if (wiek < 13 || wiek > 120) throw new BladWalidacji('Wiek musi być z zakresu 13–120', 'wiek');
return wiek;
}
for (const wejscie of ['17', 'abc', '7']) {
try {
console.log('OK:', sprawdzWiek(wejscie));
} catch (e) {
if (e instanceof BladWalidacji) {
console.log(`Pole „${e.pole}”: ${e.message}`); // komunikat dla użytkownika
} else {
throw e; // nie nasz błąd – puść dalej
}
}
}Zwróć uwagę na throw e w else: łapiemy tylko błędy walidacji, które umiemy pokazać użytkownikowi. Literówka w kodzie (ReferenceError) nie ma być maskowana komunikatem „Wpisz liczbę” – ma dotrzeć do konsoli.
JSON – dane jako tekst
JSON (JavaScript Object Notation) to tekstowy format zapisu danych: obiekty, tablice, napisy, liczby, true/false/null. Wygląda jak literał obiektu w JS, ale ma surowsze zasady – to format wymiany, który czytają wszystkie języki (PHP, Python, Java…). Dostaniesz go z każdego API, zapiszesz w nim stan aplikacji, w nim są pliki konfiguracyjne (package.json).
| Literał JS | JSON |
|---|---|
klucze bez cudzysłowów: { imie: 'Ala' } | klucze zawsze w podwójnych cudzysłowach: {"imie": "Ala"} |
napisy w '…' lub "…" | napisy tylko w "…" |
| przecinek po ostatnim elemencie dozwolony | przecinek końcowy to błąd |
komentarze // | brak komentarzy |
undefined, funkcje, Date, NaN | tylko: obiekt, tablica, napis, liczba, true, false, null |
JSON.stringify – obiekt → tekst
const uczen = {
imie: 'Ala',
oceny: [5, 4],
aktywny: true,
opiekun: null,
// poniższe JSON gubi lub zmienia:
srednia: undefined,
policz() { return 1; },
data: new Date('2026-10-20T10:00:00Z'),
limit: NaN,
};
const tekst = JSON.stringify(uczen);
console.log(tekst);
console.log(typeof tekst);
// czytelnie, z wcięciem 2 spacji – do debugowania i plików
console.log(JSON.stringify({ a: 1, b: [1, 2] }, null, 2));
// tylko wybrane pola: tablica kluczy jako drugi argument
console.log(JSON.stringify(uczen, ['imie', 'aktywny']));Co się stało: undefined i metoda zniknęły, Date stała się napisem ISO, NaN stało się null. Po odczytaniu z JSON data będzie zwykłym napisem – trzeba ją odtworzyć przez new Date(tekst). O tym, dlaczego JSON.parse(JSON.stringify(x)) to kiepska kopia głęboka, była mowa w lekcji o referencjach.
JSON.parse – tekst → obiekt
Co wypisze ten kod?
const dane = JSON.parse('{"a": 1, "b": [true, null]}');
console.log(dane.a, dane.b.length, typeof dane);JSON.parse jest surowy: na niepoprawnym tekście rzuca SyntaxError. Dlatego każde JSON.parse danych, których nie napisałeś sam (z serwera, z localStorage, z pola tekstowego), owijaj w try…catch. Przypadki brzegowe warto znać na pamięć:
console.log(JSON.parse('{"imie":"Ala","oceny":[5,4]}').oceny[0]); // 5
console.log(JSON.parse('[1, 2, 3]').length); // 3
console.log(JSON.parse('"tekst"'), JSON.parse('42'), JSON.parse('true')); // tekst 42 true
// null jest zamieniane na napis "null", a to poprawny JSON → wynik null
console.log(JSON.parse(null)); // null
const zle = ["{ imie: 'Ala' }", "{'a': 1}", '{"a": 1,}', '', undefined];
for (const tekst of zle) {
try {
JSON.parse(tekst);
console.log('OK:', tekst);
} catch (e) {
console.log(`${e.name} dla ${JSON.stringify(tekst)}`);
}
}To ważne dla localStorage: getItem zwraca null, gdy klucza nie ma, a JSON.parse(null) grzecznie daje null – nie błąd. Ale pusty napis albo uszkodzone dane rzucą SyntaxError. Stąd wzorzec parsujBezpiecznie:
function parsujBezpiecznie(tekst, domyslna) {
if (tekst === null || tekst === undefined) return domyslna;
try {
return JSON.parse(tekst);
} catch (e) {
console.log('Uszkodzone dane, używam domyślnych:', e.message);
return domyslna;
}
}
console.log(parsujBezpiecznie('[1,2]', []));
console.log(parsujBezpiecznie(null, [])); // brak klucza w storage
console.log(parsujBezpiecznie('{oops', [])); // uszkodzoneDla dat przydaje się drugi argument JSON.parse – reviver, funkcja wywoływana dla każdej pary klucz–wartość, która może ją podmienić:
const tekst = '{"tytul":"Egzamin","termin":"2027-01-11T08:00:00.000Z"}';
const zadanie = JSON.parse(tekst, (klucz, wartosc) =>
klucz === 'termin' ? new Date(wartosc) : wartosc
);
console.log(zadanie.termin.getFullYear(), typeof zadanie.tytul);localStorage – pamięć przeglądarki
localStorage to wbudowany magazyn klucz–wartość, który przeglądarka trzyma na dysku, osobno dla każdej witryny (origin: protokół + domena + port). Dane przetrwają odświeżenie, zamknięcie karty i restart komputera – aż sam je usuniesz albo użytkownik wyczyści dane strony. Limit to zwykle ok. 5 MB na witrynę. sessionStorage ma identyczne API, ale żyje tylko do zamknięcia karty.
| Metoda | Co robi |
|---|---|
localStorage.setItem(klucz, wartosc) | zapisuje; wartość jest zamieniana na napis |
localStorage.getItem(klucz) | zwraca napis albo null, gdy klucza nie ma |
localStorage.removeItem(klucz) | usuwa jeden klucz |
localStorage.clear() | usuwa wszystko z tej witryny |
localStorage.length, localStorage.key(i) | liczba kluczy i i-ty klucz |
Dwie konsekwencje, które powodują większość błędów:
- Przechowywane są tylko napisy.
setItem('licznik', 5)zapisze"5"; po odczyciegetItem('licznik') + 1da"51". Obiekt zapisany wprost stanie się"[object Object]". Dlatego wszystko oprócz prostych napisów przepuszczaj przez JSON:setItem(k, JSON.stringify(obj))iJSON.parse(getItem(k)). - Dostęp może rzucić wyjątek. W trybie prywatnym niektórych przeglądarek, przy pełnej pamięci (
QuotaExceededError) albo w ramce z zablokowanym dostępem (SecurityError) samolocalStorage.setItemwyrzuci błąd. Opakuj dostęp w funkcje ztry…catch, a aplikacja przeżyje bez pamięci.
// Uruchom ten kod w konsoli DevTools na dowolnej stronie (F12 → Console)
// albo wklej do pliku .html – w ramce „Uruchom” localStorage jest zablokowany.
localStorage.setItem('motyw', 'ciemny');
console.log(localStorage.getItem('motyw')); // 'ciemny'
console.log(localStorage.getItem('brak')); // null
localStorage.setItem('licznik', 5);
console.log(typeof localStorage.getItem('licznik')); // 'string'!
console.log(localStorage.getItem('licznik') + 1); // '51' – konkatenacja
console.log(Number(localStorage.getItem('licznik')) + 1); // 6
const zadania = [{ tekst: 'Nauczyć się JSON', zrobione: false }];
localStorage.setItem('zadania', JSON.stringify(zadania));
const odczytane = JSON.parse(localStorage.getItem('zadania'));
console.log(odczytane[0].tekst, odczytane[0].zrobione);
localStorage.removeItem('licznik');Bezpieczne opakowanie
Zamiast wywoływać localStorage w dziesięciu miejscach, napisz dwie funkcje: zapisz(klucz, wartosc) i odczytaj(klucz, domyslna). Jedno miejsce z JSON i try…catch – reszta aplikacji używa zwykłych obiektów.
function zapisz(klucz, wartosc) {
try {
localStorage.setItem(klucz, JSON.stringify(wartosc));
return true;
} catch (e) {
console.error('Nie udało się zapisać:', e.name, e.message); // np. QuotaExceededError
return false;
}
}
function odczytaj(klucz, domyslna) {
try {
const tekst = localStorage.getItem(klucz);
return tekst === null ? domyslna : JSON.parse(tekst);
} catch (e) {
console.error('Nie udało się odczytać:', e.name, e.message); // SecurityError lub SyntaxError
return domyslna;
}
}
// użycie
const ustawienia = odczytaj('ustawienia', { motyw: 'jasny', rozmiar: 16 });
ustawienia.motyw = 'ciemny';
zapisz('ustawienia', ustawienia);Dlaczego place zabaw w tej lekcji działają „zastępczo”
Plac zabaw na tej stronie uruchamia kod w ramce <iframe sandbox> o tzw. nieprzejrzystym pochodzeniu (opaque origin). Przeglądarka nie wie, do jakiej witryny należą dane, więc każde dotknięcie localStorage rzuca tam SecurityError. To nie błąd w kodzie – to ten sam przypadek, przed którym chroni try…catch wyżej. Place zabaw poniżej wykrywają go i przełączają się na magazyn w pamięci (zwykły obiekt), który znika przy ponownym uruchomieniu. Zobaczysz komunikat o trybie. Żeby zobaczyć prawdziwe zapamiętywanie po odświeżeniu, zapisz kod do pliku .html na dysku i otwórz go w przeglądarce – gotowy plik jest w ostatnim zadaniu.
<p id="tryb" style="font-size: 13px; color: #666;"></p>
<button id="przelacz">Przełącz motyw</button>
<p id="stan"></p>body { font-family: system-ui, sans-serif; transition: background 0.3s; padding: 12px; }
body.ciemny { background: #1f2a44; color: #fff; }// Magazyn: prawdziwy localStorage, a gdy jest niedostępny – obiekt w pamięci z tym samym API
function zrobMagazyn() {
try {
const test = '__test__';
localStorage.setItem(test, '1');
localStorage.removeItem(test);
document.getElementById('tryb').textContent = 'Tryb: prawdziwy localStorage – odśwież stronę, motyw zostanie.';
return localStorage;
} catch (e) {
document.getElementById('tryb').textContent =
`Tryb zastępczy (pamięć): ${e.name} – w tej ramce localStorage jest zablokowany. Zapisz kod do pliku .html, żeby działał naprawdę.`;
const pamiec = {};
return {
getItem: (k) => (k in pamiec ? pamiec[k] : null),
setItem: (k, v) => { pamiec[k] = String(v); },
removeItem: (k) => { delete pamiec[k]; },
};
}
}
const magazyn = zrobMagazyn();
const stan = document.getElementById('stan');
function zastosuj(motyw) {
document.body.classList.toggle('ciemny', motyw === 'ciemny');
stan.textContent = `Aktualny motyw: ${motyw}`;
}
// odczyt przy starcie – gdy klucza nie ma, getItem daje null, więc ?? ustawia domyślny
let motyw = magazyn.getItem('motyw') ?? 'jasny';
zastosuj(motyw);
document.getElementById('przelacz').addEventListener('click', () => {
motyw = motyw === 'jasny' ? 'ciemny' : 'jasny';
magazyn.setItem('motyw', motyw);
zastosuj(motyw);
});„Backend bez backendu”: wzorzec stan → zapis → odtworzenie
Aplikacja, która pamięta dane bez serwera, zawsze ma ten sam szkielet:
- Stan w jednej zmiennej (tablica obiektów), jak w lekcji o stanie aplikacji.
renderuj()– rysuje DOM z stanu (np.map().join('')z lekcji o metodach tablic).zapisz()– po każdej zmianie stanulocalStorage.setItem(klucz, JSON.stringify(stan)).- Start –
stan = odczytaj(klucz, []), potemrenderuj().
Każda akcja użytkownika robi trzy rzeczy w tej kolejności: zmień stan → zapisz → renderuj. Jeśli zapis pominiesz w jednym miejscu, po odświeżeniu znikną właśnie te zmiany – to najczęstszy błąd. Dlatego wiele osób łączy zapis z renderowaniem w jedną funkcję aktualizuj().
Ściąga
| Składnia | Co robi | Uwaga |
|---|---|---|
try { … } catch (e) { … } | przechwyć wyjątek z try | tylko kod synchroniczny w try |
finally { … } | zawsze na końcu | nawet po return i po błędzie |
catch { … } | bez zmiennej (ES2019) | gdy błąd nieistotny – ale zaloguj! |
throw new Error('opis') | rzuć błąd | rzucaj obiekty Error, nie napisy |
e.name, e.message, e.stack | typ, opis, stos | do użytkownika tylko message |
class X extends Error | własny typ błędu | super(message); this.name = 'X' |
e instanceof X | rozróżnij typ w catch | inne błędy rzuć dalej: throw e |
JSON.stringify(obj) | obiekt → tekst JSON | gubi undefined, funkcje; Date → napis |
JSON.stringify(obj, null, 2) | z wcięciem | do debugowania i plików |
JSON.parse(tekst) | tekst → obiekt | zły tekst → SyntaxError; parse(null) → null |
JSON.parse(t, reviver) | podmień wartości przy odczycie | np. napis → Date |
localStorage.setItem(k, v) | zapisz napis | obiekt → najpierw JSON.stringify |
localStorage.getItem(k) | napis lub null | liczby wracają jako napisy |
localStorage.removeItem(k), .clear() | usuń klucz / wszystko | tylko tej witryny |
sessionStorage | to samo API, żyje do zamknięcia karty | |
try { localStorage… } catch | ochrona przed SecurityError/QuotaExceededError | tryb prywatny, pełna pamięć, ramki |
Zadania
Bezpieczny odczyt ustawień
Napisz funkcję wczytajUstawienia(tekst), która przyjmuje napis z JSON (taki, jaki zwróciłby localStorage.getItem) i zwraca obiekt ustawień. Zasady: dla null zwraca domyślne { motyw: 'jasny', rozmiar: 16 }; dla uszkodzonego JSON wypisuje ostrzeżenie i zwraca domyślne; dla poprawnego JSON uzupełnia brakujące pola domyślnymi (spread). Przetestuj na: null, '{"motyw":"ciemny"}', '{zepsute'.
Pokaż rozwiązanie
const DOMYSLNE = { motyw: 'jasny', rozmiar: 16 };
function wczytajUstawienia(tekst) {
if (tekst === null) return { ...DOMYSLNE }; // brak klucza – kopia domyślnych
try {
const zapisane = JSON.parse(tekst);
return { ...DOMYSLNE, ...zapisane }; // zapisane nadpisują domyślne, reszta z domyślnych
} catch (e) {
console.log('Ostrzeżenie: uszkodzone ustawienia –', e.message);
return { ...DOMYSLNE };
}
}
console.log(wczytajUstawienia(null));
console.log(wczytajUstawienia('{"motyw":"ciemny"}'));
console.log(wczytajUstawienia('{zepsute'));Walidacja zamówienia z własnymi błędami
Zdefiniuj klasę BladZamowienia extends Error z dodatkowym polem kod ('PUSTE', 'ILOSC', 'BRAK_PRODUKTU'). Funkcja zlozZamowienie(koszyk, cennik) ma rzucać ten błąd, gdy: koszyk jest pusty; któraś pozycja ma ilosc niecałkowitą lub mniejszą niż 1; produktu nie ma w cenniku. Gdy wszystko gra, zwraca sumę. Osobna funkcja obsluz(koszyk) wywołuje zlozZamowienie w try…catch, wypisuje komunikat zależny od kod i puszcza dalej błędy innych typów.
Pokaż rozwiązanie
class BladZamowienia extends Error {
constructor(message, kod) {
super(message);
this.name = 'BladZamowienia';
this.kod = kod;
}
}
const cennik = { kabel: 20, mysz: 80 };
function zlozZamowienie(koszyk, cennik) {
if (koszyk.length === 0) throw new BladZamowienia('Koszyk jest pusty', 'PUSTE');
let suma = 0;
for (const { produkt, ilosc } of koszyk) {
if (!Number.isInteger(ilosc) || ilosc < 1) {
throw new BladZamowienia(`Zła ilość dla „${produkt}”: ${ilosc}`, 'ILOSC');
}
if (!(produkt in cennik)) {
throw new BladZamowienia(`Nie znamy produktu „${produkt}”`, 'BRAK_PRODUKTU');
}
suma += cennik[produkt] * ilosc;
}
return suma;
}
const komunikaty = {
PUSTE: 'Dodaj coś do koszyka.',
ILOSC: 'Popraw ilość – musi być liczbą całkowitą od 1.',
BRAK_PRODUKTU: 'Jeden z produktów jest już niedostępny.',
};
function obsluz(koszyk) {
try {
console.log('Do zapłaty:', zlozZamowienie(koszyk, cennik), 'zł');
} catch (e) {
if (e instanceof BladZamowienia) {
console.log(`[${e.kod}] ${komunikaty[e.kod]} (${e.message})`);
} else {
throw e; // np. TypeError z literówki – nie maskujemy
}
}
}
obsluz([{ produkt: 'kabel', ilosc: 2 }, { produkt: 'mysz', ilosc: 1 }]);
obsluz([]);
obsluz([{ produkt: 'kabel', ilosc: 1.5 }]);
obsluz([{ produkt: 'monitor', ilosc: 1 }]);Licznik odwiedzin i ostatnia wizyta
Zbuduj stronę, która po każdym otwarciu wypisuje: „To twoja N. wizyta. Ostatnio: data i godzina” (przy pierwszej wizycie: „Witaj po raz pierwszy!”). Stan trzymaj w localStorage pod jednym kluczem jako JSON { liczba, ostatnia }, gdzie ostatnia to data w formacie ISO (new Date().toISOString()). Dodaj przycisk „Wyzeruj”. Dostęp do magazynu opakuj w try…catch (w placu zabaw zobaczysz tryb zastępczy – prawdziwe liczenie sprawdzisz w pliku .html).
Pokaż rozwiązanie
<p id="tryb" style="font-size: 13px; color: #666;"></p>
<p id="komunikat"></p>
<button id="wyzeruj">Wyzeruj</button>const KLUCZ = 'wizyty';
function odczytaj(klucz, domyslna) {
try {
const tekst = localStorage.getItem(klucz);
return tekst === null ? domyslna : JSON.parse(tekst);
} catch (e) {
document.getElementById('tryb').textContent = `Tryb zastępczy: ${e.name} – zapisz kod do pliku .html, żeby licznik działał między odświeżeniami.`;
return domyslna;
}
}
function zapisz(klucz, wartosc) {
try {
localStorage.setItem(klucz, JSON.stringify(wartosc));
} catch (e) {
// w ramce bez localStorage po prostu nie zapisujemy – strona dalej działa
}
}
const poprzednie = odczytaj(KLUCZ, null);
const komunikat = document.getElementById('komunikat');
if (poprzednie === null) {
komunikat.textContent = 'Witaj po raz pierwszy!';
} else {
// z JSON data wraca jako napis ISO – odtwarzamy obiekt Date, żeby ją sformatować
const ostatnia = new Date(poprzednie.ostatnia);
komunikat.textContent = `To twoja ${poprzednie.liczba + 1}. wizyta. Ostatnio: ${ostatnia.toLocaleString('pl-PL')}`;
}
zapisz(KLUCZ, {
liczba: (poprzednie?.liczba ?? 0) + 1,
ostatnia: new Date().toISOString(),
});
document.getElementById('wyzeruj').addEventListener('click', () => {
try { localStorage.removeItem(KLUCZ); } catch {}
komunikat.textContent = 'Wyzerowano – odśwież stronę.';
});Lista zadań, która pamięta
Rozbuduj listę zadań z lekcji o tworzeniu elementów: dodawanie, oznaczanie „Wykonane” (przekreślenie), usuwanie – i zapis całego stanu w localStorage tak, by po odświeżeniu lista wróciła w tym samym stanie. Wymagania:
- stan to tablica obiektów
{ id, tekst, zrobione }w jednej zmiennej; DOM jest zawsze rysowany ze stanu (renderuj()), nigdy odwrotnie, - każda akcja: zmień stan →
zapisz()→renderuj(), - odczyt przy starcie przez bezpieczne
odczytaj(klucz, [])ztry…catch, idgeneruj przezDate.now()albocrypto.randomUUID(),- tekst zadania wstawiaj przez
textContent(nieinnerHTML) – użytkownik może wpisać<b>.
Zapisz rozwiązanie jako kompletny plik zadania.html i otwórz z dysku – sprawdź, że po odświeżeniu (F5) i po zamknięciu karty zadania zostają, a w DevTools → Application → Local Storage widzisz klucz z JSON.
Pokaż rozwiązanie
Wersja do placu zabaw (z trybem zastępczym) i poniżej ten sam kod jako kompletny plik.
<p id="tryb" style="font-size: 13px; color: #666;"></p>
<form id="formularz">
<input id="nowe" placeholder="Nowe zadanie" autocomplete="off" required>
<button type="submit">Dodaj</button>
</form>
<ul id="lista"></ul>
<p id="podsumowanie"></p>body { font-family: system-ui, sans-serif; }
li { display: flex; gap: 8px; align-items: center; padding: 4px 0; }
li.zrobione span { text-decoration: line-through; color: #888; }const KLUCZ = 'zadania';
function odczytaj(klucz, domyslna) {
try {
const tekst = localStorage.getItem(klucz);
return tekst === null ? domyslna : JSON.parse(tekst);
} catch (e) {
document.getElementById('tryb').textContent = `Tryb zastępczy: ${e.name}. W pliku .html na dysku lista przetrwa odświeżenie.`;
return domyslna;
}
}
function zapisz(klucz, wartosc) {
try {
localStorage.setItem(klucz, JSON.stringify(wartosc));
} catch (e) {
console.error('Zapis nieudany:', e.name);
}
}
// 1. stan – jedyne źródło prawdy
let zadania = odczytaj(KLUCZ, []);
// 2. render ze stanu
const lista = document.getElementById('lista');
function renderuj() {
lista.innerHTML = '';
for (const z of zadania) {
const li = document.createElement('li');
li.className = z.zrobione ? 'zrobione' : '';
const tekst = document.createElement('span');
tekst.textContent = z.tekst; // bezpiecznie, nawet gdy wpisano <b>
const wykonane = document.createElement('button');
wykonane.textContent = z.zrobione ? 'Cofnij' : 'Wykonane';
wykonane.addEventListener('click', () => przelacz(z.id));
const usun = document.createElement('button');
usun.textContent = 'Usuń';
usun.addEventListener('click', () => usunZadanie(z.id));
li.append(tekst, wykonane, usun);
lista.append(li);
}
const zrobione = zadania.filter((z) => z.zrobione).length;
document.getElementById('podsumowanie').textContent = `Zadań: ${zadania.length}, wykonanych: ${zrobione}`;
}
// 3. akcje: zmień stan → zapisz → renderuj (bez mutowania – nowe tablice/obiekty)
function aktualizuj(nowyStan) {
zadania = nowyStan;
zapisz(KLUCZ, zadania);
renderuj();
}
function dodaj(tekst) {
aktualizuj([...zadania, { id: Date.now(), tekst, zrobione: false }]);
}
function przelacz(id) {
aktualizuj(zadania.map((z) => (z.id === id ? { ...z, zrobione: !z.zrobione } : z)));
}
function usunZadanie(id) {
aktualizuj(zadania.filter((z) => z.id !== id));
}
document.getElementById('formularz').addEventListener('submit', (event) => {
event.preventDefault(); // formularz nie przeładowuje strony
const pole = document.getElementById('nowe');
const tekst = pole.value.trim();
if (!tekst) return;
dodaj(tekst);
pole.value = '';
pole.focus();
});
// 4. start
renderuj();Kompletny plik do zapisania na dysku (ten sam JS, bez komunikatu o trybie):
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Lista zadań</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 480px; margin: 40px auto; }
li { display: flex; gap: 8px; align-items: center; padding: 4px 0; }
li.zrobione span { text-decoration: line-through; color: #888; }
</style>
</head>
<body>
<h1>Lista zadań</h1>
<form id="formularz">
<input id="nowe" placeholder="Nowe zadanie" autocomplete="off" required>
<button type="submit">Dodaj</button>
</form>
<ul id="lista"></ul>
<p id="podsumowanie"></p>
<script>
const KLUCZ = 'zadania';
function odczytaj(klucz, domyslna) {
try {
const tekst = localStorage.getItem(klucz);
return tekst === null ? domyslna : JSON.parse(tekst);
} catch (e) {
console.error('Odczyt nieudany:', e.name);
return domyslna;
}
}
function zapisz(klucz, wartosc) {
try {
localStorage.setItem(klucz, JSON.stringify(wartosc));
} catch (e) {
console.error('Zapis nieudany:', e.name);
}
}
let zadania = odczytaj(KLUCZ, []);
const lista = document.getElementById('lista');
function renderuj() {
lista.innerHTML = '';
for (const z of zadania) {
const li = document.createElement('li');
li.className = z.zrobione ? 'zrobione' : '';
const tekst = document.createElement('span');
tekst.textContent = z.tekst;
const wykonane = document.createElement('button');
wykonane.textContent = z.zrobione ? 'Cofnij' : 'Wykonane';
wykonane.addEventListener('click', () => przelacz(z.id));
const usun = document.createElement('button');
usun.textContent = 'Usuń';
usun.addEventListener('click', () => usunZadanie(z.id));
li.append(tekst, wykonane, usun);
lista.append(li);
}
const zrobione = zadania.filter((z) => z.zrobione).length;
document.getElementById('podsumowanie').textContent = `Zadań: ${zadania.length}, wykonanych: ${zrobione}`;
}
function aktualizuj(nowyStan) {
zadania = nowyStan;
zapisz(KLUCZ, zadania);
renderuj();
}
function dodaj(tekst) {
aktualizuj([...zadania, { id: Date.now(), tekst, zrobione: false }]);
}
function przelacz(id) {
aktualizuj(zadania.map((z) => (z.id === id ? { ...z, zrobione: !z.zrobione } : z)));
}
function usunZadanie(id) {
aktualizuj(zadania.filter((z) => z.id !== id));
}
document.getElementById('formularz').addEventListener('submit', (event) => {
event.preventDefault();
const pole = document.getElementById('nowe');
const tekst = pole.value.trim();
if (!tekst) return;
dodaj(tekst);
pole.value = '';
pole.focus();
});
renderuj();
</script>
</body>
</html>Test powtórkowy
Test powtórkowy
Błędy, JSON i localStorage: 10 pytań.
Co wypisze ten kod?
try {
console.log('1');
null.x;
console.log('2');
} catch (e) {
console.log('3');
} finally {
console.log('4');
}Co wypisze ten kod?
const dane = JSON.parse('{"a":1}');
console.log(dane.a);Co wypisze ten kod?
const o = { a: undefined, b: () => 1, c: NaN, d: 'x' };
console.log(JSON.stringify(o));Które wywołanie JSON.parse NIE rzuci błędu?
Co wypisze ten kod w przeglądarce?
localStorage.setItem('n', 7);
console.log(localStorage.getItem('n') + 1);Co zwraca localStorage.getItem('klucz'), gdy takiego klucza nie ma?
Jak poprawnie zapisać tablicę obiektów zadania w localStorage?
Które zdanie o throw jest prawdziwe?
Co wypisze ten kod?
class BladDanych extends Error {
constructor(m) { super(m); this.name = 'BladDanych'; }
}
try {
throw new BladDanych('zły format');
} catch (e) {
console.log(e instanceof Error, e.name, e.message);
}Czym różni się sessionStorage od localStorage?