Kurs JavaScript i TypeScript · Moduł 5: Zaawansowany JavaScript
ES2024 i ES2025 - Najnowsze funkcje JavaScript
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) -
leticonst, funkcje strzałkowe, destrukturyzacja - ES2017 -
asynciawait - ES2020 - optional chaining (
?.) i nullish coalescing (??) - ES2023 - metody tablic bez mutacji, np.
toSorted() - ES2024 -
Object.groupBy()iPromise.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 sekundzieStary 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)); // truePierwsze 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ą:
- Object.groupBy() / Map.groupBy() - grupowanie danych bez lodash i bez reduce()
- toSorted(), toReversed(), toSpliced(), with() - immutability bez spread operator (formalnie już od ES2023)
- Promise.withResolvers() - łatwiejsze zarządzanie Promise
- Metody Set - operacje na zbiorach wbudowane w język
- Promise.try() - jednolita obsługa błędów synchronicznych i asynchronicznych
- 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. Co robi metoda Object.groupBy() wprowadzona w ES2024?
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: