Siedem powszechnych idiomów w JavaScript, które potajemnie wprowadzają przyszłe błędy
Wyjaśnia, w jaki sposób codzienne wzorce JavaScriptu, takie jak sprawdzanie wartości prawdziwych, łańcuchowanie opcjonalne i składnia rozszerzania, ukrywają założenia, które w sposób niewidoczny powodują problemy w miarę rozwoju kodu.
Zapytanie typu pull request skraca dwanaście wierszy kodu JavaScript do czterech. Zaktualizowana wersja wykorzystuje opcjonalne łączenie wartości, sprawdzanie prawdziwości, rozszerzanie obiektów oraz uporządkowaną tabelę wyszukiwania, dzięki czemu przegląd kodu przebiega gładko. Zmiana wygląda bardziej uporządkowana, jest łatwa do odczytania i zachowuje się poprawnie dla każdego danych, które obecnie przetwarza aplikacja.
Kilka miesięcy później wartość 0 zaczyna być akceptowana jako prawidłowa wartość konfiguracyjna. Odpowiedzi API zaczynają pomijać pole, które wcześniej zawsze było obecne. Zadanie w tle, które wcześniej przetwarzało dziesiątki rekordów, nagle musi obsługiwać tysiące. Zagnieżdżony obiekt zostaje ponownie wykorzystany w miejscu, gdzie zakładano, że został już skopiowany. Żaden z tych późniejszych zdarzeń nie wydaje się powiązany z wcześniejszą refaktoryzacją, jednak każde z nich ujawnia założenie, że zwięzły kod zawierał je w sobie bez jasnego określenia tego faktu.
Dlatego właśnie niektóre idiomaty JavaScript są ryzykowne. Rzadko wydają się błędne w momencie użycia, a doświadczeni programiści sięgają po nie właśnie dlatego, że są wygodne, wyraźne i dokładne w obecnych warunkach. Problemy pojawiają się, gdy ta wygoda potajemnie łączy kilka różnych stanów w jedno zachowanie lub gdy składnia, która wydaje się gwarancją bezpieczeństwa, jest w rzeczywistości tylko skrótem.
Zatem prawdziwe pytanie podczas przeglądu nie brzmi, czy dany fragment kodu jest pomysłowy czy zwięzły. Chodzi o to, czy kod wystarczająco jasno ujawnia swoje założenia, aby przyszłe zmiany powodowały wyraźny błąd, a nie cichy, błędny wynik.
1. Sprawdzanie wartości „truthy” sprowadza różne wartości do tej samej decyzji
Załóżmy, że mamy ustawienie konfiguracyjne kontrolujące, ile razy można powtórzyć operację.
function getRetryCount(config) {
if (!config.retries) {
return 3;
}
return config.retries;
}
Taka logika jest w porządku, dopóki retries to liczba dodatnia lub jest nieokreślony. Jednak gdy ktoś doda możliwość całkowitego wyłączenia powtórzeń poprzez ustawienie tej wartości na 0, funkcja po cichu ignoruje tę wyraźną decyzję i zamiast tego zwraca 3. Zasady określające wartości prawdziwe w JavaScript są przestrzegane poprawnie — po prostu te zasady już nie odpowiadają rzeczywistym potrzebom biznesowym.
W JavaScript kilka wartości innych niż false jest uznawanych za fałszywe: 0, "", null, undefined oraz NaN również należą do tej kategorii. Dlatego warunek taki jak if (!value) tak naprawdę nie sprawdza, czy coś jest brakujące — sprawdza raczej, czy JavaScript przekształci tę wartość na false, co jest znacznie szerszym pytaniem niż „czy to jest nieobecne?”.
Bardziej dokładna wersja zależy od tego, jaki jest rzeczywisty kontrakt.
function getRetryCount(config) {
if (config.retries === undefined) {
return 3;
}
return config.retries;
}
A gdy zarówno null, jak i undefined mają sygnalizować „nic nie zostało dostarczone”, łączenie wartości typu nullish może w sposób bardziej klarowny wyrazić tę intencję.
const retries = config.retries ?? 3;
To rozróżnienie ma ogromne znaczenie dla pól formularzy, parametrów paginacji, cen, limitów, flag funkcjonalności, liczników oraz innych ustawień liczbowych. Flaga o nazwie enabled może słusznie mieć wartość false. Pole wyszukiwania może słusznie zawierać pustą ścieżkę. Odstęp może słusznie wynosić 0. Traktowanie każdej wartości fałszywej jako „niepodanej” oznacza utratę informacji w imię zwięzłości.
Żadne z tych aspektów nie czyni sprawdzania wartości „prawdziwej” z natury błędnym. Działają one dobrze, gdy wymóg rzeczywiście dotyczy właśnie wartości prawdziwej — na przykład przy decydowaniu o wyświetleniu opcjonalnej ciągu znaków tylko wtedy, gdy nie jest pusty. Stają się jednak problematyczne, gdy domena ma więcej odrębnych stanów, niż może zachować własna konwersja logiczna w JavaScript.
Pytanie, które warto zadać podczas przeglądu, jest proste: czy ta warunek faktycznie musi sprawdzać wartość fałszywą, czy też wartość prawdziwa jest używana jako zamiennik bardziej specyficznej reguły biznesowej?
2. Łańcuchowanie opcjonalne może przekształcić uszkodzone dane w ciche dane brakujące
Łańcuchowanie opcjonalne należy do najcenniejszych dodatków w JavaScript, ponieważ znacznie ułatwia przechodzenie przez głęboko opcjonalne struktury.
const avatarUrl = user.profile?.avatar?.url;
Gdy profile lub avatar rzeczywiście mogą być nieobecne, to właśnie w takiej sytuacji istnieje opcjonalne łączenie. Otrzymanie wartości undefined ma tu sens, ponieważ ich brak jest normalnym i oczekiwanym wynikiem.
Problemy pojawiają się, gdy opcjonalne łączenie jest stosowane do danych, które powinny zawsze istnieć.
function getShippingCountry(order) {
return order.customer?.address?.country;
}
Załóżmy, że umowa systemu gwarantuje, iż każdy zamówienie gotowe do wysyłki zawiera klienta oraz adres dostawy. Jeśli jakiś błąd w górnych warstwach programu spowoduje, że zamówienie będzie brakować jednego z tych elementów, opcjonalne łączenie po cichu zamienia to, co powinno być naruszeniem umowy, na zwykłą wartość undefined. Ten problem może ujawnić się dopiero znacznie później, objawiając się błędnym obliczeniem podatku, brakującą etykietą wysyłkową lub mylącym rozgałęzieniem w jakimś niepowiązanym miejscu kodu.
Lepszym podejściem w tym przypadku może być natychmiastowe zwrócenie błędu, dokładnie w tym miejscu, gdzie założenie ma rzeczywiście znaczenie.
function getShippingCountry(order) {
if (!order.customer?.address) {
throw new Error(
`Order ${order.id} is missing a required shipping address`
);
}
return order.customer.address.country;
}
Ta wersja jest bardziej rozwlekła, ale przekazuje więcej informacji. Informuje każdego, kto ją później przeczyta, że adres to nie tylko dane dodatkowe, które mogą się pojawić lub nie — ich brak oznacza, że obiekt w ogóle nie powinien być traktowany jako ważny dla tej operacji.
Łańcuchowanie opcjonalne ma również tendencję do rozprzestrzeniania się, gdy w typie lub umowie API pojawia się niepewność. Jeden niepewny pola zamienia się w foo?.bar, które szybko staje się foo?.bar?.baz, a wkrótce każdy użytkownik cicho toleruje brakujące dane zamiast pytać, dlaczego pole, które powinno być obowiązkowe, ciągle znika. Składnia zmniejsza liczbę rzucanych wyjątków, ale może równie łatwo zwiększyć ogólną niejednoznaczność.
Czy opcjonalne łączenie jest odpowiednim narzędziem, powinno wynikać z modelu domeny, a nie z wygody. Jeśli wartość jest rzeczywiście opcjonalna, opcjonalne łączenie to czysty sposób na jej przedstawienie. Jeśli jest wymagana, pozwolenie undefined na dalsze przenikanie przez program może sprawić, że ostateczny błąd będzie znacznie trudniejszy do odnalezienia w jego prawdziwym źródle.
3. Składnia rozpraszania wydaje się pełną kopią, ale zagnieżdżone obiekty zachowują swoją pierwotną tożsamość
Rozpraszanie obiektu do nowego to zwięzły sposób na pisanie aktualizacji, które wyglądają jak niezmienne.
const updatedUser = {
...user,
name: "Umar",
};
{ ...object } jako „sklonuj ten obiekt”, podczas gdy w rzeczywistości operacja ta jedynie kopiuje właściwości znajdujące się bezpośrednio w tym obiekcie.Wszystko, co znajduje się o jeden poziom głębiej, nie jest traktowane w ten sposób.
const original = {
name: "Umar",
preferences: {
theme: "light",
},
};
const copy = { ...original };
copy.preferences.theme = "dark";
console.log(original.preferences.theme);
// "dark"
Tutaj copy rzeczywiście jest odrębnym obiektem zewnętrznym, ale copy.preferences wskazuje nadal na dokładnie ten sam obiekt co original.preferences. Operacja rozszerzania skopiowała jedynie referencję, a nie strukturę przypisaną do niej.
To szybko staje się ryzykowne w zarządzaniu stanem, ponieważ powstały kod nadal ma wygląd niezmiennej aktualizacji.
const nextState = { ...state };
nextState.user.preferences.theme = "dark";
Obiekt zewnętrzny jest nowy, ale user i preferences mogą być dokładnie tymi samymi obiektami z poprzedniego stanu. Wszystko, co decyduje o ponownym renderowaniu lub obliczaniu na podstawie równości referencji, może skończyć się analizą hybrydy: niektóre części są świeżo utworzone, a inne zmodyfikowane na miejscu. Tymczasem każdy inny fragment kodu, który nadal przechowuje referencję do starego stanu, również będzie widział tę modyfikację.
Jeśli tylko jedna gałąź zagnieżdżonego obiektu faktycznie musi ulec zmianie, celowo skopiuj tę gałąź zamiast polegać na pojedynczym rozszerzeniu na najwyższym poziomie.
const nextState = {
...state,
user: {
...state.user,
preferences: {
...state.user.preferences,
theme: "dark",
},
},
};
Trzeba to bardziej wpisać, ale każdy zmieniony poziom otrzymuje nową tożsamość, podczas gdy elementy, których nie edytowano, nadal są bezpiecznie udostępniane. Narzędzia takie jak Immer mogą złagodzić ten problem, gdy drzewa stanu stają się głębokie i zagnieżdżone, ale nie eliminują podstawowego wymogu: nadal musisz wiedzieć, które referencje przetrwają aktualizację, a które nie.
Sama składnia rozprzestrzeniania się nie jest błędna. Błąd wynika z założenia, że płytkie kopiowanie zapewnia głęboką izolację.
Za każdym razem, gdy rozprzestrzeniajesz obiekt, aby chronić go przed późniejszymi modyfikacjami w innych miejscach, zatrzymaj się i sprawdź, które zagnieżdżone referencje nadal są udostępniane. Ta weryfikacja jest o wiele ważniejsza niż to, czy składnia przypadkowo używa ....
4. Przekształcanie za pomocą JSON.parse(JSON.stringify(...)) nie jest tym samym co klonowanie
Kluczowym sposobem na uniknięcie problemów z płytką kopią jest ten prosty kod:
const copy = JSON.parse(JSON.stringify(original));
Wydaje się, że rozwiązuje on problem nawarstwionych odniesień, ponieważ analiza ciągu JSON faktycznie tworzy zupełnie nowe obiekty. W przypadku danych już w formie zwykłego JSON może nawet dać dokładnie to, czego oczekiwano.
Problem polega na traktowaniu tego wyniku jako ogólnej gwarancji głębokiego klonowania.
JSON rozumie jedynie ograniczony model danych. Gdy tylko obiekt zawiera wartości poza tym modelem, serializacja może je cicho zmienić, usunąć lub wywołać błąd.
Weźmy jako przykład ten obiekt:
const value = {
createdAt: new Date(),
missing: undefined,
settings: new Map([["theme", "dark"]]),
};
Po przetworzeniu przez proces JSON, instancja Date staje się zwykłą strungą i traci całe zachowania specyficzne dla Date. Właśćność undefined całkowicie znika. Struktura Map nie przetrwa jako taka, z nienaruszonymi parametrami klucz-wartość, co stanowi problem, jeśli użytkownik oczekiwał prawidłowego klonu strukturalnego.
Odwołania cykliczne psują sytuację jeszcze bardziej bezpośrednio:
const user = {
name: "Umar",
};
user.self = user;
JSON.stringify(user);
// TypeError
Inne rodzaje wartości również sprawiają problemy. BigInt nie może zostać przetworzony za pomocą zwykłej funkcji JSON.stringify bez dodatkowej obróbki, właściwości o kluczach typu symbol są po prostu pomijane, a instancje klas użytkownika tracą prototyp, który pierwotnie nadawał im specyficzne zachowania klasy.
Żadne z tych elementów nie czyni serializacji JSON niewiarygodną — robi dokładnie to, do czego ma służyć serializacja, czyli kompresuje wartości JavaScript do bardziej ograniczonej formy obsługiwanej przez JSON.
Gdy naprawdę potrzebujesz klonu strukturalnego, structuredClone() jest często lepszym wbudowanym rozwiązaniem, pod warunkiem że typy, z którymi pracujesz, są przez niego obsługiwane. W przypadku danych ściśle związanych z twoją domeną napisanie wyraźnej funkcji kopiowania może być jeszcze bardziej niezawodne, ponieważ precyzyjnie określa, które właściwości powinny zostać zachowane, zamiast zakładać, że każda właściwość powinna być klonowana w sposób ogólny.
Warto również zauważyć, że głębokie klonowanie jest często zupełnie niewłaściwym narzędziem. Jeśli potrzebujesz zmienić tylko jeden zagnieżdżony element obiektu, klonowanie całej struktury marnuje wysiłek i utrudnia zobaczenie, która część faktycznie uległa zmianie.
Dobrym nawykiem jest nazywanie operacji tak, jaką faktycznie jest. Gdy zamierzasz zserializować coś, rób to celowo. Gdy chcesz sklonować coś, wybierz metodę klonowania, której zachowanie rzeczywiście odpowiada wartościom, które przechowujesz.
5. Promise.all() nie ogranicza liczby jednocześnie wykonywanych operacji
Załóżmy aplikację, która musi wyszukać informacje o grupie użytkowników.
const users = await Promise.all(
userIds.map((id) => fetchUser(id))
);
Dla niewielkiej liczby użytkowników to działa dobrze i zazwyczaj jest właściwym rozwiązaniem. Kod uruchamia wszystkie asynchroniczne wywołania, czeka na nie wspólnie i zwraca wyniki uporządkowane tak, aby odpowiadały wejściowej tablicy.
Później ktoś ponownie używa tej samej funkcji do przetwarzania zestawu 50 000 rekordów.
const users = await Promise.all(
userIds.map((id) => fetchUser(id))
);
W składni nic się nie zmieniło, ale zasada działania tak. Ponieważ .map() wywołuje fetchUser dla każdego elementu, a każde takie wywołanie prawdopodobnie natychmiast wysyła żądanie do sieci lub bazy danych, Promise.all() nie narzuca żadnych ograniczeń co do liczby jednocześnie wykonywanych takich żądań.
To może doprowadzić do przeładowania usługi poniżej w łańcuchu, wyczerpania puli połączeń, przekroczenia limitów szybkości, gwałtownego wzrostu zużycia pamięci lub wywołania serii prób ponownych pojawienia się błędów. Z wyglądu kod nadal przypomina uporządkowaną operację zbiorczą, więc łatwo przeoczyć rzeczywiste koszty jej wykonywania.
Istnieje drugie ukryte założenie w built-inie Promise.all(): gdy tylko jedna obietnica zostanie odrzucona, cała grupa jest odrzucana. To właściwe zachowanie, gdy traktujemy tę grupę jako jedną całość, ale nie oznacza to, że pozostałe operacje są anulowane – one nadal się wykonują nawet po wystąpieniu błędu.
Gdy zadania są faktycznie niezależne i chcemy uzyskać wszystkie wyniki bez względu na pojedyncze błędy, Promise.allSettled() często lepiej odpowiada temu, co chcemy osiągnąć.
const results = await Promise.allSettled(
userIds.map((id) => fetchUser(id))
);
Ale jeśli twoim prawdziwym problemem jest obciążenie innego systemu, przechodzenie między Promise.all() a Promise.allSettled() samo w sobie tego nie naprawi — żadna z tych funkcji nie ogranicza jednoczesności wykonywania operacji. To, czego naprawdę potrzebujesz, to grupowanie zadań, semafor, kolejka lub narzędzie do ograniczania jednoczesności, które pozwala utrzymać aktywnymi tylko określoną liczbę operacji naraz.
Taki model jest w pełni przydatny, gdy zbiór danych pozostaje mały i przewidywalny, a system docelowy jest w stanie obsłużyć równoległe obciążenie. Problem pojawia się później, gdy logika napisana z myślą o dziesięciu rekordach zostaje zastosowana do dziesięciu tysięcy, a nikt nie zatrzymuje się, by ponownie ocenić, czy to nadal jest bezpieczne.
Jednoczesność nie zależy od tego, jak zwięźle czekasz na realizację swoich obietnic — zależy od ilości pracy, którą uruchamiasz, oraz od tego, co faktycznie potrafią obsłużyć systemy podstawowe.
6. Metody tablic o funkcjonalnym wyglądzie nadal mogą powodować mutację
Metody tablic w JavaScript umożliwiają tworzenie zwięzłych, czytelnych łańcuchów przetwarzania danych, a w większości przypadków jest to rzeczywiście zaleta.
const activeUsers = users
.filter((user) => user.active)
.map((user) => user.name);
Ponieważ filter() i map() zwracają nowe tablice, łatwo jest założyć, że każdy łańcuch metod tablic pozostawia oryginał nietknięty. To założenie zostaje obalone w momencie, gdy do łańcucha dołączają sort(), reverse(), splice() lub inne metody powodujące mutację.
Załóżmy komponent React, który otrzymuje tablicę users jako właściwość:
function UserList({ users }) {
const sortedUsers = users.sort(
(a, b) => a.name.localeCompare(b.name)
);
return sortedUsers.map((user) => (
<User key={user.id} user={user} />
));
}
Wygląda na to, że ten kod tworzy nowy tablicę sortedUsers, ale w rzeczywistości metoda sort() przestawia elementy oryginalnej tablicy na miejscu i zwraca jedynie odwołanie do tej samej tablicy. Dlatego komponent, którego jedynym zadaniem jest renderowanie, powoduje jako skutek uboczny zmianę danych należących do jego rodzica.
Tego typu modyfikacje mogą pozostawać niewidoczne przez długi czas, dopóki jakaś inna część aplikacji nie będzie polegać na oryginalnej kolejności. Testy tego komponentu nadal przepuszczają testy, ponieważ wyświetlany wynik wygląda poprawnie, podczas gdy inny komponent lub zapisana wartość w innym miejscu zaczyna pracować z przestawionymi danymi, których w ogóle się nie spodziewał.
Jednym z rozwiązań jest sklonowanie tablicy przed zastosowaniem metody powodującej zmianę.
const sortedUsers = [...users].sort(
(a, b) => a.name.localeCompare(b.name)
);
Nowsze środowiska wykonywania JavaScriptu oferują również wersje nemutujące, takie jak toSorted(), jeśli Twoje środowisko docelowe je obsługuje.
Prawdziwą lekcją tutaj nie jest zapamiętywanie, które metody tablic są „bezpieczne”. Chodzi o to, by zastanowić się, czy dana operacja szanuje to, kto jest właścicielem danych.
Jeśli funkcji zostanie przekazana wartość, której nie posiada, jej ciche modyfikowanie powoduje zachowanie, które nie ma nic wspólnego z tym, za co funkcja powinna być odpowiedzialna. Kod jest o wiele łatwiejszy do zrozumienia, gdy modyfikacja albo w ogóle nie następuje, albo dzieje się jawnie, w miejscu, które faktycznie posiada tę wartość.
7. Tabele wyszukiwania mogą ukrywać nieważne dane zamiast je odrzucić
Pożądane jest często zastąpienie instrukcji switch zwykłym obiektem używanym jako tabela dyspozycji, ponieważ jest to bardziej deklaratywne.
const handlers = {
create: createUser,
update: updateUser,
delete: deleteUser,
};
handlers[action]?.(payload);
To jest zwięzłe, a dyspozycja poprzez wyszukiwanie to rzeczywiście dobry wzorzec w wielu przypadkach. Trudno jednak przeoczyć to, co się dzieje, gdy action nie odpowiada żadnemu znanemu kluczu.
Ponieważ ten przykład wykorzystuje opcjonalne łączenie, nieuznana akcja po prostu nic nie robi, w sposób niesłyszalny.
action = "udpate"; // typo
handlers[action]?.(payload); // no handler runs
Prosty błąd pisowni może teraz przeniknąć przez system pod pozorem udanego, bezskutecznego działania. Jeśli wywołujący nie sprawdzi jakiejś istotnej wartości zwracanej, reszta aplikacji może zgłosić, że operacja się udała, mimo że w rzeczywistości nic nie zostało wykonane.
Jasne wykrywanie błędów sprawia, że umowa między komponentami jest znacznie bardziej uczciwa.
const handler = handlers[action];
if (!handler) {
throw new Error(`Unsupported action: ${action}`);
}
return handler(payload);
const handlers = new Map([
["create", createUser],
["update", updateUser],
["delete", deleteUser],
]);
const handler = handlers.get(action);
if (!handler) {
throw new Error(`Unsupported action: ${action}`);
}
return handler(payload);
W TypeScriptu połączenie zjednoczenia dyskryminowanego z wyczerpującym switch może przynieść jeszcze większe korzyści, ponieważ dodanie nowej akcji zmusza kompilator do oznaczenia każdego miejsca w kodzie, które wymaga aktualizacji.
To wcale nie oznacza, że switch jest kategorycznie lepszy od tabeli wyszukiwania. Tabele dyspozycji sprawdzają się, gdy operacje są naturalnie związane z danymi, a zachowanie dla nieznanych kluczy zostało celowo określone.
Problemy pojawiają się, gdy pośrednictwo uniemożliwia odróżnienie sytuacji „program celowo obsłużył to” od „nic się w ogóle nie stało”.
Prawdziwą cenę sprytu stanowią ukryte założenia
Żadna z tych siedmiu praktyk nie zasługuje na całkowity zakaz.
Sprawdzanie wartości prawdziwej działa dobrze, gdy ta zasada rzeczywiście jest zgodna z regułami danego obszaru. Łańcuchowanie opcjonalne sprawdza się doskonale w przypadku relacji, które są faktycznie opcjonalne. Rozprzestrzenianie obiektów jest idealne do powierzchownych aktualizacji. JSON to naturalny format dla danych w formacie JSON. Promise.all() jest doskonale przystosowany do ograniczonej pracy równoległej. Metody modyfikujące tablice są w porządku, gdy masz pełną kontrolę nad tą tablicą. A tablice wyszukiwawcze mogą znacznie uproszczyć logikę obsługi zapytań.
Ryzyko polega na traktowaniu wygodnych domyśłów jako gwarancji, których składnia w rzeczywistości nie zapewnia. !value nie oznacza „brakującego”. ?. nie oznacza „bezpiecznie obecnego i ważnego”. { ...obj } nie oznacza „w pełni niezależnej kopii”. Serializacja do JSON i z powrotem nie oznacza „uogólnionego klonu”. A Promise.all() nie oznacza „kontrolowanej równoległości”.
Dlatego najłatwiejsza w utrzymaniu wersja kodu JavaScript nie zawsze jest najkrótsza. Kilka dodatkowych linijek, które odróżniają undefined od 0, odrzucają nieprawidłowo skonstruowane obiekty, ograniczają liczbę równoległych żądań lub wywołują błąd przy nieznanej akcji, może zachować informacje, które w przeciwnym razie przyszli administratorzy musieliby wyszukiwać w historii komitetów.
Gdy kod wyda ci się niezwykle pomysłowy, poświęć chwilę na sprawdzenie, co zostało skompresowane. Czasami odpowiedzią jest to, że usunięto jedynie niepotrzebne elementy formalne, a znaczenie pozostało nienaruszone – dokładnie to ma za zadanie dobra abstrakcja. Innym razem skompresowana zostaje różnica, której aktualne wymagania jeszcze nie potrzebują.
To właśnie ten drugi przypadek często jest przyczyną błędów w przyszłości.
Polusem przy przeglądaniu kodu jest zastanowienie się, jaka warunek musi pozostać prawdziwy, aby zwięzły wzorzec nadal funkcjonował po zmianie wymagań. Jeśli ten warunek jest ważny, ale nie jest widoczny nigdzie w kodzie, zapisz go wyraźnie, póki logika jest jeszcze świeża i łatwa do zrozumienia.
Budujący oprogramowanie w przyszłości rzadko napotykają problemy, ponieważ JavaScript pozwala im pisać zbyt dużo kodu. Problemy pojawiają się, gdy kod przestaje im cicho wskazywać, które różnice są naprawdę istotne.