Audytując utils.js: Sześć wbudowanych funkcji zastępujących ręcznie tworzone pomocniki
Zobacz, które natywne API JavaScript mogą zastąpić metody typu deep-clone, kopie obronne, ręczne timeouty, odroczone obietnice oraz funkcję lodash groupBy, oraz jakie są związane z nimi ograniczenia.
Źródła: 6 Modern JavaScript Features You’re Probably Still Working Around
Wiele baz kodu nadal zawiera funkcje pomocnicze oraz importy bibliotek dla problemów, które platforma rozwiązała już lata temu. Te dodatki pojawiły się po cichu, więc nawyki związane ze starymi rozwiązaniami nigdy nie zostały ponownie przeanalizowane. Poniżej znajduje się sześć wbudowanych funkcji, które warto sprawdzić w pliku utils.js, wraz z problemami, które eliminują, oraz ograniczeniami, o których powinieneś wiedzieć przed usunięciem czegokolwiek.
structuredClone() zamiast przetwarzania danych w formacie JSON
Najczęściej stosowana metoda kopiowania obiektów w sposób głęboki polega na ich zserializowaniu, a następnie ponownym deserializowaniu:
const copy = JSON.parse(JSON.stringify(original));
Metoda ta zawodzi w przypadku wszystkiego, czego JSON nie może przedstawić: instancje Date wracają jako łańcuchy tekstowe, właściwości ustawione na undefined są pomijane, struktury typu Map i Set zamieniają się w puste obiekty bez żadnego ostrzeżenia, a cyrkularne odniesienia powodują błąd u JSON.stringify.
structuredClone() to funkcja globalna dostępna we współczesnych przeglądarkach i Node.js, której nie trzeba importować. Weźmy obiekt zawierający datę, zbiór oraz mapę:
const original = {
date: new Date(),
items: new Set([1, 2, 3]),
meta: new Map([['key', 'value']]),
};
Klonowanie go zachowuje całość tych typów w niezmienionej formie, a struktury cykliczne są również poprawnie kopiowane:
const copy = structuredClone(original);
// Dates, Sets, Maps, and circular refs all survive intact
Wykorzystuje algorytm skonstruowanego klonowania używany w postMessage i IndexedDB, dzięki czemu radzi sobie z przypadkami krawędziowymi, których nie obejmują ręcznie napisane klony. Ma jednak ograniczenia: funkcje i węzły DOM nie mogą być sklonowane, co powoduje błąd, a instancje własnych klas wracają jako zwykłe obiekty bez ich prototypu.
toSorted() i toReversed() zamiast kopii obronnych
.sort() i .reverse() modyfikują dane na miejscu, więc sortowanie tablicy udostępnianej przez referencję zmienia ją we wszystkich miejscach. Zacznijmy od tej tablicy:
const scores = [30, 10, 20];
toSorted() zwraca nowy, posortowany tablicę i nie zmienia oryginału:
const sorted = scores.toSorted((a, b) => a - b);console.log(scores); // [30, 10, 20] — untouched
console.log(sorted); // [10, 20, 30]
toReversed() oraz toSpliced() stosują ten sam wzorzec zwracania kopii. Jeśli twój kod zawiera wiele wyrażeń typu [...arr].sort() w celu utrzymania niezmienności stanu, jak to często ma miejsce w reduktorach React, rozszerzanie tablicy nie jest już konieczne. Szerszy opis tych metod zwracających kopię znajduje się w nowoczesnych metodach tablic JavaScript, które zastępują standardowe wzorce kodu.
Object.hasOwn() zamiast hasOwnProperty
Wywoływanie tej metody przez obiekt kryje w sobie pułapkę:
obj.hasOwnProperty('key'); // breaks if obj = Object.create(null)
Obiekt utworzony za pomocą Object.create(null) nie ma prototypu, więc nigdy nie dziedziczy metody hasOwnProperty, co powoduje wywołanie błędu TypeError. To samo dzieje się, gdy obiekt definiuje własną właściwość o takiej samej nazwie. Funkcja Object.hasOwn() jest statyczna, więc nie zależy od tego, co obiekt dziedziczy:
Object.hasOwn(obj, 'key'); // works regardless of the object's prototype chain
Ta zamiana jest niewielka, ale zapobiega błędom, które pojawiają się tylko w środowisku produkcyjnym, gdy ktoś przekazuje obiekt w formie, której nikt nie planował.
AbortSignal.timeout() zamiast ręcznego timera
Aby ustalić termin dla fetch, konieczny był AbortController w połączeniu z funkcją setTimeout, która go uruchamiała:
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 5000);
Następnie trzeba było pamiętać o usunięciu timera po zakończeniu żądania:
fetch(url, { signal: controller.signal }).finally(() => clearTimeout(id));
Teraz sygnał może zawierać własny termin:
fetch(url, { signal: AbortSignal.timeout(5000) });
Jedna linia, brak czyszczenia, brak pozostałego timera. Gdy wystrzeli, żądanie zostaje odrzucone z błędem TimeoutError DOMException, co umożliwia odróżnienie przekroczenia czasu od anulowania spowodowanego przez użytkownika. Aby połączyć przekroczenie czasu z innym źródłem anulowania, sprawdź AbortSignal.any() w aktualnej dokumentacji.
Promise.withResolvers() zamiast użycia rozwiązań resolve i reject
Czasami obietnica musi zostać rozstrzygnięta z zewnątrz jej egzekutora. Klasyczne rozwiązanie polega na deklaracji zmiennych i przypisywaniu im wartości wewnątrz callbacka konstruktora:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
To działa, ale przenoszenie funkcji poza zamykanie jest łatwe do błędnego odczytania. Promise.withResolvers() zwraca obietnicę wraz z obiema funkcjami rozstrzygającymi jednocześnie:
const { promise, resolve, reject } = Promise.withResolvers();
To pasuje naturalnie, gdy kolejkuje się zadania asynchroniczne lub otacza API oparte na zdarzeniach obiektami promise. Aby zobaczyć większy przykład, sprawdź koordynację wywołań narzędzi LLM za pomocą Promise.withResolvers.
Object.groupBy() zamiast lodash groupBy
Grupowanie rekordów według klucza to jeden z najczęstszych powodów importowania biblioteki pomocniczej. Przykładowo, dla listy osób:
const people = [
{ name: 'Sam', age: 21 },
{ name: 'Lee', age: 34 },
{ name: 'Kai', age: 21 },
];
Object.groupBy() przyjmuje obiekt iterowalny i funkcję zwrotną, a następnie zwraca obiekt zawierający jeden tablicę dla każdego klucza:
const byAge = Object.groupBy(people, (p) => p.age);
// { 21: [Sam, Kai], 34: [Lee] }
Zwróć uwagę na nazwę. Wcześniejsza propozycja nazywała to Array.prototype.group(), ale przed standaryzacją zostało ono przemianowane i przeniesione do statycznej funkcji Object.groupBy() (oraz Map.groupBy(), gdy klucze nie są łańcuchami), a dostarczane są jedynie wersje statyczne. Ten mechanizm ma prototyp null i jest najnowszą API na tej liście, dlatego sprawdź swoje minimalne wymagane wersje przeglądarki i Node.js przed usunięciem tej zależności.
Zanim usuniesz te narzędzia pomocnicze
Żadna z tych zmian nie wpływa na architekturę, ale każda eliminuje problematyczny wzorzec lub niepotrzebną zależność. Rozsądna kolejność:
- Zachowaj polyfilli, dopóki twoja matryca obsługi rzeczywiście nie pozwoli na ich usunięcie.