Kurs JavaScript i TypeScript · Moduł 5: Zaawansowany JavaScript

ES2024 i ES2025 - Najnowsze funkcje JavaScript

8 min czytania
W tej lekcji5

Dr. Henry Wu właśnie wrócił z konferencji programistów. "JavaScript ciągle ewoluuje," mówi. "Najnowsze wersje ECMAScript przynoszą funkcje, które znacznie uproszczą nasz kod!" Do tej pory grupowanie wymagało reduce() i pustego obiektu, a posortowanie listy bez niszczenia oryginału - ręcznej kopii. Nowe wersje standardu zamieniają takie łatki w jedną linijkę.

Jak ewoluuje ECMAScript

Nad językiem pracuje komitet TC39, a od 2015 roku nowa wersja standardu ukazuje się co roku i nosi nazwę od roku wydania. Propozycja przechodzi kolejne etapy i trafia do standardu po osiągnięciu etapu 4, który wymaga między innymi działających implementacji. Kamienie milowe tej ewolucji:

  • ES6 (ES2015) - let i const, funkcje strzałkowe, destrukturyzacja
  • ES2017 - async i await
  • ES2020 - optional chaining (?.) i nullish coalescing (??)
  • ES2023 - metody tablic bez mutacji, np. toSorted()
  • ES2024 - Object.groupBy() i Promise.withResolvers()
  • ES2025 - nowe metody Set, Promise.try() i import JSON

ES2024 - Najważniejsze nowości

1. Object.groupBy() i Map.groupBy()

Grupowanie elementów tablicy według klucza - bez bibliotek zewnętrznych! Podajesz tablicę i funkcję, która dla każdego dinozaura zwraca nazwę jego grupy:

1const dinosaurs = [
2  { name: 'Rex', type: 'carnivore', sector: 7 },
3  { name: 'Blue', type: 'carnivore', sector: 3 },
4  { name: 'Sarah', type: 'herbivore', sector: 5 },
5  { name: 'Bronty', type: 'herbivore', sector: 5 },
6  { name: 'Delta', type: 'carnivore', sector: 3 }
7];
8
9// Grupowanie po typie
10const byType = Object.groupBy(dinosaurs, dino => dino.type);
11console.log(byType);
12// {
13//   carnivore: [{ name: 'Rex', ... }, { name: 'Blue', ... }, { name: 'Delta', ... }],
14//   herbivore: [{ name: 'Sarah', ... }, { name: 'Bronty', ... }]
15// }
16
17// Grupowanie po sektorze
18const bySector = Object.groupBy(dinosaurs, dino => `Sector-${dino.sector}`);
19console.log(bySector);
20
21// Map.groupBy - gdy potrzebujesz Map zamiast obiektu
22const byTypeMap = Map.groupBy(dinosaurs, dino => dino.type);
23console.log(byTypeMap.get('carnivore'));

Każdy klucz wyniku prowadzi do tablicy pasujących dinozaurów - to samo budowaliśmy wcześniej przez reduce(). Obiekt z Object.groupBy() nie ma prototypu, więc nie odziedziczy metod w rodzaju hasOwnProperty(). Map.groupBy() wybierz, gdy kluczami mają być obiekty, a nie napisy.

2. Metody tablicowe bez mutacji (ES2023)

Formalnie te metody weszły do standardu rok wcześniej, w ES2023, ale wciąż należą do najświeższych narzędzi. Zwracają nową tablicę zamiast modyfikować oryginalną:

1const sectors = ['Sector-1', 'Sector-7', 'Sector-3', 'Sector-5'];
2
3// Stare metody MUTUJĄ oryginalną tablicę
4const oldWay = [...sectors];
5oldWay.sort(); // Zmienia oldWay!
6
7// toSorted() - sortuje bez mutacji
8const sorted = sectors.toSorted();
9console.log(sorted);   // ['Sector-1', 'Sector-3', 'Sector-5', 'Sector-7']
10console.log(sectors);  // ['Sector-1', 'Sector-7', 'Sector-3', 'Sector-5'] - niezmienione!
11
12// toReversed() - odwraca bez mutacji
13const reversed = sectors.toReversed();
14console.log(reversed); // ['Sector-5', 'Sector-3', 'Sector-7', 'Sector-1']
15console.log(sectors);  // Nadal oryginalne!
16
17// toSpliced() - splice bez mutacji
18const withNew = sectors.toSpliced(1, 1, 'Sector-9', 'Sector-10');
19console.log(withNew);  // ['Sector-1', 'Sector-9', 'Sector-10', 'Sector-3', 'Sector-5']
20
21// with() - zamień element bez mutacji
22const updated = sectors.with(1, 'Sector-ALPHA');
23console.log(updated);  // ['Sector-1', 'Sector-ALPHA', 'Sector-3', 'Sector-5']
24
25// Praktyczny przykład: sortowanie dinozaurów
26const dinoList = [
27  { name: 'Rex', dangerLevel: 10 },
28  { name: 'Blue', dangerLevel: 8 },
29  { name: 'Sarah', dangerLevel: 3 }
30];
31
32// Sortuj po poziomie zagrożenia (malejąco) bez mutacji
33const byDanger = dinoList.toSorted((a, b) => b.dangerLevel - a.dangerLevel);
34console.log(byDanger[0].name); // 'Rex'

Każda nowa metoda ma mutującą starszą siostrę: toSorted() to sort(), toReversed() to reverse(), a toSpliced() to splice(). with(index, value) zastępuje przypisanie po indeksie i rzuca RangeError dla indeksu spoza tablicy. Lista sectors po tych operacjach wygląda dokładnie tak samo jak na początku.

3. Promise.withResolvers()

Łatwiejsze tworzenie Promise z zewnętrznym resolve/reject. Przydaje się, gdy obietnicę trzeba rozstrzygnąć z innego miejsca niż jej konstruktor:

1// Stary sposób - niewygodny
2let externalResolve, externalReject;
3const oldPromise = new Promise((resolve, reject) => {
4  externalResolve = resolve;
5  externalReject = reject;
6});
7
8// Nowy sposób z Promise.withResolvers()
9const { promise, resolve, reject } = Promise.withResolvers();
10
11// Teraz możemy resolve/reject z dowolnego miejsca
12setTimeout(() => {
13  resolve('Operacja zakończona!');
14}, 1000);
15
16promise.then(console.log); // "Operacja zakończona!" po 1 sekundzie

Stary sposób wymaga zmiennych zadeklarowanych przed konstruktorem, a Promise.withResolvers() od razu zwraca obiekt z trzema polami, które rozpakowujemy destrukturyzacją. Najlepiej widać to na czujniku, który obiecuje odczyt teraz, a dostarcza go później:

1// Przydatne przy operacjach asynchronicznych sterowanych z zewnątrz
2class DinosaurSensor {
3  constructor(id) {
4    this.id = id;
5    const { promise, resolve, reject } = Promise.withResolvers();
6    this.readingPromise = promise;
7    this.resolveReading = resolve;
8    this.rejectReading = reject;
9  }
10
11  receiveReading(data) {
12    this.resolveReading(data);
13  }
14
15  sensorFailure(error) {
16    this.rejectReading(error);
17  }
18}
19
20const sensor = new DinosaurSensor("TREX-001");
21
22// Ustalamy, co się stanie, gdy przyjdzie odczyt
23sensor.readingPromise.then(reading => {
24  console.log(`Czujnik ${sensor.id}: ${JSON.stringify(reading)}`);
25});
26
27// Później, gdy odczyt dotrze:
28sensor.receiveReading({ temperature: 38.5, heartRate: 72 });

Konstruktor tworzy obietnicę i odkłada resolve oraz reject na później. Kod czekający na odczyt podpina się przez then(), a receiveReading() spełnia obietnicę dopiero wtedy, gdy przyjdą dane.

ES2025 - Najnowsze funkcje

1. Nowe metody Set

Operacje na zbiorach - wreszcie wbudowane w JavaScript! Każda metoda przyjmuje drugi zbiór i zwraca nowy Set albo wartość logiczną:

1const carnivores = new Set(['Rex', 'Blue', 'Delta', 'Echo']);
2const sectorA = new Set(['Rex', 'Sarah', 'Delta']);
3
4// intersection() - elementy wspólne
5const dangerousInA = carnivores.intersection(sectorA);
6console.log([...dangerousInA]); // ['Rex', 'Delta']
7
8// difference() - elementy tylko w pierwszym zbiorze
9const safeInA = sectorA.difference(carnivores);
10console.log([...safeInA]); // ['Sarah']
11
12// union() - wszystkie elementy z obu zbiorów
13const allKnown = carnivores.union(sectorA);
14console.log([...allKnown]); // ['Rex', 'Blue', 'Delta', 'Echo', 'Sarah']
15
16// symmetricDifference() - elementy tylko w jednym ze zbiorów
17const exclusive = carnivores.symmetricDifference(sectorA);
18console.log([...exclusive]); // ['Blue', 'Echo', 'Sarah']
19
20// isSubsetOf() - czy jest podzbiorem
21const smallGroup = new Set(['Rex', 'Blue']);
22console.log(smallGroup.isSubsetOf(carnivores)); // true
23
24// isSupersetOf() - czy zawiera wszystkie elementy
25console.log(carnivores.isSupersetOf(smallGroup)); // true
26
27// isDisjointFrom() - czy nie mają wspólnych elementów
28const herbivores = new Set(['Sarah', 'Bronty', 'Tricy']);
29console.log(carnivores.isDisjointFrom(herbivores)); // true

Pierwsze cztery metody zwracają nowy Set, a oryginalne zbiory zostają nietknięte. Metody z przedrostkiem is odpowiadają tylko true lub false, a spread zamienia wynik w tablicę, bo Set jest iterowalny.

Skoro mowa o zbiorach: struktura danych decyduje o szybkości. Dostęp po indeksie (array[0]) czy do właściwości obiektu (obj.key) ma stały koszt, zapisywany jako O(1), niezależny od liczby elementów. indexOf() i includes() w najgorszym razie przeglądają całą tablicę, czyli O(n), a sortowanie to zwykle O(n log n). Set.has() działa w praktyce w stałym czasie, więc przy częstym sprawdzaniu obecności zbiór wygrywa z tablicą.

2. Promise.try()

Bezpieczne wykonanie funkcji w kontekście Promise. Funkcja może zawieść na dwa sposoby: rzucić błąd od razu albo zwrócić odrzuconą obietnicę, a bez Promise.try() obie ścieżki trzeba obsłużyć osobno:

1// Bez Promise.try - błędy synchroniczne i asynchroniczne obsługujemy osobno
2function getDinosaurStatus(id) {
3  return new Promise((resolve, reject) => {
4    try {
5      const result = syncOperation(id); // może rzucić błąd od razu
6      resolve(asyncOperation(result));  // może zwrócić odrzuconą obietnicę
7    } catch (e) {
8      reject(e);
9    }
10  });
11}
12
13// Z Promise.try - czytelniej
14function getDinosaurStatus(id) {
15  return Promise.try(() => {
16    const result = syncOperation(id);  // błąd synchroniczny zostanie złapany
17    return asyncOperation(result);     // odrzucenie też
18  });
19}
20
21// W praktyce: bezpieczne wykonanie dowolnej funkcji jako obietnicy
22const safeCheck = Promise.try(() => {
23  validateDinoId("RAPTOR-001"); // może rzucić błąd
24  return fetchDinosaurData("RAPTOR-001"); // zwraca obietnicę
25});
26
27safeCheck
28  .then(data => console.log("Dane:", data))
29  .catch(err => console.error("Błąd:", err));

Promise.try() zamienia błąd rzucony synchronicznie w odrzuconą obietnicę, więc jedno .catch() łapie oba rodzaje awarii. Krótki test na funkcji, która rzuca błąd na żądanie:

1function riskyOperation(shouldThrow) {
2  if (shouldThrow) {
3    throw new Error('Synchroniczny błąd!');
4  }
5  return 'Sukces';
6}
7
8// Nowy sposób z Promise.try()
9const result = Promise.try(() => riskyOperation(false));
10result.then(console.log); // 'Sukces'
11
12const error = Promise.try(() => riskyOperation(true));
13error.catch(err => console.error(err.message)); // 'Synchroniczny błąd!'

Oba wyniki trafiają do zwykłych then() i catch() - błąd synchroniczny nie przerwał programu, tylko stał się odrzuceniem.

3. Import Attributes (JSON Modules)

Importowanie JSON bezpośrednio. Atrybut with { type: 'json' } mówi środowisku, jakiego typu plik ładujemy:

1// Importowanie pliku JSON z atrybutem typu
2import dinoConfig from './dinosaurs.json' with { type: 'json' };
3
4console.log(dinoConfig.species);
5
6// Dynamiczny import z atrybutami
7const config = await import('./config.json', { with: { type: 'json' } });

Atrybut to zabezpieczenie: jeśli serwer zamiast JSON-a zwróci skrypt, import się nie powiedzie, zamiast wykonać obcy kod. Składnia działa w modułach ES, które poznałeś wcześniej w tej lokacji.

Sprawdzanie wsparcia przeglądarki

Nowe funkcje nie trafiają do wszystkich przeglądarek jednocześnie. Zanim ich użyjesz, sprawdź, czy istnieją, porównując typ z napisem 'function':

1const es2024Support = {
2  objectGroupBy: typeof Object.groupBy === 'function',
3  arrayToSorted: typeof [].toSorted === 'function', // formalnie ES2023
4  arrayToReversed: typeof [].toReversed === 'function', // formalnie ES2023
5  arrayWith: typeof [].with === 'function', // formalnie ES2023
6  promiseWithResolvers: typeof Promise.withResolvers === 'function'
7};
8
9const es2025Support = {
10  setIntersection: typeof Set.prototype.intersection === 'function',
11  setDifference: typeof Set.prototype.difference === 'function',
12  promiseTry: typeof Promise.try === 'function'
13};
14
15console.log('ES2024 Support:', es2024Support);
16console.log('ES2025 Support:', es2025Support);

Takie wykrywanie funkcji (feature detection) jest pewniejsze niż zgadywanie po nazwie przeglądarki. Gdy funkcji brakuje, dołącz polyfill, czyli skrypt dopisujący brakującą metodę.

Podsumowanie

ES2024 i ES2025 przynoszą:

  1. Object.groupBy() / Map.groupBy() - grupowanie danych bez lodash i bez reduce()
  2. toSorted(), toReversed(), toSpliced(), with() - immutability bez spread operator (formalnie już od ES2023)
  3. Promise.withResolvers() - łatwiejsze zarządzanie Promise
  4. Metody Set - operacje na zbiorach wbudowane w język
  5. Promise.try() - jednolita obsługa błędów synchronicznych i asynchronicznych
  6. Import Attributes - jawna deklaracja typu importowanego modułu

Pamiętajcie, aby sprawdzać kompatybilność przeglądarek i w razie potrzeby używać polyfilli! Moja rada: po toSorted() i Object.groupBy() sięgaj śmiało, ale przy starszych przeglądarkach sprawdź tabelę zgodności na MDN. W kolejnej lekcji poznasz obserwatory wbudowane w przeglądarkę.

Pamiętaj: JavaScript ewoluuje jak gatunki w laboratorium - co roku przybywa nowych funkcji, ale przed wypuszczeniem na wybieg sprawdź, czy przeglądarka je rozpozna.

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co robi metoda Object.groupBy() wprowadzona w ES2024?

  2. 2. Czym toSorted() różni się od sort() w JavaScript?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Pogrupuj dinozaury po typie, posortuj po zagrożeniu i zamień elementy bez mutacji.

  • Układanie w pionie

    Uporządkuj funkcje JavaScript chronologicznie według standardu ECMAScript:

Przydatne artykuły