Te nawyki używania JavaScript, które potajemnie niszczą twoją bazę kodu
Wyjaśnia dziesięć powszechnych problemów w JavaScript i TypeScript, od luźnej równości po modyfikację stanu, oraz pokazuje bezpieczniejsze wzorce zastępujące każdy z nich.
Prawie każdy projekt napisany w JavaScript, niezależnie od firmy czy używanego frameworku, ma tendencję do występowania tych samych, powtarzających się problemów. Rzadko są to egzotyczne błędy lub nieprzewidywalne przypadki graniczne – chodzi o te same, powtarzające się nawyki.
Żaden z tych problemów nie spowoduje natychmiastowego awarii aplikacji. Właśnie dlatego są tak ryzykowne. Pozostają nierozpoznane, dopóki projekt się nie rozrośnie, nowy członek zespołu nie zacznie edytować kodu lub nagle nie wzrośnie jego wykorzystanie, a wtedy ujawniają się jako błędy, które pochłaniają całe popołudnie. Poniżej znajduje się dziesięć najczęściej występujących wzorców wraz z lepszymi alternatywami.
1. Używanie == zamiast ===
Operator równości luźnej w JavaScript konwertuje typy przed ich porównaniem, a wyniki tego procesu są niezwykle trudne do przewidzenia:
0 == "0" // true
0 == "" // true
"" == "0" // false
null == undefined // true
Oczywiście, po zapamiętaniu tych reguł przymusowej konwersji istnieje w nich pewna wewnętrzna logika. Ale nikt nie powinien musieć utrzymywać tego modelu mentalnego w pamięci tylko po to, by napisać prosty warunek. Zawsze używaj ===. Sprawdza on zarówno wartość, jak i typ jednocześnie, dając dokładnie takie porównanie, jakiego oczekujesz, bez żadnych ukrytych konwersji.
if (userInput === "0") { ... } // clear, predictable
2. Bezpośrednia modyfikacja stanu
To błąd powoduje problemy, które są szczególnie trudne do zlokalizowania, ponieważ objawy często pojawiają się daleko od rzeczywistej przyczyny:
function addItem(cart, item) {
cart.items.push(item); // mutates the original array
return cart;
}
Jeśli obiekt cart jest obserwowany gdzie indziej, w stanie React, w sklepie Redux lub w jakimkolwiek systemie, który wykrywa zmiany poprzez porównywanie referencji, tego typu mutacja bezpośrednia jest dla niego całkowicie niewidoczna. Sama referencja nigdy się nie zmienia, więc nie dochodzi do ponownego renderowania, żaden subskrybent nie otrzymuje powiadomienia, a w rezultacie próbujesz naprawić interfejs użytkownika, który tajemniczo odmawia aktualizacji.
function addItem(cart, item) {
return { ...cart, items: [...cart.items, item] };
}
Tworzenie zupełnie nowego obiektu lub tablicy zamiast modyfikowania oryginału faktycznie zużywa nieco więcej pamięci. W zamian otrzymujesz przewidywalne zmiany stanu, co jest korzystną transakcją, którą warto dokonywać znacznie częściej, niż się wydaje.
3. Brak obsługi odrzuceń obietnic
Funkcja async, która rzuca błąd bez umieszczenia jej w bloku try/catch, lub łańcuch .then() bez elementu .catch(), zazwyczaj zawodzi w sposób niewidoczny. W przeglądarce może w ogóle nic nie pokazać; w Node może wygenerować ostrzeżenie o nierozwiązanym błędzie, które łatwo przeoczyć wśród pozostałych logów.
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
getUser(42); // if this fails, where does the error go?
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return await res.json();
} catch (err) {
logger.error("Failed to fetch user", { id, err });
throw err;
}
}
Każda funkcja async, która może zawieść, wymaga wyraźnej strategii na taki przypadek. Pozostawienie jej bez obsługi w rzeczywistości nie jest strategią – to po prostu błąd, który może się pojawić później.
4. Głęboko zagnieżdżone callbacki
Nikt specjalnie nie zamierza tworzyć „piekła callbacków”. Powstaje ono stopniowo, krok po kroku, dzięki kolejnym operacjom asynchronicznym, aż kod osiągnie sześć poziomów zagnieżdżenia, a wcięcia przypominają schody:
getUser(id, (user) => {
getOrders(user.id, (orders) => {
getShipping(orders[0].id, (shipping) => {
updateUI(shipping); // and it keeps going
});
});
});
async/await zostały wprowadzone właśnie po to, aby uniknąć takiego rodzaju nawinięcia:
async function loadShippingInfo(id) {
const user = await getUser(id);
const orders = await getOrders(user.id);
const shipping = await getShipping(orders[0].id);
updateUI(shipping);
}
Podstawowe zachowanie asynchroniczne pozostaje takie samo, ale teraz logika jest czytana od góry do dołu, mniej więcej w taki sposób, w jaki opisałbyś ją na głos.
5. Ukryte zmienne globalne
Jeśli pominiesz deklarację const, let lub var poza trybem strict, JavaScript w tajemnicy powiąże tę zmienną z obiektem globalnym, zamiast wywołać błąd:
function calculateTotal() {
total = 0; // no declaration — this is now global
for (const item of items) total += item.price;
return total;
}
Zmienna total znajduje się teraz poza zakresem funkcji, więc może kolidować z dowolną inną zmienną o tym samym nazwie w całym kodzie – albo nadpisać coś innego, albo zostać sama nadpisana w zależności od kolejności wykonywania. Dodanie "use strict" na początku pliku przekształca to w natychmiastowy, widoczny błąd zamiast cichego, opóźnionego. Współczesna składnia modułów wykorzystująca import/export automatycznie włącza tryb ścisły, więc tego typu błędy stają się znacznie rzadsze po zastosowaniu modułów ES.
6. Porównywanie obiektów i tablic za pomocą ===
Ten błąd często występuje u osób, które zbyt dosłownie traktują zasadę nr 1. === sprawdza obiekty i tablice pod kątem referencji, a nie tego, co się w nich znajduje:
{ a: 1 } === { a: 1 } // false
[1, 2, 3] === [1, 2, 3] // false
Dwa obiekty, które wyglądają identycznie, są nadal oddzielnymi jednostkami w pamięci, dlatego ścisła równość traktuje je jako nierówne. Aby porównać rzeczywisty zawartość, potrzebna jest metoda głębokiego porównania: pomocnik taki jak isEqual z lodash, JSON.stringify w prostszych przypadkach lub własna rutyna porównawcza. Użycie === w tym kontekście nie jest błędem składniowym – po prostu odpowiada na inne pytanie niż to, które faktycznie chciałeś uzyskać.
7. Brak usuwania słuchaczy zdarzeń i timerów
Każde wywołanie addEventListener, setInterval lub subskrypcji to obietnica, że coś w końcu je usunie. Zaniedbanie tej obietnicy prowadzi do wycieku pamięci, który łatwo przeoczyć podczas rozwoju, ale ma poważne konsekwencje po uruchomieniu w produkcji:
useEffect(() => {
window.addEventListener("resize", handleResize);
// no cleanup — this listener never goes away
}, []);
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
8. Nadmierne używanie any w TypeScript
any nie jest tak naprawdę typem, lecz raczej sposobem na uniknięcie konieczności określania typu, a jego częste stosowanie powoli przekształca silnie typowany kod z powrotem w nietypowany, bez żadnego wyraźnego decyzji o tym:
function processPayment(data: any) {
return data.amount * data.rate; // no safety net at all
}
Za każdym razem, gdy modyfikujesz tu data, robisz przypuszczenia. Kompilator nie może zaznaczyć błędu pisowni, brakucej właściwości ani niezgodnego typu, ponieważ wyraźnie kazałeś mu przestać sprawdzać. Nawet słabo zdefiniowany typ jest lepszy niż brak jakiegokolwiek typu:
type PaymentData = { amount: number; rate: number };
function processPayment(data: PaymentData) {
return data.amount * data.rate;
}
Jeśli naprawdę jeszcze nie znasz struktury danej rzeczy, unknown jest uczciwym odpowiednikiem any. Wymaga on od ciebie określenia dokładniejszego typu przed użyciem, zamiast pozwalać na działanie w oparciu o niepotwierdzone założenia.
9. Ignorowanie różnicy między null a undefined
JavaScript oferuje dwa odrębne sposoby na określenie „nic tu nie ma”, a bazy kodu, które używają ich niespójnie, wypełniają się takimi sprawdzaniami:
if (value === null || value === undefined) { ... }
Taki wzorzec zazwyczaj wskazuje, że nikt nigdy nie uzgodnił żadnej konwencji. Lepszym podejściem jest przypisanie każdej wartości odrębnego znaczenia: undefined oznacza „to nigdy nie zostało ustawione”, a null oznacza „to zostało celowo ustawione na nic”. Dzięki temu operator łączenia wartości nullish pozwala sprawdzić obie jednocześnie, bez konieczności pisania porównania dwukrotnie:
const displayName = user.nickname ?? "Anonymous";
?? zostaje użyty tylko wtedy, gdy wartość po lewej stronie to null lub undefined. Różni się to od ||, który również korzysta z wartości 0, "" lub false – wartości, które często są prawidłowe i nie powinny być traktowane jako brakujące.
10. Pisanie kodu dla komputera, a nie dla kogoś innego
Ostatni punkt na tej liście to nie błąd składniowy, a mimo to powoduje większe straty niż pozostałe dziewięć razem wzięte. Zbyt złożony jednoliniowy kod może sprawiać przyjemność podczas pisania, ale jest trudny do odczytania dla innych osób.
const r = a.filter(x=>x.a).map(x=>x.b).reduce((a,b)=>a+b,0);
Funkcjonuje to. Jednak każdy, kto będzie to później czytał, włączając ciebie za kilka miesięcy, musi dokonać analizy wstecznej, aby dowiedzieć się, co tak naprawdę oznaczają a, x oraz pozostałe elementy łańcucha, zanim wprowadzi jakiekolwiek bezpieczne zmiany.
const activeUserBalances = users
.filter((user) => user.isActive)
.map((user) => user.balance);
const totalActiveBalance = activeUserBalances.reduce((sum, balance) => sum + balance, 0);
Ta wersja wymaga kilku dodatkowych linijek kodu, ale jest natychmiast zrozumiała i nie wymaga żadnego dekodowania. JavaScript ma tendencję do nagradzania pomysłowości w momencie jej użycia, a potem za to pobierać karę – a tym „później” jest niemal zawsze ktoś inny niż osoba, która pierwotnie napisała kod.
Wzorzec leżący u podstaw wszystkich dziesięciu
Gdy spojrzymy ponad szczegółami, okazuje się, że żaden z tych dziesięciu punktów nie dotyczy skomplikowanych faktów związanych z JavaScriptem. Wszystkie one dotyczą przewidywalności: porównań, które zachowują się tak, jak to opisano, stanu, który nie zmienia się bez twojej wiedzy, błędów, które są wykrywane, zamiast znikać bez śladu, oraz kodu, którego struktura odpowiada temu, co faktycznie robi. Jeśli poradzisz sobie z tymi dziesięcioma nawykami, pozostanie zwykłe debugowanie, takie jak to, które występuje we wszystkich bazach kodu, zamiast tego samo spowodowanego problemu, który po cichu pochłania twoje popołudnie.
Literatura pokrewna
- Powszechne pułapki JavaScript i TypeScript, które cicho niszczą kod — Wyjaśnia subtelne problemy związane z JavaScript i TypeScript – od porównań z NaN po kwestie związane z asynchronicznym działaniem i przymusową konwersją typów – które powodują błędy mimo pozornie poprawnego kodu.
- Funkcje Node.js 26, które cicho zastępują lata stosowanych rozwiązań — Przegląd API Temporal w Node.js 26, natywnego wykonywania kodu TypeScript, narzędzi do zarządzania pamięcią cache oraz innych ulepszeń, które eliminują długo stosowane rozwiązania awaryjne.