Siedem wbudowanych funkcji JavaScript, które zastępują zależności od bibliotek pomocniczych
Jak Temporal, wykorzystując deklaracje, operacje Map upsert, funkcję Math.sumPrecise, pomocniki iteratorów, metody zbiórów oraz Promise.withResolvers, eliminuje typowe powtarzalne fragmenty kodu w JavaScript.
Ranie poważnego projektu w JavaScript oznaczało kiedyś konieczność zainstalowania biblioteki do obsługi dat, narzędzia do kolekcji oraz pomocnika do grupowania, a następnie otaczania każdego załadowanego zasobu obronnymi blokami try...finally. W ciągu ostatnich kilku cykli wydań szereg propozycji TC39, które były dyskutowane przez lata, stał się częścią standardu i trafia do najnowszych przeglądarek, Node.js, Bun oraz Deno. Ten przewodnik omawia siedem z nich, problem, który każda z nich rozwiązuje, oraz na co zwrócić uwagę przed usunięciem danej zależności.
Uwaga dotycząca wsparcia przed rozpoczęciem pracy: te funkcje trafiły do silników w różnym czasie, a niektóre są bardzo nowe. Sprawdź aktualne dane kompatybilności dla Twoich docelowych przeglądarek i wersji środowiska wykonawczego, a także użyj polyfilla lub starej biblioteki tam, gdzie wsparcie nie jest jeszcze gwarantowane.
Temporal: prawdziwa API do obsługi dat i czasu
Każdy, kto korzystał z wbudowanego obiektu Date przez więcej niż kilka minut, natrafił na jego pułapki:
- Miesiące są liczone od zera (
0to styczeń, a11to grudzień), natomiast dni w miesiącu zaczynają się od1. - Instancje
Datesą zmiennicze, więc przekazanie ich funkcji pomocniczej może w tajemnicy zmienić wartość użycia. - Konwersja stref czasowych oraz obliczenia związane z czasem letnim wymagają skomplikowanych ręcznych obliczeń lub użycia bibliotek takich jak date-fns, Day.js czy Luxon.
Temporal to nowa, niezmiennej struktury API do obsługi dat i godzin, zaprojektowana jako zamiennik Date. Nie usuwa ona Date z języka – istniejący kod nadal działa – ale nowy kod już go nie potrzebuje.
Odrębne typy dla odrębnych koncepcji
Główną decyzją projektową Temporal jest modelowanie różnych koncepcji jako odrębnych typów:
Temporal.PlainDate: data kalendarzowa bez godziny i strefy czasowej, np. data urodzin.Temporal.PlainTime: godzina zegara ściennego bez daty, np. alarm o godzinie 07:00.Temporal.ZonedDateTime: dokładna chwila powiązana z konkretną strefą czasową i kalendarzem.
Poniższy fragment odczytuje obecną godzinę w Nowym Jorku, dodaje 14 dni do danej daty i mierzy różnicę między dwiema datami w miesiącach i dniach. Zauważ, że różnica jest wyliczana przy użyciu largestUnit: 'month', co sprawia, że wynik jest podawany w postaci miesięcy plus dni, a nie prostego liczbę dni.
// 1. Getting current time in a specific time zone
const meetingTime = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(meetingTime.toString());
// e.g. "2026-09-04T12:09:28-04:00[America/New_York]"
// 2. Safe, readable date math
const deadline = Temporal.PlainDate.from('2026-09-01');
const followUp = deadline.add({ days: 14 });
console.log(followUp.toString()); // "2026-09-15"
// 3. Calculating the exact difference between two dates
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-09-04');
const diff = start.until(end, { largestUnit: 'month' });
console.log(`${diff.months} months and ${diff.days} days`);
// "8 months and 3 days"
Każdy obiekt Temporal jest niezmienialny. Wywołanie metody add() nigdy nie modyfikuje oryginału; zwraca nowy obiekt. W wielu projektach oznacza to, że biblioteka do obsługi dat może zostać wyłączona z pakietu. Przed migracją przeczytaj częste pułapki związane z Temporal.
Jawna obsługa zasobów za pomocą using
Częstym powodem wycieków pamięci i zablokowanych zasobów jest zapomnienie zamknięcia czegoś, co zostało otwarte:
- pliku, którego strumień pozostaje otwarty po odczytaniu
- połączenia z bazą danych, które pozostaje aktywne po błędzie w trasie API
- słuchaczy zdarzeń lub wątków roboczych, które nigdy nie są odłączane
Klasycznym rozwiązaniem jest try...finally. Gdy w jednej funkcji znajdują się trzy zasoby, szybko przekształca się to w głęboko nawiasowaną, powtarzalną strukturę kodu. Wyraźne zarządzanie zasobami polega na użyciu deklaracji using wraz z dwoma dobrze znanymi symbolami: Symbol.dispose i Symbol.asyncDispose.
Jak uruchamia się usuwanie zasobów
Gdy wartość jest deklarowana za pomocą using, silnik wywołuje jej metodę [Symbol.dispose]() zaraz po opuszczeniu otaczającego bloku, niezależnie od tego, czy blok zakończył się normalnie, czy z powodu wystąpienia błędu. Jeśli w jednym zakresie deklarowanych jest kilka zasobów, są one usuwane w odwrotnej kolejności deklaracji, co jest dokładnie tym, co zrobiłyby nawiasowane bloki try...finally.
W przykładzie DatabaseSession implementuje mechanizm usuwania zasobów, a runReport deklaruje sesję za pomocą using. Kolejność zapisów w dzienniku pokazuje, że czyszczenie odbywa się po zapytaniu, gdy funkcja się kończy:
// Define a resource that knows how to clean itself up
class DatabaseSession {
constructor(dbName) {
this.connection = `Connected to ${dbName}`;
console.log("Database opened.");
}
query(sql) {
return `Results for: ${sql}`;
}
// Built-in disposal hook
[Symbol.dispose]() {
console.log("Database connection closed automatically!");
}
}
// Using the resource
function runReport() {
using session = new DatabaseSession("AnalyticsDB");
const data = session.query("SELECT * FROM metrics");
console.log(data);
// When runReport finishes (or if it throws),
// session[Symbol.dispose]() runs immediately.
}
runReport();
// Logs:
// 1. "Database opened."
// 2. "Results for: SELECT * FROM metrics"
// 3. "Database connection closed automatically!"
Dla asynchronicznego usuwania zasobów, np. zamknięcia gniazda sieciowego, należy zaimplementować [Symbol.asyncDispose]() i deklarować zasób za pomocą await using wewnątrz funkcji asynchronicznej. Wtedy środowisko wykonawcze czeka na usunięcie zasobów przed kontynuowaniem.
Modyfikacja mapy za pomocą getOrInsert i getOrInsertComputed
Map jest odpowiednią strukturą dla kolekcji opartych na kluczach, jednak wzorzec „przeczytaj je lub utwórz, jeśli ich brakuje” zawsze był niewygodny w użyciu. Grupowanie wartości pod danym kluczem zwykle wygląda w ten sposób:
// The old way: multiple lookups and manual branching
if (!userCache.has(userId)) {
userCache.set(userId, fetchDefaultProfile(userId));
}
const profile = userCache.get(userId);
To oznacza dwa wyszukiwania (has następujące po get) plus jedną ścieżkę decyzyjną, tylko po to, by upewnić się, że element istnieje. Nowe metody łączą to w jedną wywołanie i jedno wyszukiwanie:
map.getOrInsert(key, defaultValue): zwraca istniejącą wartość dlakey, albo przechowujedefaultValuepod tym kluczem i zwraca ją.map.getOrInsertComputed(key, callback): to samo, ale funkcja zwrotna jest wykonywana tylko wtedy, gdy klucz jest brakujący, dzięki czemu droga wartość domyślna nie jest tworzona bez potrzeby.
Różnica ta ma znaczenie w powyższym przykładzie. getOrInsert(userId, fetchDefaultProfile(userId)) wywoływałby fetchDefaultProfile przy każdym dostępie, ponieważ argumenty funkcji są ewaluowane przed wywołaniem; wersja oparta na obliczeniach unika tego problemu. Następny fragment grupuje zdarzenia według identyfikatorów użytkowników, tworząc pusty tablicę tylko wtedy, gdy po raz pierwszy pojawi się dany użytkownik:
const userActivity = new Map();
// Grouping events under user IDs
function logEvent(userId, eventName) {
// If userId doesn't exist, create an empty array, insert it, and return it.
const events = userActivity.getOrInsertComputed(userId, () => []);
events.push(eventName);
}
logEvent("user_42", "login");
logEvent("user_42", "clicked_button");
console.log(userActivity.get("user_42"));
// ['login', 'clicked_button']
Math.sumPrecise dla dokładnych sum
Liczby w JavaScript korzystają z formatu zmiennoprzecinkowego o podwójnej precyzji IEEE 754, co skutkuje słynnym wynikiem poniżej:
0.1 + 0.2; // 0.30000000000000004
W fakturach, koszykach zakupowych lub agregacjach wykresów takie błędy gromadzą się, aż suma staje się wyraźnie błędna.
Math.sumPrecise() przyjmuje zbiór liczb i oblicza ich sumę tak, jakby precyzja była nieograniczona, zaokrąglając ją tylko raz na samym końcu. Prosty reduce zaokrąglza po każdym kroku, co jest przyczyną błędów. W przykładzie cztery pozycje faktury dają łącznie 0.9999999999999999 przy użyciu reduce, natomiast Math.sumPrecise zwraca 1 (w konsoli wyświetla się 1, a nie 1.0):
const invoiceItems = [0.1, 0.2, 0.3, 0.4];
// Old reduce approach:
const naiveTotal = invoiceItems.reduce((acc, n) => acc + n, 0);
console.log(naiveTotal);
// 0.9999999999999999
// New Math.sumPrecise approach:
const accurateTotal = Math.sumPrecise(invoiceItems);
console.log(accurateTotal);
// 1.0
Ważne jest zrozumienie tego ograniczenia. Funkcja Math.sumPrecise usuwa nagromadzoną błąd zaokrąglenia, ale nie może zmienić faktu, że wartości takie jak 0.1 nie mogą być dokładnie przedstawione w systemie binarnym. Sumowanie zmiennych [0.1, 0.2] nadal daje wynik 0.30000000000000004, ponieważ dokładna suma tych dwóch liczb typu double zaokrągla się do tej wartości. Jest to rzeczywisty postęp w dziedzinie statystyk i paneli kontrolnych; jeśli chodzi o pieniądze, bezpieczniejszym wyborem pozostają całkowite jednostki pomocnicze (centy) lub biblioteki działań z dziesiętnymi liczbami.
Pomocniki iteratorów i Iterator.concat
Od ponad dziesięciu lat tablice oferują metody .map(), .filter() i .slice(), podczas gdy iteratorzy i generatory, takie jak nieskończone sekwencje czy kursorzy bazy danych, nie miały nic porównywalnego. Aby użyć .map() z generatorem generującym tysiące wierszy, trzeba było najpierw przenieść całość do tablicy za pomocą [...generator()], co eliminuje korzyści związane z oszczędnością pamięci dzięki przepływowemu przetwarzaniu.
Pomocniki iteratorów dodają bezpośrednio do nich metody takie jak map, filter, take, drop i flatMap, a Iterator.concat() łączy kilka iteratorów w jeden. Wszystkie one są opóźnione – element jest obliczany tylko wtedy, gdy o to prosi konsument.
Przykład łączy pomocniki z nieskończonym generatorem. Ponieważ ewaluacja jest opóźniona, a take(3) zatrzymuje się po trzech wartościach, pętla kończy swoją pracę, mimo że źródło danych nigdy się nie kończy:
function* infiniteCounter() {
let count = 1;
while (true) {
yield count++;
}
}
// Grab an iterator from our infinite generator
const stream = infiniteCounter()
.filter(num => num % 2 === 0) // Keep even numbers
.map(num => `Count: ${num}`) // Format them
.take(3); // Stop after 3 values
for (const item of stream) {
console.log(item);
}
// Logs:
// "Count: 2"
// "Count: 4"
// "Count: 6"
Aby połączyć kilka sekwencji bez tworzenia przechodniego tablicy, przekaż je do Iterator.concat(). Funkcja ta kolejno obsługuje każdy iterator:
const firstBatch = [1, 2, 3].values();
const secondBatch = [4, 5, 6].values();
const combined = Iterator.concat(firstBatch, secondBatch);
console.log([...combined]); // [1, 2, 3, 4, 5, 6]
Pomocniki do obsługi iteratorów były dostępne w głównych silnikach już od dłuższego czasu; Iterator.concat() jest nowszy, więc należy go sprawdzić osobno.
Operacje na zbiorach natywnych
Przez długi czas Set oferował niewiele poza metodami .has(), .add() i .delete(). Aby wykonać operację taką jak przecięcie, trzeba było konwertować dane do tablic i filtrować je ręcznie:
// The old manual way:
const intersection = new Set([...setA].filter(x => setB.has(x)));
Set.prototype obejmuje teraz prawdziwą algebrę zbiorów: union, intersection, difference, symmetricDifference, a także predykaty isSubsetOf, isSupersetOf i isDisjointFrom. Każda operacja zwraca nowy Set, pozostawiając oryginały nietknięte.
Przykład porównuje dwa zbiory ról, wyodrębniając uprawnienia dostępne tylko dla administratorów i potwierdzając, że role edytora są podzbiorem ról administratora:
const adminRoles = new Set(['read', 'write', 'delete', 'audit']);
const editorRoles = new Set(['read', 'write']);
// Find privileges exclusive to admins
const adminOnly = adminRoles.difference(editorRoles);
console.log([...adminOnly]); // ['delete', 'audit']
// Check role containment
console.log(editorRoles.isSubsetOf(adminRoles)); // true
Pomijają również tymczasowe tablice tworzone przez podejście oparte na rozszerzaniu i filtrowaniu.
Promise.withResolvers dla opóźnionych obietnic
Czasami obietnicę trzeba realizować poza jej konstruktorem, na przykład za pomocą obsługi zdarzeń lub funkcji callback zarejestrowanej w innym miejscu. Ten „odroczony” wzorzec wymagał wcześniej przenoszenia zmiennych poza obszar wykonywania kodu:
// The clunky way
let resolveFn, rejectFn;
const myPromise = new Promise((res, rej) => {
resolveFn = res;
rejectFn = rej;
});
Promise.withResolvers() zwraca obietnicę wraz z jej funkcjami resolve i reject w jednym obiekcie. W przykładzie obsługa kliknięcia realizuje obietnicę, a pozostały kod po prostu na nią czeka. Należy zauważyć, że ten fragment używa await na poziomie najwyższym, więc musi być uruchamiany w module ES lub wewnątrz funkcji async:
const { promise, resolve, reject } = Promise.withResolvers();
// Attach your listeners or pass the resolve handle to an event handler
document.getElementById("submit-btn").addEventListener("click", () => {
resolve("User clicked submit!");
}, { once: true });
// Await the promise wherever you need it
const message = await promise;
console.log(message);
Wzorzec ten jest przydatny do tworzenia niestandardowych kolejków zdarzeń, połączeń z Web Workers oraz dostosowywania interfejsów opartych na funkcjach callback lub emitter do kodu async.
Co to oznacza dla twoich zależności
Głównym motywem jest parowość: język ten obejmuje teraz rutynowe zadania takie jak obsługa dat, oczyszczanie zasobów, grupowanie według kluczy oraz operacje na zbiorach, które wcześniej były przekazywane bibliotekom użytkownika. Zanim dodasz kolejny pakiet w następnym sprintzie, sprawdź swoje cele:
- Jeśli celujesz w nowoczesne przeglądarki lub aktualne wersje Node.js, Bun czy Deno, wiele z tych narzędzi już istnieje globalnie.
- Każda uniknięta zależność oznacza mniejszy plik, szybsze uruchomienie oraz kod, który każdy programista JavaScript może przeczytać bez konieczności nauki API third-party.
Główne wnioski
- Temporal zapewnia datom niezmienne, wyraźnie oddzielone typy; zastępuje
Datew nowym kodzie, nie usuwając go. usingiawait usingsprawiają, że oczyszczanie jest automatyczne i uporządkowane, zastępując nawarstwionetry...finally.
getOrInsertComputed, gdy utworzenie wartości domyślnej jest kosztowne.Math.sumPrecise naprawia efekt kumulatywnego zaokrąglania, a nie reprezentację binarną; pieniądze należy przechowywać jako liczby całkowite.Literatura pokrewna
- Sześć wbudowanych funkcji HTML, które zastępują popularne biblioteki interfejsu użytkownika w JavaScript — Dowiedz się, jak popover, ekskluzywne informacje, okno dialogowe, Declarative Shadow DOM, fetchpriority i datalist zastępują własny kod JavaScript oraz jakie ograniczenia nadal mają każde z nich.
- Cechy Node.js 26, które cicho zastępują lata workaroundów — Przegląd API Temporal w Node.js 26, natywnego wykonywania TypeScript, narzędzi do zarządzania pamięcią cache oraz innych ulepszeń, które eliminują długo stosowane rozwiązania tymczasowe.
- Auditing utils.js: Sześć wbudowanych funkcji, które zastępują ręcznie tworzone narzędzia — Dowiedz się, które natywne API JavaScript mogą zastąpić metody takie jak głębokie klonowanie, kopie obronne, ręczne ustawianie czasu wygaśnięcia, odroczone obietnice oraz funkcję lodash groupBy, oraz jakie są związane z tym ograniczenia.