ECMAScript 2024 (ES2024): nowości w JavaScripcie z przykładami

Mateusz Bogolubow

05-11-2026

Co weszło do ES2024: Object.groupBy, Promise.withResolvers, isWellFormed, flaga v w regex, ArrayBuffer ze zmienianym rozmiarem i Atomics.waitAsync. Każda nowość z przykładem.

ECMAScript 2024 (ES2024): nowości w JavaScripcie z przykładami
ECMAScript 2024 (ES2024) to 15. wydanie standardu JavaScript, zatwierdzone w czerwcu 2024 roku. Najbardziej użyteczne nowości to grupowanie danych (Object.groupBy, Map.groupBy), Promise.withResolvers oraz metody isWellFormed i toWellFormed dla napisów. Dochodzą też flaga v w wyrażeniach regularnych, ArrayBuffer ze zmienianym rozmiarem i transfer oraz Atomics.waitAsync. W Node.js 22 działają wszystkie, a w Chromium 156 sprawdziłem wszystkie poza Atomics.waitAsync.

Co roku komitet TC39 dopisuje do JavaScriptu kilka nowych funkcji, a w czerwcu Ecma International zatwierdza je jako kolejne wydanie standardu. W tym artykule przejdziemy po wszystkich nowościach z ES2024. Każdą pokażemy na krótkim przykładzie, a tam, gdzie w serwisie jest artykuł o powiązanym temacie, dostaniesz do niego link. Cały kod uruchomiłem w przeglądarce Chromium 156 i w Node.js 22, a wyniki w komentarzach pochodzą z tych uruchomień. Nowości z kolejnych lat opisujemy w artykułach o ES2025 i ES2026.

Czym jest ES2024 i jakie nowości przynosi?

ECMAScript to oficjalna specyfikacja, na podstawie której działa JavaScript. Przeglądarki i środowiska takie jak Node.js wdrażają ją na własnym tempie. ES2024 to 15. edycja standardu ECMA-262, zatwierdzona w czerwcu 2024 roku. Do wydania trafiają propozycje, które w TC39 osiągnęły ostatni, czwarty etap (stage 4).

NowośćDo czego służy
Object.groupBy, Map.groupBygrupuje elementy tablicy według klucza
Promise.withResolverstworzy obietnicę razem z funkcjami resolve i reject
isWellFormed, toWellFormedsprawdza i naprawia napisy z uszkodzonym Unicode
Flaga v w wyrażeniach regularnychoperacje na zbiorach znaków (różnica, część wspólna)
ArrayBuffer ze zmienianym rozmiarembufor danych, który można powiększać i zmniejszać
ArrayBuffer.prototype.transferprzenosi dane do nowego bufora bez kopiowania
Atomics.waitAsyncnieblokujące czekanie na zmianę w pamięci współdzielonej

Czy mogę używać nowości z ES2024 już dziś?

Prawdopodobnie tak. Sprawdziłem dwa środowiska:

  • Chromium 156 ma wszystkie nowości z tego artykułu poza Atomics.waitAsync, której tam nie uruchomiłem. Ta funkcja wymaga pamięci współdzielonej (SharedArrayBuffer), która w przeglądarce jest dostępna tylko na stronach z odpowiednimi nagłówkami bezpieczeństwa, więc na zwykłej stronie jej nie przetestowałem.
  • Node.js 22.22.3 ma wszystkie nowości z tego artykułu.

Firefoksa, Safari i nowszych wersji Node.js nie sprawdzałem, więc zajrzyj do aktualnych tabel zgodności (np. MDN), zanim użyjesz którejś funkcji w projekcie, który ma działać w starszych przeglądarkach. Dostępność większości sprawdzisz w kodzie, np. typeof Object.groupBy === "function". Wyjątkiem jest flaga v: jeśli środowisko jej nie zna, zapis /.../v kończy się błędem składni już przy wczytaniu pliku.

Jak pogrupować dane według klucza (Object.groupBy i Map.groupBy)?

Grupowanie listy po jakiejś cechie (poziom doświadczenia, kategoria, status) to jedno z najczęstszych zadań na tablicach. Do ES2024 robiło się to przez reduce i ręczne tworzenie tablic dla każdego klucza. Teraz wystarczy jedna funkcja:

const people = [
  { name: "Ala", level: "junior" },
  { name: "Ola", level: "mid" },
  { name: "Jan", level: "junior" },
  { name: "Ewa", level: "senior" },
];

const byLevel = Object.groupBy(people, (person) => person.level);

console.log(Object.keys(byLevel)); // ["junior", "mid", "senior"]
console.log(byLevel.junior.map((p) => p.name)); // ["Ala", "Jan"]

Funkcja dostaje element i zwraca klucz grupy, a wynik to obiekt, w którym pod każdym kluczem leży tablica pasujących elementów. Wynik ma jedną ważną cechę: to obiekt bez prototypu (Object.getPrototypeOf(byLevel) zwraca null). Dzięki temu nazwy grup, takie jak toString czy constructor, nie kolidują z metodami wbudowanymi, ale też nie dostaniesz na takim obiekcie metod takich jak byLevel.toString() (w teście typeof byLevel.toString dało undefined).

Gdy klucze nie są napisami (np. liczby, obiekty) albo chcesz zachować kolejność wstawiania, użyj Map.groupBy, który zwraca mapę:

const parity = Map.groupBy([1, 2, 3, 4, 5, 6], (n) =>
  n % 2 === 0 ? "parzyste" : "nieparzyste"
);

console.log([...parity.keys()]); // ["nieparzyste", "parzyste"]
console.log(parity.get("parzyste")); // [2, 4, 6]

Kolejność kluczy wynika z kolejności pierwszego wystąpienia w danych: pierwsza liczba 1 jest nieparzysta, więc ta grupa jest pierwsza. Wcześniejsze wzorce na tablicach (reduce, filter) poznasz w artykule o metodach tablicowych, a podstawy pracy z kluczami i właściwościami w obiektach w JavaScript. Jeśli dopiero zaczynasz z listami, zacznij od tablic w JavaScript.

Jak utworzyć obietnicę i rozwiązać ją z zewnątrz (Promise.withResolvers)?

Konstruktor new Promise przyjmuje funkcję, a resolve i reject są dostępne tylko wewnątrz niej. Gdy chcesz rozwiązać obietnicę w zupełnie innym miejscu kodu (np. po zdarzeniu albo po upływie czasu), musiałeś wyciągać obie funkcje na zewnątrz ręcznie. Promise.withResolvers robi to jednym wywołaniem i zwraca obiekt z trzema polami: promise, resolve i reject.

const { promise, resolve, reject } = Promise.withResolvers();

setTimeout(() => resolve("gotowe"), 10);

console.log(await promise); // gotowe

To samo dla odrzucenia:

const d = Promise.withResolvers();
d.reject(new Error("nie wyszło"));

const message = await d.promise.catch((e) => e.message);
console.log(message); // nie wyszło

Zastosowanie jest wszędzie tam, gdzie zakończenie operacji przychodzi z innego miejsca niż jej początek: kliknięcie w przycisk „Zatwierdź” w oknie dialogowym, komunikat z WebSocket, odpowiedź z Web Workera. W testach przydaje się do kontrolowania momentu, w którym asynchroniczna funkcja ma się zakończyć. Powiązane tematy znajdziesz w artykułach o funkcjach zwrotnych (callbackach), o tym, jak uzależnić wykonanie kodu od odpowiedzi serwera, oraz o testowaniu kodu asynchronicznego.

Jak sprawdzić i naprawić napis z uszkodzonym Unicode (isWellFormed i toWellFormed)?

Napisy w JavaScripcie są zapisane w UTF-16. Znaki spoza podstawowego zakresu, np. emoji, zajmują w nim dwie jednostki (parę zastępczą, surrogate pair). Jeśli napis zawiera samotną jednostkę bez pary, jest uszkodzony i funkcje takie jak encodeURIComponent rzucają błąd. ES2024 dodaje dwie metody do pracy z takimi napisami.

const bad = "ab\uD83Dcd";   // samotna połowa pary
const good = "ab😀cd";

console.log(bad.isWellFormed());  // false
console.log(good.isWellFormed()); // true

console.log(bad.toWellFormed());  // "ab�cd" (znak zastępczy U+FFFD)

Sprawdziliśmy też, po co to w praktyce:

try {
  encodeURIComponent(bad);
} catch (e) {
  console.log(e.name); // URIError
}

console.log(encodeURIComponent(bad.toWellFormed())); // ab%EF%BF%BDcd

encodeURIComponent na uszkodzonym napisie rzuca URIError, a po naprawie przez toWellFormed zwraca poprawny zapis. Uszkodzone napisy najczęściej powstają przy ucinaniu tekstu w środku emoji, np. przez slice na surowych indeksach. Warto naprawiać je tam, gdzie tekst z zewnątrz trafia do adresu URL, nagłówka albo API. O tym, jak traktować dane przychodzące od użytkownika, piszemy w poradniku o walidacji formularza w JavaScript i w artykule o XSS.

Jak w wyrażeniu regularnym odjąć jeden zbiór znaków od drugiego (flaga v)?

Flaga v (od unicodeSets) rozszerza flagę u o operacje na zbiorach znaków w klasach [...]: różnicę (--) i część wspólną (&&). Pozwala też dopasowywać całe sekwencje emoji jako jeden znak.

// litery, ale bez małych liter łacińskich a-z
const noLower = /^[\p{L}--[a-z]]+$/v;

console.log(noLower.test("ŁÓDŹ")); // true
console.log(noLower.test("Łódź")); // false (litera d to mała litera z zakresu a-z)

// litery, które jednocześnie są w alfabecie greckim
const greek = /[\p{Letter}&&\p{Script=Greek}]/v;

console.log(greek.test("α")); // true
console.log(greek.test("a")); // false

Drugi przykład pokazuje część wspólną: znak musi być jednocześnie literą i literą greckiego alfabetu. Bez flagi v takie warunki zapisywałeś jako długie ręczne zakresy lub kilka osobnych wyrażeń.

Druga zmiana to właściwości sekwencji. Rodzina z emoji (👨‍👩‍👧) to kilka znaków sklejonych niewidzialnym łącznikiem, ale \p{RGI_Emoji} traktuje ją jako jeden znak:

const family = "👨‍👩‍👧";

console.log(/^\p{RGI_Emoji}$/v.test(family)); // true
console.log(/^[👨‍👩‍👧]$/u.test(family));        // false

// bez flagi v ta właściwość nie istnieje:
new RegExp("^\\p{RGI_Emoji}$", "u"); // SyntaxError

Warto pamiętać, że v i u to alternatywy: nie użyjesz obu naraz, a przy v niektóre znaki w klasach trzeba ucieczkować (np. -, (, )). Od ES2025 możesz dodatkowo zmieniać flagi dla fragmentu wzorca, o czym piszemy w artykule o ES2025.

Jak zmieniać rozmiar bufora danych (ArrayBuffer ze zmienianym rozmiarem)?

ArrayBuffer to blok surowych bajtów, na którym pracują tablice typowane, np. Uint8Array. Do ES2024 jego rozmiar był stały: żeby powiększyć bufor, trzeba było utworzyć nowy i skopiować dane. Teraz możesz podać maksymalny rozmiar i zmieniać go metodą resize.

const buf = new ArrayBuffer(8, { maxByteLength: 16 });
console.log(buf.byteLength, buf.resizable, buf.maxByteLength); // 8 true 16

buf.resize(12);
console.log(buf.byteLength); // 12

try {
  buf.resize(20);
} catch (e) {
  console.log(e.name); // RangeError (powyżej maxByteLength)
}

Tablica typowana utworzona na takim buforze śledzi jego rozmiar:

const view = new Uint8Array(buf);
console.log(view.length); // 12

buf.resize(16);
console.log(view.length); // 16

Bez maxByteLength bufor ma stały rozmiar jak dawniej. Przydaje się to przy wczytywaniu danych, których długość znasz dopiero w trakcie, np. przy odbieraniu plików kawałek po kawałku. O bezpiecznej obsłudze plików od użytkownika piszemy w artykule o uploadzie plików.

Jak przenieść dane do nowego bufora bez kopiowania (ArrayBuffer.transfer)?

Metoda transfer tworzy nowy ArrayBuffer z danymi starego, a stary unieważnia (detach): jego długość spada do zera i nie można już na nim pracować. Dzięki temu dane zmieniają właściciela, a nie są kopiowane. Opcjonalny argument pozwala od razu zmienić rozmiar.

const a = new Uint8Array([1, 2, 3, 4]).buffer;
const moved = a.transfer(8); // nowy bufor, 8 bajtów

console.log(moved.byteLength);                    // 8
console.log([...new Uint8Array(moved).slice(0, 4)]); // [1, 2, 3, 4]

console.log(a.detached);   // true
console.log(a.byteLength); // 0

try {
  new Uint8Array(a).fill(1);
} catch (e) {
  console.log(e.name); // TypeError (bufor został unieważniony)
}

Nowy bufor ma 8 bajtów, z czterema pierwszymi skopiowanymi, a reszta wypełniona zerami. Do tego służy też transferToFixedLength(), które zwraca bufor o stałym rozmiarze nawet wtedy, gdy oryginał był ze zmienianym. Typowe zastosowanie to przekazywanie dużych buforów między wątkami (Web Workery), gdzie kopiowanie byłoby kosztowne.

Jak czekać na zmianę w pamięci współdzielonej bez blokowania (Atomics.waitAsync)?

To najbardziej specjalistyczna nowość z listy. Atomics.wait blokuje cały wątek, więc w głównym wątku strony jest zabroniony. Atomics.waitAsync robi to samo, ale zwraca obietnicę, więc wątek pracuje dalej. Wymaga pamięci współdzielonej (SharedArrayBuffer) i jest używana w wielowątkowych programach z Web Workerami albo worker_threads. Poniższy przykład uruchomiłem w Node.js 22:

const sab = new SharedArrayBuffer(4);
const i32 = new Int32Array(sab);

const result = Atomics.waitAsync(i32, 0, 0, 50);
console.log(result.async); // true (czekamy na zmianę)

setTimeout(() => {
  Atomics.store(i32, 0, 1);
  Atomics.notify(i32, 0);
}, 10);

console.log(await result.value); // "ok"

const immediate = Atomics.waitAsync(i32, 0, 0);
console.log(immediate.async, immediate.value); // false "not-equal"

Pierwsze wywołanie czeka, aż ktoś zmieni wartość i wywoła notify, a wynik to "ok". Drugie nie czeka wcale: wartość w pamięci (1) jest już inna niż oczekiwana (0), więc wynik to od razu "not-equal". Na co dzień w aplikacjach webowych użyjesz tego rzadko. Gdy będziesz mógł obejść się bez współdzielonej pamięci, zrób to: komunikaty między wątkami (postMessage) są prostsze.

Co dalej?

Najlepszym sposobem na zapamiętanie nowości jest użycie ich w małym projekcie: zamień ręczne reduce do grupowania na Object.groupBy, a konstrukcje z wyciąganiem resolve na zewnątrz na Promise.withResolvers. Przed użyciem sprawdź, w jakich przeglądarkach i wersjach Node.js ma działać aplikacja. Kolejne wydania opisujemy w artykułach o ES2025 i ES2026.

Jeśli chcesz uporządkować cały nowoczesny JavaScript pod okiem mentora, zobacz nasz kurs Zaawansowany JS.

Konsultacja i warsztaty devmentor.pl

Masz pytania?

Partnerzy

Firmy, z którymi współpracujemy

To partnerzy odpowiedzialni za staże po naszych kursach. Organizują je dla najlepszych absolwentów.

Zobacz wszystkich partnerów →
  • VR Medical
  • Sophona
  • Robot Force