Przejdź do treści
← wszystkie przedmioty

JavaScript: zaawansowana semantyka i nowoczesne API (ES2023/ES2024)

Dogłębny 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.

TEMATY EGZAMINACYJNE W TYM PRZEDMIOCIE
  • == vs === i mechanizm koercji typów
  • Hoisting oraz Temporal Dead Zone (let/const/var)
  • Dokładna kolejność microtasków i makrotasków w event loop
  • `this` w function declarations, arrow functions, metodach i klasach
  • Promise.all/allSettled/race/any oraz obsługa błędów w async/await
  • Domknięcia jako źródło wycieków pamięci w długożyjących aplikacjach

1 · Równość, koercja typów i podstawy wartości

★ egzamin

Operator == 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.

Termin kluczowy
Koercja typów (type coercion) - Automatyczna konwersja wartości z jednego typu na inny wykonywana przez silnik JS w kontekstach takich jak porównania, operacje arytmetyczne czy konkatenacja stringów. Kierowana algorytmami ToPrimitive, ToNumber i ToString ze specyfikacji ECMA-262.
Porównanie=====Komentarz
0 == ''truefalse'' konwertowane do 0
null == undefinedtruefalsespecjalny przypadek w spec
NaN == NaNfalsefalseNaN nigdy nie jest równe samemu sobie
[] == falsetruefalse[] → '' → 0, false → 0
'0' == falsetruefalseoba konwertowane do 0
Pułapki == w praktyce javascript
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
Pułapka
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')).
Termin kluczowy
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 vs === javascript
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.
Mnemonik
Zapamiętaj: '3 znaki równości = 3 rzeczy sprawdzane: typ i wartość, zero niespodzianek'. Jeśli piszesz ==, dopisz komentarz dlaczego.
typeof - pułapki
typeof null zwraca 'object' (błąd historyczny w JS z 1995 roku, niemożliwy do naprawienia bez złamania zgodności wstecznej). typeof function zwraca 'function', mimo że funkcje są obiektami. typeof niezadeklarowanej zmiennej nie rzuca błędu, zwraca 'undefined'.

2 · Hoisting i Temporal Dead Zone

★ egzamin

Hoisting to mechanizm, w którym deklaracje zmiennych i funkcji są rejestrowane na górze swojego zakresu podczas fazy tworzenia środowiska wykonania, przed wykonaniem kodu. var, let, const i function zachowują się przy tym zupełnie inaczej, co jest częstym źródłem błędów w code review.

Termin kluczowy
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.
Termin kluczowy
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.
DeklaracjaHoistowana?Wartość przed deklaracjąBłąd przy dostępie
var xtakundefinedbrak
let x / const xtak (do TDZ)brak dostępuReferenceError
function f(){}tak, w całościcała funkcja dostępnabrak
class C {}tak (do TDZ)brak dostępuReferenceError
TDZ w praktyce javascript
console.log(typeof aVar);   // 'undefined' - var jest hoistowane
console.log(typeof aLet);   // ReferenceError: Cannot access 'aLet' before initialization

var aVar = 1;
let aLet = 2;
Pułapka
Częsty błąd: założenie, że let/const 'nie są hoistowane w ogóle'. W rzeczywistości są hoistowane identycznie jak var, ale silnik blokuje dostęp do nich aż do linii deklaracji (TDZ), co objawia się jako ReferenceError, a nie undefined.
Function declaration vs function expression javascript
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'; };
  1. 1 Silnik skanuje zakres i rejestruje wszystkie deklaracje function, var, let, const, class.
  2. 2 Function declarations otrzymują od razu pełną wartość (referencję do funkcji).
  3. 3 var otrzymuje wartość undefined.
  4. 4 let, const i class trafiają do TDZ, bez przypisanej wartości.
  5. 5 Wykonanie kodu liniowego rozpoczyna się, TDZ kończy się w linii z deklaracją.
Mnemonik
TDZ = 'zadeklarowane, ale zablokowane'. var = 'zadeklarowane i puste (undefined)'. function = 'zadeklarowane i gotowe'.
Class hoisting
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.

3 · Domknięcia (closures) i zakres leksykalny

★ egzamin

Domknię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.

Termin kluczowy
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.
Klasyczny licznik przez domknięcie javascript
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    value: () => count,
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.value()); // 2
Pułapka
Klasyczna pułapka: pętla for z var i setTimeout wewnątrz loguje ostatnią wartość licznika dla każdej iteracji, bo var ma zasięg funkcji, więc wszystkie callbacki współdzielą tę samą zmienną.
Pułapka var w pętli asynchronicznej javascript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// wypisuje: 3, 3, 3
Poprawka: let tworzy nowe wiązanie na iterację javascript
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.
Memoizacja przez domknięcie javascript
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
Koszt pamięciowy
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.
Mnemonik
Domknięcie = plecak: funkcja nosi ze sobą zmienne z miejsca urodzenia, gdziekolwiek zostanie wywołana.

4 · `this`, prototypy i klasy pod maską

★ egzamin

Wartość 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łaniaWartość this
obj.method()obj (odbiornik wywołania)
function() w strict mode (samodzielnie)undefined
function() w sloppy mode (samodzielnie)globalThis (np. window)
Arrow functionthis 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 klasieinstancja, na której wywołano metodę (strict mode zawsze)
Arrow function dziedziczy this leksykalnie javascript
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
Pułapka
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.
Naprawa przez arrow function jako pole klasy javascript
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
Termin kluczowy
Ł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 jako cukier składniowy nad prototypami javascript
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`;
};
Różnice class vs function constructor
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), czego funkcje konstruktorów 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.
Mnemonik
this = kto zadzwonił, nie kto napisał numer. Arrow function nie odbiera telefonu, tylko podaje dalej ten sam numer co rodzic.

5 · Event loop: microtaski vs makrotaski

★ egzamin

JavaScript 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.

Termin kluczowy
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ń.
Termin kluczowy
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.
Termin kluczowy
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.
Dokładna kolejność wykonania javascript
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. 1 1. Kod synchroniczny 'start' i 'end' logują się od razu, call stack musi się opróżnić zanim ruszy event loop.
  2. 2 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. 3 3. Pierwszy makrotask Dopiero teraz silnik bierze callback z setTimeout i loguje 'timeout', mimo delay 0.
ŹródłoTyp kolejki
Promise.then/catch/finallymicrotask
queueMicrotask()microtask
await (wznowienie po nim)microtask
setTimeout / setIntervalmacrotask
setImmediate (Node.js)macrotask (po fazie I/O)
Zdarzenia DOM (click, input)macrotask
MutationObservermicrotask
Pułapka
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.
process.nextTick w Node.js
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.
Mnemonik
Microtask = VIP przy wejściu, wchodzi przed każdym kolejnym makrotaskiem. Makrotask = zwykła kolejka, jeden na turę.
async/await to cukier nad microtaskami javascript
async function example() {
  console.log('A');
  await null; // wymusza wznowienie jako microtask
  console.log('B');
}
example();
console.log('C');
// kolejność: A, C, B

6 · async/await i kombinatory Promise

★ egzamin

async/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.

Podstawowy wzorzec try/catch javascript
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
  }
}
Pułapka
Sekwencyjne await na niezależnych operacjach niepotrzebnie serializuje czas wykonania: await fetchA(); await fetchB(); czeka na zakończenie A, zanim W OGÓLE zacznie B, mimo że mogłyby biec równolegle.
Naprawa: równoległość przez Promise.all javascript
// ź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()]);
MetodaKiedy się rozstrzygaWynik przy sukcesieWynik przy porażce
Promise.allgdy wszystkie fulfilled, lub od razu przy pierwszym rejectedtablica wartości w oryginalnej kolejnościodrzucenie z powodem pierwszego rejected
Promise.allSettledgdy wszystkie się rozstrzygną (fulfilled lub rejected)tablica {status, value} lub {status, reason}nigdy się nie odrzuca
Promise.racegdy pierwsza się rozstrzygnie (fulfilled lub rejected)wartość pierwszej rozstrzygniętejpowód pierwszej odrzuconej, jeśli to ona wygra wyścig
Promise.anygdy pierwsza zostanie fulfilledwartość pierwszej fulfilledAggregateError, gdy wszystkie rejected
Promise.any i AggregateError javascript
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
}
Promise.allSettled - żaden wynik nie ginie javascript
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.
async function zawsze zwraca Promise
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.
Mnemonik
all = wszystko albo nic (fail-fast). allSettled = raport ze wszystkiego. race = kto pierwszy dobiegnie (dobry lub zły). any = pierwszy dobry wygrywa.

7 · Nowoczesne API: optional chaining, nullish coalescing, nowe metody Array/Object

ES2020-ES2024 wprowadziły grupę drobnych, ale codziennie używanych usprawnień: bezpieczny dostęp do zagnieżdżonych właściwości, precyzyjniejszą obsługę wartości domyślnych, niemutujące warianty klasycznych metod tablicowych oraz naturalne grupowanie danych bez bibliotek typu lodash.

Optional chaining + nullish coalescing javascript
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
Pułapka
?? różni się od || tym, że zatrzymuje się TYLKO na null lub undefined, nie na innych falsy wartościach. count || 10 zwróci 10 nawet gdy count wynosi 0, co jest zwykle błędem; count ?? 10 poprawnie zwróci 0.
?? vs || na wartości 0 javascript
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
Array.prototype.at - indeksowanie od końca javascript
const arr = [10, 20, 30];
console.log(arr.at(-1));          // 30
console.log(arr[arr.length - 1]); // 30, ten sam wynik, mniej czytelnie
Niemutujące metody tablicowe (ES2023) javascript
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
Object.groupBy (ES2024) javascript
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
structuredClone dla głębokiego kopiowania javascript
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] = xwith(i, x)
Czego structuredClone nie sklonuje
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.
Mnemonik
?? = 'tylko null/undefined mnie zatrzyma'. || = 'każdy falsy mnie zatrzyma'. to* (toSorted, toReversed, toSpliced) = 'zwracam nowość, oryginał zostaje'.

8 · Moduły ESM vs CommonJS oraz wycieki pamięci

★ egzamin

ECMAScript Modules (ESM, import/export) i CommonJS (require/module.exports) różnią się nie tylko składnią, ale fundamentalną semantyką ładowania: statyczną i asynchroniczną analizę w ESM kontra dynamiczne, synchroniczne wykonanie w CommonJS. 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
Składnia ESM javascript
// 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';
Składnia CommonJS javascript
// utils.cjs
const PI = 3.14159;
function double(x) { return x * 2; }
module.exports = { PI, double };

// main.cjs
const { PI, double } = require('./utils.cjs');
Interop CJS ⇄ ESM w Node.js
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, a 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.
Detached DOM node - wyciek i naprawa javascript
let cachedRows = [];

function renderTable(rows) {
  document.querySelector('#table').replaceChildren(...rows);
  cachedRows = rows; // wyciek: stare rows nadal referencjonowane 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
}
Pułapka
Listener dodany na długo żyjącym obiekcie (np. window, document, EventEmitter singleton) z callbackiem-domknięciem, które przechwytuje duży obiekt lokalny, utrzymuje ten obiekt przy życiu tak długo, jak długo istnieje listener, nawet jeśli komponent, który go dodał, dawno zniknął z UI.
  1. 1 1. Zrób heap snapshot Chrome DevTools → Memory → Heap snapshot, przed i po scenariuszu podejrzanym o wyciek.
  2. 2 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. 3 3. Szukaj Detached Filtruj po 'Detached' w widoku Containment - to węzły DOM usunięte z drzewa, ale nadal w pamięci.
  4. 4 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.
Mnemonik
Wyciek pamięci w JS to zawsze 'ktoś wciąż trzyma referencję'. Znajdź trzymającego (retainer), nie ofiarę.
Fiszki - aktywne przypominanie

Sprawdź się - testowanie to nauka

24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.

Rozpocznij quiz
Zbudowane przez Tenzan Logic