JavaScript: zaawansowana semantyka i nowoczesne API (ES2023/ES2024)
Kurs semantyki JavaScriptu: koercja typów, hoisting, domknięcia, this, event loop, async/await, prototypy, klasy oraz nowoczesne API ES2023/2024 dla praktykujących inżynierów.
JavaScript: zaawansowana semantyka i nowoczesne API (ES2023/ES2024)
Kurs semantyki JavaScriptu: koercja typów, hoisting, domknięcia, this, event loop, async/await, prototypy, klasy oraz nowoczesne API ES2023/2024 dla praktykujących inżynierów.
Równość, koercja typów i podstawy wartości
na egzaminieOperator == wykonuje niejawną konwersję typów przed porównaniem (abstract equality comparison), podczas gdy === porównuje wartość i typ bez konwersji (strict equality comparison). W kodzie produkcyjnym === jest domyślnym wyborem; == rezerwuje się dla świadomych, udokumentowanych przypadków.
Koercja typów (type coercion): Automatyczna konwersja wartości z jednego typu na inny, którą silnik JS wykonuje np. w porównaniach, operacjach arytmetycznych i konkatenacji stringów. Kierują nią algorytmy ToPrimitive, ToNumber i ToString ze specyfikacji ECMA-262.
| Porównanie | == | === | Komentarz |
|---|---|---|---|
| 0 == '' | true | false | '' konwertowane do 0 |
| null == undefined | true | false | specjalny przypadek w spec |
| NaN == NaN | false | false | NaN nigdy nie jest równe samemu sobie |
| [] == false | true | false | [] → '' → 0, false → 0 |
| '0' == false | true | false | oba konwertowane do 0 |
console.log(0 == ''); // true
console.log(null == undefined); // true
console.log(NaN == NaN); // false
console.log([] == false); // true
console.log('0' == false); // true
console.log(0 === ''); // false Porównywanie z NaN operatorem == lub === zawsze zwraca false, także dla NaN == NaN. Do sprawdzania NaN używaj Number.isNaN(x), nigdy x == NaN ani globalnego isNaN(x) (który najpierw koercjuje argument do liczby i daje fałszywe pozytywy, np. dla isNaN('foo')).
Object.is(): Metoda porównania wartości bez koercji, zachowująca się jak === z dwoma wyjątkami: Object.is(NaN, NaN) zwraca true, a Object.is(0, -0) zwraca false. Używana głównie wewnętrznie przez silnik oraz w edge-case'ach numerycznych.
Object.is(NaN, NaN); // true
NaN === NaN; // false
Object.is(0, -0); // false
0 === -0; // true - ToPrimitive: obiekt konwertowany jest do prymitywu (najpierw valueOf, potem toString, chyba że kontekst preferuje string).
- ToNumber: string, boolean i null/undefined konwertowane są do liczby wg ściśle zdefiniowanych reguł.
- Jeśli oba operandy == są tego samego typu, zachowuje się identycznie jak ===.
- null == undefined jest jedynym przypadkiem, w którym null porównuje się jako równe czemukolwiek innemu niż samo siebie.
Zapamiętaj: '3 znaki równości = 3 rzeczy sprawdzane: typ i wartość, zero niespodzianek'. Jeśli piszesz ==, dopisz komentarz dlaczego.
typeof null zwraca 'object' (błąd historyczny w JS z 1995 roku, niemożliwy do naprawienia bez złamania zgodności wstecznej). Dla function typeof zwraca 'function', mimo że funkcje są obiektami. Dla niezadeklarowanej zmiennej typeof nie rzuca błędu, zwraca 'undefined'.
Hoisting i Temporal Dead Zone
na egzaminieHoisting to mechanizm, w którym deklaracje zmiennych i funkcji są rejestrowane na górze swojego zakresu podczas fazy tworzenia środowiska wykonania, przed wykonaniem kodu.
Przy hoistingu deklaracje var, let, const i function zachowują się zupełnie inaczej. To częste źródło błędów w code review.
Hoisting: Faza tworzenia środowiska wykonania (creation phase), w której silnik rejestruje wszystkie deklaracje w bieżącym zakresie, zanim wykona jakikolwiek kod.
- Function declarations są hoistowane wraz z całym ciałem funkcji
- var jest hoistowane z wartością początkową undefined
- let/const są hoistowane, ale pozostają w Temporal Dead Zone aż do miejsca deklaracji
Temporal Dead Zone (TDZ): Okres między wejściem w zakres blokowy a wykonaniem deklaracji let/const, w którym zmienna istnieje, ale jej odczyt lub zapis rzuca ReferenceError. TDZ wymusza deklarację przed użyciem i wyłapuje błędy kolejności.
| Deklaracja | Hoistowana? | Wartość przed deklaracją | Błąd przy dostępie |
|---|---|---|---|
| var x | tak | undefined | brak |
| let x / const x | tak (do TDZ) | brak dostępu | ReferenceError |
| function f(){} | tak, w całości | cała funkcja dostępna | brak |
| class C {} | tak (do TDZ) | brak dostępu | ReferenceError |
console.log(typeof aVar); // 'undefined' - var jest hoistowane
console.log(typeof aLet); // ReferenceError: Cannot access 'aLet' before initialization
var aVar = 1;
let aLet = 2; Częsty błąd: założenie, że let/const 'nie są hoistowane w ogóle'. W rzeczywistości są hoistowane identycznie jak var. Silnik blokuje jednak dostęp do nich aż do linii deklaracji (TDZ), co objawia się jako ReferenceError, a nie undefined.
console.log(declared()); // działa: 'hoisted'
console.log(expr()); // TypeError: expr is not a function
function declared() { return 'hoisted'; }
var expr = function() { return 'not hoisted'; }; - Silnik skanuje zakres i rejestruje wszystkie deklaracje function, var, let, const, class.
- Function declarations otrzymują od razu pełną wartość (referencję do funkcji).
- var otrzymuje wartość undefined.
- let, const i class trafiają do TDZ, bez przypisanej wartości.
- Wykonanie kodu liniowego rozpoczyna się, TDZ kończy się w linii z deklaracją.
TDZ = 'zadeklarowane, ale zablokowane'. var = 'zadeklarowane i puste (undefined)'. function = 'zadeklarowane i gotowe'.
Deklaracje class są hoistowane tak jak let/const: trafiają do TDZ. Próba użycia klasy przed jej deklaracją rzuca ReferenceError, w przeciwieństwie do starszych function-based konstruktorów, które można było wywołać przed deklaracją dzięki pełnemu hoistingowi function.
Domknięcia (closures) i zakres leksykalny
na egzaminieDomknięcie (closure) powstaje, gdy funkcja 'zapamiętuje' zmienne ze swojego zakresu leksykalnego (miejsca zdefiniowania), nawet po tym, jak zakres zewnętrzny zakończył wykonanie. To fundament modułów, memoizacji, prywatnego stanu i większości wzorców funkcyjnych w JS.
Domknięcie (closure): Kombinacja funkcji wraz z referencjami do jej otaczającego stanu leksykalnego (lexical environment). Silnik JS utrzymuje przy życiu zmienne z zewnętrznego zakresu tak długo, jak długo istnieje jakiekolwiek domknięcie, które się do nich odwołuje.
function createCounter() {
let count = 0;
return {
increment: () => ++count,
value: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.value()); // 2 Klasyczna pułapka: pętla for z var i setTimeout wewnątrz loguje ostatnią wartość licznika dla każdej iteracji. Powód: var ma zasięg funkcji, więc wszystkie callbacki współdzielą tę samą zmienną.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// wypisuje: 3, 3, 3 for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// wypisuje: 0, 1, 2 - Enkapsulacja prywatnego stanu (module revealing pattern) bez klas.
- Memoizacja: cache wyników funkcji trzymany w domknięciu między wywołaniami.
- Fabryki funkcji (curry, partial application) z zamrożonymi argumentami.
- Debounce/throttle: domknięcie przechowuje id timera i timestamp między wywołaniami.
function memoize(fn) {
const cache = new Map();
return (arg) => {
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg);
cache.set(arg, result);
return result;
};
} Pętla z var (błąd)
- Jedna wspólna zmienna i dla wszystkich iteracji
- setTimeout loguje wartość po zakończeniu pętli
- Typowy wynik: 3, 3, 3
Pętla z let (poprawnie)
- Nowe wiązanie i tworzone przy każdej iteracji
- Każde domknięcie łapie własną kopię i
- Wynik: 0, 1, 2
Domknięcie utrzymuje przy życiu cały leksykalny scope, w którym powstało, nie tylko zmienne faktycznie używane. Silniki V8 częściowo to optymalizują, ale nie należy na tym polegać.
Długo żyjące domknięcia trzymające referencje do dużych obiektów lub węzłów DOM są jedną z głównych przyczyn wycieków pamięci w aplikacjach SPA.
Domknięcie = plecak: funkcja nosi ze sobą zmienne z miejsca urodzenia, gdziekolwiek zostanie wywołana.
`this`, prototypy i klasy pod maską
na egzaminieWartość this nie jest ustalana w miejscu zdefiniowania funkcji (poza arrow functions), lecz w miejscu jej wywołania (call site). To najczęstsze źródło bugów przy przekazywaniu metod jako callbacki.
| Forma wywołania | Wartość this |
|---|---|
| obj.method() | obj (odbiornik wywołania) |
| function() w strict mode (samodzielnie) | undefined |
| function() w sloppy mode (samodzielnie) | globalThis (np. window) |
| Arrow function | this z otaczającego zakresu leksykalnego (nie zmienia się nigdy) |
| new Ctor() | nowo utworzony obiekt |
| fn.call(ctx) / fn.apply(ctx) / fn.bind(ctx) | jawnie podany ctx |
| Metoda w klasie | instancja, na której wywołano metodę (strict mode zawsze) |
const obj = {
value: 42,
regular: function () { return this.value; },
arrow: () => this?.value,
};
console.log(obj.regular()); // 42
console.log(obj.arrow()); // undefined - this to zakres modułu, nie obj Przekazanie metody jako referencji (np. setTimeout(obj.method, 1000) albo button.addEventListener('click', obj.method)) odrywa metodę od obiektu. Wewnątrz method this przestaje wskazywać na obj, bo o wartości this decyduje sposób wywołania, nie miejsce definicji.
class Button {
label = 'OK';
handleClick = () => console.log(this.label); // this związane raz, przy tworzeniu instancji
}
const b = new Button();
const el = document.querySelector('button');
el.addEventListener('click', b.handleClick); // działa poprawnie Łańcuch prototypów (prototype chain): Mechanizm dziedziczenia w JS: każdy obiekt ma wewnętrzną referencję [[Prototype]] (dostępną przez Object.getPrototypeOf) do innego obiektu. Odczyt właściwości, której nie ma na obiekcie, przechodzi w górę łańcucha aż do Object.prototype lub null.
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} robi dźwięk`; }
}
// odpowiednik pod maską:
function AnimalFn(name) { this.name = name; }
AnimalFn.prototype.speak = function () {
return `${this.name} robi dźwięk`;
}; Metody zdefiniowane w class są nieenumerowalne (nie pojawiają się w for...in ani Object.keys na prototypie), w przeciwieństwie do metod dopisanych ręcznie do prototype.
Deklaracje class działają zawsze w strict mode i muszą być wywoływane z new (wywołanie bez new rzuca TypeError). Funkcje konstruktorów tego nie wymuszają.
- extends ustawia [[Prototype]] klasy potomnej na klasę bazową przez Object.setPrototypeOf.
- super() musi zostać wywołane przed użyciem this w konstruktorze klasy potomnej.
- Klasy są hoistowane do TDZ (jak let/const), function constructors są hoistowane w pełni.
- Pola instancji (class fields) są przypisywane na początku konstruktora, przed jego resztą ciała.
this = kto zadzwonił, nie kto napisał numer. Arrow function nie odbiera telefonu, tylko podaje dalej ten sam numer co rodzic.
Event loop: microtaski vs makrotaski
na egzaminieJavaScript jest single-threaded, ale event loop pozwala obsługiwać operacje asynchroniczne bez blokowania wątku głównego. Kluczowe jest rozróżnienie microtask queue i macrotask (task) queue oraz zasada: po każdym pojedynczym makrotasku silnik opróżnia CAŁĄ kolejkę microtasków, zanim przejdzie dalej.
Call stack: Struktura LIFO, w której silnik śledzi aktualnie wykonywane wywołania funkcji. Kod synchroniczny wykonuje się w całości, zanim event loop przejdzie do kolejek zadań.
Microtask queue: Kolejka o wyższym priorytecie zasilana przez: callbacki .then/.catch/.finally Promise, queueMicrotask() oraz wznowienia po await. Cała kolejka microtasków jest opróżniana (łącznie z microtaskami dodanymi w trakcie jej opróżniania) przed przejściem do kolejnego makrotasku lub renderowania.
Macrotask (task) queue: Kolejka zasilana przez setTimeout, setInterval, I/O, eventy UI i (w przeglądarce) requestAnimationFrame w osobnej kategorii. Po każdym pojedynczym makrotasku silnik wraca do opróżnienia microtask queue, dopiero potem bierze kolejny makrotask.
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise 1'));
queueMicrotask(() => console.log('queueMicrotask'));
Promise.resolve().then(() => console.log('promise 2'));
console.log('end');
// kolejność: start, end, promise 1, queueMicrotask, promise 2, timeout - 1. Kod synchroniczny 'start' i 'end' logują się od razu, call stack musi się opróżnić zanim ruszy event loop.
- 2. Opróżnienie microtask queue 'promise 1', 'queueMicrotask', 'promise 2' wykonują się w kolejności FIFO dodania do kolejki, całość przed jakimkolwiek makrotaskiem.
- 3. Pierwszy makrotask Dopiero teraz silnik bierze callback z setTimeout i loguje 'timeout', mimo delay 0.
| Źródło | Typ kolejki |
|---|---|
| Promise.then/catch/finally | microtask |
| queueMicrotask() | microtask |
| await (wznowienie po nim) | microtask |
| setTimeout / setInterval | macrotask |
| setImmediate (Node.js) | macrotask (po fazie I/O) |
| Zdarzenia DOM (click, input) | macrotask |
| MutationObserver | microtask |
setTimeout(fn, 0) nie oznacza 'wykonaj natychmiast' - oznacza 'wykonaj jak najszybciej po opróżnieniu obecnego call stacka i wszystkich microtasków'. Kod z Promise zawsze wykona się przed setTimeout(fn, 0) zaplanowanym wcześniej w tej samej turze.
W Node.js process.nextTick() ma wyższy priorytet niż microtaski Promise: cała kolejka nextTick jest opróżniana przed przejściem do microtask queue. To zachowanie specyficzne dla Node, nieobecne w specyfikacji przeglądarkowej ECMA-262/HTML.
Microtask = VIP przy wejściu, wchodzi przed każdym kolejnym makrotaskiem. Makrotask = zwykła kolejka, jeden na turę.
async function example() {
console.log('A');
await null; // wymusza wznowienie jako microtask
console.log('B');
}
example();
console.log('C');
// kolejność: A, C, B async/await i kombinatory Promise
na egzaminieasync/await to składniowy cukier nad Promise, ułatwiający czytanie kodu asynchronicznego jako sekwencji kroków, przy zachowaniu w pełni asynchronicznej, nieblokującej semantyki pod spodem.
async function fetchUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('Nie udało się pobrać użytkownika', err);
throw err; // propaguj dalej, nie połykaj cicho
}
} Sekwencyjne await na niezależnych operacjach niepotrzebnie serializuje czas wykonania. Kod await fetchA(); await fetchB(); czeka na zakończenie A, zanim W OGÓLE zacznie B. Obie operacje mogłyby biec równolegle.
// źle: sumaryczny czas obu operacji, sekwencyjnie
const a = await fetchA();
const b = await fetchB();
// dobrze: obie operacje startują równolegle
const [a2, b2] = await Promise.all([fetchA(), fetchB()]); | Metoda | Kiedy się rozstrzyga | Wynik przy sukcesie | Wynik przy porażce |
|---|---|---|---|
| Promise.all | gdy wszystkie fulfilled, lub od razu przy pierwszym rejected | tablica wartości w oryginalnej kolejności | odrzucenie z powodem pierwszego rejected |
| Promise.allSettled | gdy wszystkie się rozstrzygną (fulfilled lub rejected) | tablica {status, value} lub {status, reason} | nigdy się nie odrzuca |
| Promise.race | gdy pierwsza się rozstrzygnie (fulfilled lub rejected) | wartość pierwszej rozstrzygniętej | powód pierwszej odrzuconej, jeśli to ona wygra wyścig |
| Promise.any | gdy pierwsza zostanie fulfilled | wartość pierwszej fulfilled | AggregateError, gdy wszystkie rejected |
try {
const first = await Promise.any([
fetch('/mirror-a'),
fetch('/mirror-b'),
]);
} catch (err) {
console.error(err instanceof AggregateError); // true, gdy oba mirrory padły
console.error(err.errors); // tablica wszystkich powodów odrzucenia
} const results = await Promise.allSettled([fetchA(), fetchB()]);
// [{status:'fulfilled', value: ...}, {status:'rejected', reason: ...}]
const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value); - try/catch wokół await łapie zarówno błędy synchroniczne, jak i odrzucenia promisów.
- Niezłapane odrzucenie promisa emituje event unhandledrejection (przeglądarka) lub 'unhandledRejection' (Node) - zawsze dodawaj listener w aplikacjach produkcyjnych.
- Promise.all fail-fast: jedno odrzucenie przerywa oczekiwanie na resztę, nawet jeśli reszta i tak wykona się w tle.
- Do równoległych operacji, gdzie porażka części jest akceptowalna, używaj allSettled, nie all.
Nawet return 42 wewnątrz funkcji async faktycznie zwraca Promise.resolve(42) do wywołującego. Rzucenie wyjątku (throw) wewnątrz async function jest równoważne zwróceniu odrzuconego promisa, nie przerywa programu synchronicznie.
all = wszystko albo nic (fail-fast). allSettled = raport ze wszystkiego. race = kto pierwszy dobiegnie (dobry lub zły). any = pierwszy dobry wygrywa.
Nowoczesne API: optional chaining, nullish coalescing, nowe metody Array/Object
ES2020-ES2024 wprowadziły grupę drobnych, ale codziennie używanych usprawnień. Są to: bezpieczny dostęp do zagnieżdżonych właściwości, precyzyjniejsza obsługa wartości domyślnych, niemutujące warianty klasycznych metod tablicowych oraz naturalne grupowanie danych bez bibliotek typu lodash.
const city = user?.address?.city ?? 'nieznane';
const total = config?.limits?.max ?? 100;
user?.notify?.(); // wywołanie opcjonalnej metody, bez rzucania jeśli notify nie istnieje ?? różni się od || tym, że zatrzymuje się TYLKO na null lub undefined, nie na innych falsy wartościach. Wyrażenie count || 10 zwróci 10 nawet gdy count wynosi 0, co jest zwykle błędem. Wyrażenie count ?? 10 poprawnie zwróci 0.
const count = 0;
console.log(count || 10); // 10 - błąd logiczny, 0 jest falsy
console.log(count ?? 10); // 0 - poprawne, 0 nie jest null/undefined const arr = [10, 20, 30];
console.log(arr.at(-1)); // 30
console.log(arr[arr.length - 1]); // 30, ten sam wynik, mniej czytelnie const nums = [3, 1, 2];
const sorted = nums.toSorted(); // [1, 2, 3], nums bez zmian
const reversed = nums.toReversed(); // [2, 1, 3], nums bez zmian
const replaced = nums.with(0, 99); // [99, 1, 2], nums bez zmian
console.log(nums); // [3, 1, 2] - oryginał nietknięty const inventory = [
{ name: 'apple', type: 'fruit' },
{ name: 'carrot', type: 'vegetable' },
{ name: 'banana', type: 'fruit' },
];
const byType = Object.groupBy(inventory, item => item.type);
// { fruit: [apple, banana], vegetable: [carrot] } - obiekt bez prototypu const original = { date: new Date(), items: new Map([['a', 1]]) };
const copy = structuredClone(original);
copy.items.set('b', 2);
console.log(original.items.has('b')); // false - niezależna kopia | Mutująca (oryginał) | Niemutująca (ES2023) |
|---|---|
| sort() | toSorted() |
| reverse() | toReversed() |
| splice() | toSpliced() |
| arr[i] = x | with(i, x) |
Funkcje i węzły DOM rzucają DataCloneError. Prototyp obiektu nie jest zachowywany - instancja klasy po sklonowaniu przestaje być instanceof tej klasy. Gettery/settery i property descriptory są tracone, w kopii stają się zwykłymi właściwościami read/write.
?? = 'tylko null/undefined mnie zatrzyma'. || = 'każdy falsy mnie zatrzyma'. to* (toSorted, toReversed, toSpliced) = 'zwracam nowość, oryginał zostaje'.
Moduły ESM vs CommonJS oraz wycieki pamięci
na egzaminieECMAScript Modules (ESM, import/export) i CommonJS (require/module.exports) różnią się nie tylko składnią, ale fundamentalną semantyką ładowania. ESM analizuje kod statycznie i asynchronicznie, CommonJS wykonuje go dynamicznie i synchronicznie. Interop między nimi to częste źródło błędów w Node.js.
ESM (import/export)
- Statyczna struktura, analizowana przed wykonaniem (umożliwia tree-shaking)
- Bindingi żywe: zaimportowana wartość aktualizuje się przy zmianie w module źródłowym
- Domyślnie strict mode, wspiera top-level await
- Ładowanie może być asynchroniczne (fetch modułów w przeglądarce)
CommonJS (require/module.exports)
- Dynamiczne wywołanie require() w dowolnym miejscu kodu, w tym warunkowo
- Eksport to kopia wartości w momencie require (dla prymitywów)
- Synchroniczne ładowanie, blokujące I/O
- Brak top-level await, brak natywnego tree-shakingu
// utils.mjs
export const PI = 3.14159;
export function double(x) { return x * 2; }
export default class Calculator {}
// main.mjs
import Calculator, { PI, double } from './utils.mjs'; // utils.cjs
const PI = 3.14159;
function double(x) { return x * 2; }
module.exports = { PI, double };
// main.cjs
const { PI, double } = require('./utils.cjs'); W starszych Node.js (<22.12) require() nie mógł ładować ESM - trzeba było użyć dynamicznego import(). Od Node 22.12+/23 require() domyślnie potrafi synchronicznie załadować moduł ESM, o ile ten nie używa top-level await.
Dla modułów z top-level await nadal wymagany jest dynamiczny import(). Import CommonJS z modułu ESM działa: module.exports trafia jako default export. Node dodatkowo próbuje wystawić named exports przez statyczną analizę (cjs-module-lexer), co nie zawsze wykrywa dynamiczne przypisania.
- Zapomniane event listenery: addEventListener bez odpowiadającego removeEventListener trzyma referencję do całego zamknięcia i elementu.
- Odłączone (detached) węzły DOM: element usunięty z drzewa DOM, ale wciąż referencjonowany przez zmienną JS lub domknięcie, nie może zostać zwolniony przez garbage collector.
- Timery bez czyszczenia: setInterval/setTimeout przechowujące w domknięciu referencje do dużych obiektów, nigdy niewyczyszczone przez clearInterval/clearTimeout.
- Rosnące globalne cache/Map bez limitu rozmiaru lub strategii eksmisji (np. LRU), akumulujące dane przez cały czas życia aplikacji.
let cachedRows = [];
function renderTable(rows) {
document.querySelector('#table').replaceChildren(...rows);
cachedRows.push(...rows); // wyciek: każde wywołanie dokłada wiersze, stare zostają w tablicy po usunięciu z DOM
}
// naprawa: nie trzymaj referencji dłużej niż potrzeba
function renderTableFixed(rows) {
document.querySelector('#table').replaceChildren(...rows);
// brak długożyjącej referencji poza zakresem funkcji
} Weźmy listener na długo żyjącym obiekcie (np. window, document, EventEmitter singleton), z callbackiem-domknięciem, które przechwytuje duży obiekt lokalny. Taki listener utrzymuje ten obiekt przy życiu tak długo, jak sam istnieje, nawet jeśli komponent, który go dodał, dawno zniknął z UI.
- 1. Zrób heap snapshot Chrome DevTools → Memory → Heap snapshot, przed i po scenariuszu podejrzanym o wyciek.
- 2. Wymuś GC i porównaj Kliknij 'Collect garbage', zrób drugi snapshot, użyj widoku 'Comparison' do zobaczenia obiektów, które przybyły i nie zostały zwolnione.
- 3. Szukaj Detached Filtruj po 'Detached' w widoku Containment - to węzły DOM usunięte z drzewa, ale nadal w pamięci.
- 4. Prześledź retainers Panel 'Retainers' pokazuje dokładny łańcuch referencji (closure → zmienna → obiekt) trzymający obiekt przy życiu - tam jest źródło wycieku.
Wyciek pamięci w JS to zawsze 'ktoś wciąż trzyma referencję'. Znajdź trzymającego (retainer), nie ofiarę.
Sprawdź się
20 z 24 pytań, losowo, z wyjaśnieniem po każdym. Wynik zostaje na tym urządzeniu.
Rozpocznij quiz