Strona główna / Artykuły / Siedem wbudowanych funkcji JavaScript, które zastępują zależności od bibliotek pomocniczych

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.

2067 słów

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 (0 to styczeń, a 11 to grudzień), natomiast dni w miesiącu zaczynają się od 1.
  • Instancje Date są 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ść dla key, albo przechowuje defaultValue pod 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 Date w nowym kodzie, nie usuwając go.
  • using i await using sprawiają, że oczyszczanie jest automatyczne i uporządkowane, zastępując nawarstwione try...finally.
  • Należy preferować 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.
  • Pomocniki iteratorów oraz metody zbiórów eliminują całe kategorie konwersji tablic.
  • Należy sprawdzić dostępność funkcji w czasie wykonywania, ponieważ te propozycje zostały zaplanowane w zupełnie różnych terminach.
  • Literatura pokrewna