Strona główna / Artykuły

Prawdziwa złożoność nowoczesnego JavaScript pochodzi od narzędzi, a nie samego języka.

Ten artykuł wyjaśnia, w jaki sposób takie podstawowe funkcje JavaScriptu jak async/await i optional chaining upraszczają kod, podczas gdy nadmiar narzędzi i zależności wprowadzają niepotrzebną złożoność.

Wydawca: REACTAPP.TOP17 min czytania3951 słów

Źródła: Modern JavaScript Is Simpler Than We Pretend

Istnieje dziwny moment w rozwoju JavaScriptu, gdy uświadamiasz sobie, że wiesz zbyt wiele.

Nie tak bardzo o samym JavaScriptzie.

Zbyt wiele o ekosystemie otaczającym JavaScript.

Opanowałeś React. Potem pojawiły się hooksy. Następnie w grę weszło zarządzanie stanem, a za nim ulubiona biblioteka do tego celu. Masz mocne opinie na temat pobierania danych po stronie klienta. Rozumiesz, co robi bundler, chociaż rzadko otwierasz jego plik konfiguracyjny. Spotkałeś się z komponentami serwerowymi, sygnałami, „islandami”, środowiskami działającymi na brzegu sieci, meta-frameworkami, narzędziami do budowania projektów, linterami, formatatorami, menedżerami pakietów oraz nie mniej niż trzema konkurencyjnymi konwencjami organizacji folderu o nazwie components.

Następnie otwierasz mały plik JavaScriptu i uświadamiasz sobie, że cała logika sprowadza się do tego:

const users = await fetch("/api/users").then(res => res.json());

To jest cała sprawa.

Język sam w sobie jest często zaskakująco prosty.

To, co sprawia, że nowoczesny JavaScript wydaje się przytłaczający, to wszystko, co zostało na niego dodane.

Fakt, że bazy kodu JavaScript rozrastają się z kilku plików do systemów zawierających setki zależności, wskazuje na to, że pomyliśmy zaawansowanie z prawdziwą złożonością.

Praca z nowoczesnym JavaScriptem nie oznacza konieczności przyjmowania każdej nowej funkcji, jaką oferuje.

Oznacza to rozpoznanie, które problemy język już za ciebie rozwiązał.

Język po cichu dorósł

Kiedyś JavaScript miał reputację dziwnego, pochopnie zaprojektowanego języka, który jakoś ostatecznie stał się silnikiem całej sieci internetowej.

Część tej reputacji była uzasadniona.

W wczesnych wersjach występowały poważne trudności. API przeglądarek nie zachowywały się spójnie w różnych środowiskach. Rozumienie kodu asynchronicznego było naprawdę trudne. Środowisko wykonywania brakowało wbudowanych modułów, więc duże bazy kodu polegały na wzorcach, które często okazywały się bardziej skomplikowane niż sam język.

Dzisiejszy JavaScript to zupełnie inna historia.

Oto co masz teraz do dyspozycji:

  • Zmienne o zasięgu bloku za pomocą const i let
  • Zwięzła składnia funkcji dzięki funkcjom strzałkowym
  • Destruktyzacja do wyodrębniania wartości z obiektów i tablic
  • Interpolacja ciągów znaków z użyciem literówek szablonowych
  • Parametry domyślne, które pozwalają uniknąć ręcznych sprawdzeń awaryjnych
  • Składnia rozszerzania do kopiowania i łączenia danych
  • Natywny system modułów
  • Klasy, stosowane tylko wtedy, gdy są naprawdę konieczne
  • wbudowany sposób reprezentacji pracy asynchronicznej za pomocą obietnic
  • syntaksa, która sprawia, że praca asynchroniczna wygląda jak zwykłe kroki, mianowicie async i await
  • bezpieczny dostęp do właściwości przy użyciu łańcuchowania opcjonalnego
  • czystszy sposób użycia wartości domyślnych poprzez łączenie wartości nullowych
  • pomocniki do tablic i kolekcji wbudowane w język
  • prawdziwa prywatność w klasach dzięki polom prywatnym
  • generatory i iteratorzy do tworzenia niestandardowych sekwencji
  • dedykowane struktury danych Map i Set
  • solidne, wbudowane wsparcie dla JSON
  • środowiska wykonawcze, które stają się coraz bardziej funkcjonalne
  • Żadna z wymienionych funkcji nie utrudnia pracy z tym językiem.

    Prawdziwym problemem jest to, że programiści mają tendencję do zapamiętywania tych elementów jako chaotycznego zbioru składni, zamiast traktować je jako dość kompaktowy zestaw narzędzi służący do wyrażania codziennych koncepcji programistycznych.

    Gdy zrozumiemy podstawowe idee, składnia przestaje wydawać się onieśmielająca.

    Prawdopodobnie nie potrzebujesz połowy architektury

    Jednym z najszybszych sposobów na nadmierną skomplikowanie projektu w JavaScript jest podejmowanie decyzji architektonicznych, zanim w ogóle zrozumiemy problem.

    Wyobraź sobie panel sterowania: musi on załadować dane, umożliwić użytkownikowi ich edycję i zapisać wynik.

    Rozsądny pierwszy krok mógłby wyglądać w ten sposób:

    async function loadUsers() {
      const response = await fetch("/api/users");
    
      if (!response.ok) {
        throw new Error("Failed to load users");
      }  return response.json();
    }
    

    Następnie wywołanie tego jest tak proste jak:

    const users = await loadUsers();
    

    Na podstawie samego tego wzorca można stworzyć zaskakująco dużo oprogramowania funkcyjnego.

    A jednak współczesne nawyki skłaniają nas do natychmiastowego zadawania serii pytań zamiast tego:

    Czy powinieneś od razu sięgnąć po framework?

    A co z warstwą do pobierania danych?

    Czy potrzebujesz dedykowanej biblioteki tylko do zarządzania stanem?

    Czy powinien istnieć mechanizm cache przed Twoimi żądaniami?

    Czy wymagana jest biblioteka do walidacji schematu?

    Jak powinny być zorganizowane foldery i pliki?

    Który wzorzec powinny stosować Twoje komponenty?

    Które narzędzia do budowania powinny to wszystko skompilować?

    Jaki podejście powinien zastosować serwer do obsługi żądań?

    To pytania mogą okazać się ważne z czasem.

    Rzadko są to odpowiednie pytania do rozpoczęcia.

    Pierwsze pytanie powinno zawsze brzmieć:

    Czego tak naprawdę wymaga ta aplikacja?

    To rozróżnienie jest ważne, ponieważ każda abstrakcja ma swój koszt.

    Każda z tych koncepcji to coś, czego ktoś inny musi się nauczyć, zanim będzie mógł bezpiecznie pracować z kodem.

    Jeśli zadanie może wykonać zwykła funkcja, prawdopodobnie nie potrzebujesz klasy.

    Jeśli zadanie może wykonać moduł, prawdopodobnie nie potrzebujesz frameworka.

    Jeśli twój stan może być przechowywany w zwykłym obiekcie, prawdopodobnie nie potrzebujesz systemu zarządzania stanem.

    Jeśli fetch() spełnia twoje potrzeby, dodatkowa warstwa pobierania danych może jedynie stanowić dodatkowy pośrednik między tobą a siecią.

    To wcale nie oznacza, że frameworki są z natury złym rozwiązaniem.

    Oznacza to, że framework powinien rozwiązywać rzeczywisty problem, a nie taki, który sam sobie wyobraziłeś.

    async i await eliminują ogromne obciążenie umysłowe

    Nie każda ważna aktualizacja we współczesnym JavaScript jest efektowna.

    async i await to doskonały przykład cichej, ale znaczącej poprawy.

    Spójrzmy na stary podejście oparte na callbackach:

    getUser(id, function (user) {
      getOrders(user, function (orders) {
        processOrders(orders, function (result) {
          console.log(result);
        });
      });
    });
    

    Problemem nie była tylko tańsza wizualnie forma kodu.

    Bardzo głęboko nawarstwione callbacki sprawiały, że śledzenie przebiegu wykonywania było naprawdę trudne.

    Promises poprawiły sytuację:

    getUser(id)
      .then(user => getOrders(user))
      .then(orders => processOrders(orders))
      .then(result => console.log(result));
    

    A potem async i await przybliżyły kod asynchroniczny jeszcze bardziej do zwykłej, sekwencyjnej logiki:

    const user = await getUser(id);
    const orders = await getOrders(user);
    const result = await processOrders(orders);
    
    console.log(result);
    

    To właśnie tego typu funkcjonalności z czasem znika w tle, gdy stają się normą.

    To właściwie oznaka dobrego projektowania.

    Dobrze zaprojektowane funkcje językowe zazwyczaj znikają w codziennej praktyce, zamiast przyciągać uwagę.

    Błąd polega na założeniu, że skoro zachowanie asynchroniczne jest z natury trudne w obsłudze, to również składnia go otaczająca musi być skomplikowana.

    Zazwyczaj tak nie jest.

    Destructuring to nie test osobowości

    Część programistów nadmiernie polega na destruktorach, przez co zwykły kod staje się trudniejszy do zrozumienia.

    Weźmy przykład:

    const { name, email, address: { city } } = user;
    

    Ma swoje zastosowanie.

    Ale czasami tak jest jaśniej napisane:

    const name = user.name;
    const email = user.email;
    const city = user.address.city;
    

    Nie ma dodatkowych punktów za wypełnianie jednej linii większą ilością składni.

    Destructuring jest przydatny, gdy ułatwia zrozumienie związku pomiędzy kodem a leżącymi u jego podstaw danymi.

    Naprzимер:

    function createUser({ name, email }) {
      // ...
    }
    

    To jest wyraźne.

    Mówi czytelnikowi dokładnie, jakiego kształtu obiekt oczekuje funkcja.

    Ale gdy dekonstrukcja staje się na tyle złożona, że czytelnik musi w myślach odbudować pierwotną strukturę obiektu, ta funkcjonalność przestaje być pomocna i zaczyna działać na niekorzyść programisty.

    Współczesny JavaScript pozostaje prosty, gdy traktujemy składnię jako sposób komunikacji, a nie jako dekorację.

    Optional chaining rozwiązał prawdziwy problem

    Część funkcjonalności rzeczywiście zmniejsza chaos w kodzie.

    Optional chaining jest dobrym przykładem.

    Zanim pojawił się, często spotykało się coś takiego:

    if (
      user &&
      user.profile &&
      user.profile.settings &&
      user.profile.settings.theme
    ) {
      return user.profile.settings.theme;
    }
    

    Teraz można napisać:

    return user?.profile?.settings?.theme;
    

    To nie jest tylko krótsze.

    Jasno wyraża intencję.

    Rozwijający w praktyce mówi:

    To ścieżka może nie istnieć. Jeśli tak, bezpiecznie wyjść z tego trybu.

    Ta sama logika dotyczy nullish coalescing:

    const pageSize = options.pageSize ?? 20;
    

    które często jest dokładniejsze niż:

    const pageSize = options.pageSize || 20;
    

    Ponieważ || traktuje wartości fałszywe, takie jak 0 lub "", jakby ich w ogóle nie było, podczas gdy ?? reaguje tylko na null lub undefined.

    Takie funkcje warto poznać, ponieważ eliminują przypadkową złożoność, zamiast dodawać tylko estetyczne efekty.

    Oto przydatny filtr do zastosowania do każdej nowej składni:

    Czy to ułatwia zrozumienie kodu?

    Jeśli odpowiedź brzmi tak, naucz się tego.

    Jeśli służy to tylko do nadania kodowi imponującego wyglądu, postępuj ostrożnie.

    Współczesny JavaScript nie oznacza akrobacji programowania funkcyjnego

    Istnieje jeszcze jeden powszechny źródło uniknionej złożoności: założenie, że współczesny JavaScript musi być pisany w silnie funkcyjnym stylu.

    Często natrafisz na coś w rodzaju:

    const result = users
      .filter(user => user.active)
      .map(user => ({
        ...user,
        displayName: `${user.firstName} ${user.lastName}`
      }))
      .reduce((groups, user) => {
        const key = user.department;
        groups[key] ??= [];
        groups[key].push(user);
        return groups;
      }, {});
    

    Samo w sobie nie ma z tym żadnego problemu.

    Jednak łatwo zapomnieć, że zwykły pętla to nadal w pełni poprawny JavaScript.

    Ta wersja może być równie rozsądna:

    const result = {};
    
    for (const user of users) {
      if (!user.active) continue;  const department = user.department;  if (!result[department]) {
        result[department] = [];
      }  result[department].push({
        ...user,
        displayName: `${user.firstName} ${user.lastName}`
      });
    }
    

    Wersja z pętlą zajmuje więcej wierszy.

    Może być też prostsza do przeglądania podczas debugowania.

    Kod profesjonalny nie ocenia się po liczbie użytych wierszy.

    Ocenia się go po tym, jak mało niepotrzebnego wysiłku poznawczego wymaga od osoby, która go czyta.

    Czasami map, filter i reduce zmniejszają ten wysiłek.

    Czasami pętla lepiej spełnia zadanie.

    Ważniejsze jest wiedzenie, kiedy użyć danego narzędzia, niż zapamiętywanie wszystkich dostępnych metod tablic.

    Model obiektów w JavaScript staje się prostszy, gdy przestaniesz z nim walczyć

    Znaczna część zamieszania wokół JavaScript wynika z prób wpasowania go w model mentalny innego języka.

    Obiekty JavaScript mogą po prostu przechowywać dane:

    const user = {
      name: "Sarah",
      age: 31
    };
    

    Mogą również przechowywać funkcje:

    const user = {
      name: "Sarah",
    
      greet() {
        return `Hello, ${this.name}`;
      }
    };
    

    Można definiować klasy:

    class User {
      constructor(name) {
        this.name = name;
      }
    
      greet() {
        return `Hello, ${this.name}`;
      }
    }
    

    Jednak klasy nie są konieczne we wszystkich sytuacjach.

    Zwykły obiekt w połączeniu z funkcją fabryczną często jest równie przejrzysty:

    function createUser(name) {
      return {
        name,
    
        greet() {
          return `Hello, ${name}`;
        }
      };
    }
    

    Żaden z tych stylów nie jest lepszy we wszystkich przypadkach.

    Prawdziwą umiejętnością jest rozpoznawanie tego, czego faktycznie potrzebuje dany fragment kodu.

    Klasy mają sens, gdy mamy do czynienia z rzeczywistą tożsamością obiektów, ich zachowaniem, zarządzaniem cyklem życia, dziedziczeniem lub zamykaniem informacji.

    Zwykłe obiekty nadają się do prostych danych.

    Funkcje nadają się do przekształceń.

    Modyuły zazwyczaj nadają się do wyznaczania granic pomiędzy poszczególnymi częściami systemu.

    Nie musisz przekształcać każdego pojęcia w klasę tylko dlatego, że język to umożliwia.

    System modułów należy do najsilniejszych cech JavaScript

    Modyuły natywne w tle rozwiązują problem, który kiedyś powodował dużą niepotrzebną złożoność.

    Rozważmy ten wzorzec:

    // math.js
    export function add(a, b) {
      return a + b;
    }
    

    W połączeniu z:

    // app.js
    import { add } from "./math.js";
    
    console.log(add(2, 3));
    

    Taka konfiguracja tworzy czystą granicę zależności. Modyuł importujący pobiera tylko to, czego faktycznie potrzebuje. Sama zależność pozostaje widoczna w kodzie. Możesz przetestować funkcję osobno. I to wszystko nie wymaga wkładania wszystkiego do rozległego globalnego przestrzeni nazw.

    To, co sprawia, że jest to istotne, polega na tym, że nie potrzebujesz żadnego frameworku, aby osiągnąć modularność. Sam język pozwala ci wyraźnie oddzielić poszczególne części twojego systemu. Projekt może być zorganizowany w następujący sposób:

    src/
      users.js
      orders.js
      payments.js
      validation.js
      config.js
      app.js
    

    bez konieczności zmuszania każdego pliku do demonstracji wzorca projektowego.

    Dobrze zbudowany moduł zazwyczaj od razu ujawnia trzy rzeczy po samym tylko spojrzeniu: jaką funkcjonalność zawiera, które jej elementy udostępnia zewnętrznemu środowisku oraz na jakie inne fragmenty kodu polega, aby działać.

    Gdy te kwestie stają się oczywiste, twój projekt jest już w przyzwoitej formie.

    Nie wszystko musi być zabiegane

    To może być najważniejszy punkt w tym tekście.

    Często zdarza się, że programiści wprowadzają abstrakcję w momencie, gdy zauważą powtarzający się kod. Jednak samo powtórzenie niekoniecznie stanowi problem wymagający rozwiązania.

    Załóżmy, że masz:

    const user = await fetch("/api/users/42");
    const order = await fetch("/api/orders/18");
    

    W pewnym momencie możesz zauważyć, że logika obsługi odpowiedzi ciągle się powtarza. To spostrzeżenie może skłonić cię do napisania:

    async function getJson(url) {
      const response = await fetch(url);
    
      if (!response.ok) {
        throw new Error(`Request failed: ${response.status}`);
      }  return response.json();
    }
    

    Co pozwala ci uprościć to do:

    const user = await getJson("/api/users/42");
    const order = await getJson("/api/orders/18");
    

    Taka abstrakcja ma swoje uzasadnienie – pełni jasno określone zadanie.

    Teraz porównaj to z tworzeniem ogólnej fabryki żądań, wzorca strategii żądań, interfejsu repozytorium, adaptera transportowego, opakowania odpowiedzi oraz pełnej warstwy usług jeszcze zanim napiszesz drugi wywołanie API. W takim scenariuszu w rzeczywistości nie zmniejszyłeś złożoności – jedynie przeniosłeś ją do samej architektury.

    Ten wzorzec pojawia się stale w kodach napisanych w JavaScript. Programista zauważa dwie podobne linijki i od razu zaczyna projektować dla hipotetycznej przyszłości. Dobra praktyka inżynierska często wymaga podejścia przeciwnego.

    Czekaj, aż powtórzenia staną się na tyle oczywiste, by ujawniła się odpowiednia abstrakcja.

    Abstrakcje wprowadzane zbyt wcześnie są niezwykle trudne do usunięcia później, głównie dlatego, że w końcu nikt nie może z całą pewnością stwierdzić, czy nadal wykonują przydatną pracę.

    Brauzer już wiele dla ciebie robi

    Współczesny JavaScript może wydawać się bardziej skomplikowany, niż to konieczne, częściowo dlatego, że programiści tracą poczucie tego, ile potrafi zrobić sama platforma.

    Możesz bezpośrednio manipulować DOM-em. Możesz dołączać słuchacze zdarzeń. Możesz wysyłać żądania sieciowe. Masz do dyspozycji Web Storage. Możesz uruchamiać Web Workers. Możesz otwierać połączenia WebSocket. Możesz zarządzać historią przeglądania. Możesz pracować z formami natywnymi.

    Wszystko to jest dostępne bez konieczności korzystania z dodatkowych pakietów do każdego drobnego zadania. Na przykład:

    const button = document.querySelector("#save");
    
    button.addEventListener("click", async () => {
      const response = await fetch("/api/save", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ message: "Hello" })
      });  if (!response.ok) {
        throw new Error("Save failed");
      }
    });
    

    To jest w pełni funkcjonalna interakcja w prawdziwej aplikacji, bez użycia żadnego frameworka.

    To wcale nie jest argument przeciwko frameworkom – one rozwiązują rzeczywiste problemy. Jednak podstawowa platforma zasługuje na większe uznanie, niż często otrzymuje. Gdy zrozumiesz, co przeglądarka i język oferują ci już za darmo, będziesz w o wiele lepszej pozycji, by zdecydować, za co właściwie powinien odpowiadać framework.

    Zależności mają swoją cenę

    Instalacja pakietu może zaoszczędzić ci czas. Może też zobowiązać cię do długoterminowego korzystania z czegoś, co sam mógłbyś napisać za dziesięć linijek kodu.

    Weźmy małą narzędziową funkcję typu:

    function clamp(value, min, max) {
      return Math.min(Math.max(value, min), max);
    }
    

    Istnieje duże prawdopodobieństwo, że nie potrzebujesz do tego żadnego zewnętrznego pakietu. Jednak takie kwestie jak autoryzacja, kryptografia, skomplikowana arytmetyka dat, klienty bazy danych, przetwarzanie obrazów oraz zaawansowana logika walidacji należą do zupełnie innej kategorii.

    Lekcja, którą można stąd wyciągnąć, to nie „unikaj zależności za wszelką cenę”. Chodzi o to, by być świadomym tego, ile one faktycznie cię kosztują.

    Każda zależność niesie ze sobą:

    • ciągłą konserwację
    • aspekty bezpieczeństwa
    • prace związane z aktualizacją wersji
    • dokumentację do przeczytania
    • drzewo zależności transytywnych
    • wpływ na proces budowania aplikacji
    • jeszcze jedną abstrakcję, którą trzeba opanować

    Najlepsi programiści JavaScript niekoniecznie to ci, którzy zapamiętali najwięcej pakietów npm. To ci, którzy potrafią rozpoznać, gdy dany pakiet faktycznie nie jest potrzebny.

    TypeScript zwiększa odczuwaną złożoność, ale to już inna kwestia

    W przypadku większych baz kodu TypeScript może znacząco poprawić codzienną pracę nad rozwojem oprogramowania. Jednocześnie dodaje kolejną warstwę do języka.

    W rezultacie piszemy rzeczy takie jak:

    type User = {
      id: number;
      name: string;
      email: string;
    };
    

    Oprócz tego:

    function getUser(id: number): Promise<User> {
      // ...
    }
    

    To ma rzeczywistą wartość. Ważne jest jednak oddzielenie złożoności wynikającej z samego języka od tej, która pochodzi z ustawień narzędzi używanych do pracy.

    Sam JavaScript nie staje się trudniejszy tylko dlatego, że do projektu dodano sprawdzanie typów, lintera, narzędzie do łączenia modułów, framework interfejsu użytkownika, zestaw testów automatycznych oraz kilka wtyczek do budowania aplikacji. To, co stało się bardziej złożone, to nasze środowisko rozwojowe, a taka zaawansowaność może się zdecydowanie opłacić.

    Ale rozbudowana ścieżka narzędziowa nie powinna skłaniać cię do wniosku, że język leżący u jej podstaw jest z natury skomplikowany. Często w rzeczywistości jest dość prosty.

    Prawdziwa umiejętność to wybór najprostszej, przydatnej abstrakcji

    Pewien wzorzec przenika wszystko, o czym dotychczas rozmawialiśmy.

    Pisanie dobrego JavaScripta ma mniej wspólnego z opanowaniem każdej dostępnej funkcji i bardziej z wyborem najlżejszego narzędzia, które sprosta zadaniu.

    Trzeba przekształcić zawartość tablicy? Użyj map.

    Trzeba zawęzić zbiór wartości? Użyj filter.

    Trzeba uporządkować asynchroniczne zadania? Użyj async i await.

    Potrzebujesz ponownie używalnej logiki z jasno określonymi granicami? Napisz funkcję lub moduł.

    Potrzebujesz kolekcji, w której klucze nie muszą być wyłącznie ciągami znaków? Spójrz na Map.

    Czy potrzebujesz zagwarantować unikalność? Spójrz na Set.

    Czy chcesz bezpiecznie uzyskać dostęp do właściwości, która może być nieobecna? Użyj łańcuchowania opcjonalnego.

    Czy potrzebujesz wartości domyślnej tylko wtedy, gdy coś jest null lub undefined? Użyj ??.

    Czy potrzebujesz prostego kontenera danych? Wystarczy obiekt.

    Czy potrzebujesz bardziej złożonego zachowania oraz identyfikacji powiązanej z danymi? Klasa może się nadać.

    Czy musisz zorganizować naprawdę dużą aplikację? Wtedy warto rozważyć framework.

    Zwróć uwagę na tę sekwencję.

    Zaczynasz od samego języka.

    Następnie sprawdzasz, co już oferuje platforma.

    Potem dochodzą małe, specjalnie stworzone abstrakcje.

    Następnie biblioteki zewnętrzne.

    A na końcu frameworki.

    Inna kolejność powoduje wiele niepotrzebnej złożoności.

    Prostota to nie brak inżynierii

    Istnieje powszechne, lecz mylące założenie dotyczące tego, co tak naprawdę oznacza kod „prosty”.

    Część ludzi utożsamia prostotę z brakiem doświadczenia.

    To porównanie nie jest trafne.

    Rozważmy kogoś nowicjusza piszącego coś w tym stylu:

    function process(data) {
      // everything
    }
    

    Na pierwszy rzut oka wygląda to minimalistycznie, ale w rzeczywistości trudno jest go utrzymywać, gdy się rozrasta.

    Zdolniejszy programista raczej podzieliłby tę samą logikę na pięć małych modułów, z których każdy ma wyraźnie określone granice.

    W rezultacie jest więcej elementów, ale w rzeczywistości znacznie łatwiej jest o nim myśleć.

    To zupełnie inny, bardziej znaczący rodzaj prostoty.

    Celem nie jest zmniejszenie liczby plików, listy zależności czy liczby wierszy.

    Celem jest usunięcie złożoności, która nie ma uzasadnienia.

    To rozróżnienie staje się coraz ważniejsze w miarę wzrostu bazy kodu.

    Skrypt składający się z zaledwie dwudziestu linijek może nadal być mylący.

    Aplikacja z dwoma tysiącami linijek może, co zaskakujące, być łatwa do zrozumienia.

    Czynnikiem decydującym zazwyczaj nie jest sama ilość kodu.

    Chodzi o to, czy ten kod sprawia, że leżący u jego podstaw model jest oczywisty dla czytelnika.

    Przestań traktować każdą nową funkcjonalność jako obowiązkową wiedzę

    JavaScript rozwija się szybko, a ten tempa powoduje swego rodzaju stres.

    Przyzwyczajasz się do określonego sposobu działania, a kilka miesięcy później zupełnie inna metoda dominuje we wszystkich dyskusjach.

    Pojawia się nowy framework.

    Nowy środowisko wykonywania zaczyna zdobywać popularność.

    Nowy wzorzec renderowania staje się trendem.

    Pojawia się nowy element składni.

    Latwo jest mieć wrażenie, że ciągle się spóźniasz.

    Prawdopodobnie tak nie jest.

    Nie musisz natychmiast opanowywać każdej nowej techniki JavaScript, gdy tylko ktoś napisał o niej post.

    Zamiast tego inwestuj w podstawy.

    Opanuj programowanie asynchroniczne.

    Opanuj koncepcję zakresu dostępu.

    Opanuj zamykania funkcji.

    Opanuj obiekty.

    Opanuj moduły.

    Opanuj pętlę zdarzeń.

    Opanuj obietnice (promises).

    Opanuj obsługę błędów.

    Opanuj API dostępne w twoim przeglądarce lub środowisku wykonawczym.

    Opanuj śledzenie przepływu danych w aplikacji.

    Dopiero wtedy poznawaj nowe narzędzia, i to tylko wtedy, gdy faktyczny problem wymaga ich użycia.

    Taki podejście buduje wiedzę trwałą.

    Znajomość konkretnego frameworku może stać się przestarzała.

    Mocny model mentalny funkcjonowania programów zazwyczaj tak nie jest.

    Najlepszy kod JavaScriptu często wygląda nudno

    Rozwijający ostatecznie dochodzą do tego wniosku.

    Kod, który najłatwiej jest utrzymywać, rzadko robi wrażenie przy pierwszej lekturze.

    Nie składa się z pięciu warstw abstrakcji na sobie.

    Nie wymaga diagramu, aby pierwsza funkcja stała się zrozumiała.

    Nie korzysta ze wszystkich funkcji dodanych w najnowszej wersji języka.

    Często po prostu wygląda zwyczajnie.

    To nie jest krytyka, to zaleta.

    Otwierasz plik i intencja jest od razu jasna:

    import { getUser } from "./users.js";
    
    export async function loadProfile(id) {
      const user = await getUser(id);  if (!user) {
        throw new Error("User not found");
      }  return {
        name: user.name,
        email: user.email
      };
    }
    

    Prawie nie ma nic do dekodowania.

    Funkcja pobiera użytkownika.

    Sprawdza, czy ta operacja zakończyła się sukcesem.

    Zwraca dane, które są rzeczywiście potrzebne.

    Nic tutaj nie próbuje udowodnić, jak sprytny jest jego autor.

    Po prostu unika zmuszania czytelnika do wykonywania niepotrzebnej pracy, a to jest o wiele cenniejsza cecha w kodzie.

    Współczesny JavaScript jest prostszy od naszych projektów

    JavaScript ma swoje prawdziwe niedoskonałości. Każdy język je ma.

    Jednak duża część złożoności, którą ludzie przypisują „współczesnemu JavaScriptowi”, w rzeczywistości nie pochodzi z samego języka.

    Pochodzi ona z warstw nałożonych na niego.

    Frameworki.

    Systemy budowania aplikacji.

    Zależności.

    Konwencje.

    Abstrakcje.

    Narzędzia.

    Wybory architektoniczne.

    Część z tych warstw jest rzeczywiście uzasadniona.

    Inne nie są.

    Prawidłowe pytanie to nie to, czy dane narzędzie można uznać za nowoczesne.

    Prawidłowe pytanie brzmi: czy faktycznie zmniejsza ono złożoność w przypadku konkretnego problemu, przed którym stoimy.

    Zanim sięgniesz po kolejną zależność, sprawdź, czy platforma już ją obejmuje.

    Zanim wprowadzisz kolejną abstrakcję, sprawdź, czy problem duplikacji, który próbujesz rozwiązać, rzeczywiście istnieje.

    Zanim dodasz kolejną warstwę architektoniczną, dokładnie sprawdź, jakie awarie ma ona zapobiec.

    Zanim wybierzesz kolejne frameworki, sprawdź, jaką ograniczenie w obecnym ustawieniu faktycznie próbujesz naprawić.

    A zanim doszedniesz do wniosku, że sam JavaScript jest zbyt skomplikowany, spróbuj rozwiązać problem wykorzystując jedynie ten język oraz platformę, na której działa.

    Możesz być zaskoczony wynikiem.

    Język jest często najprostszą częścią całej sytuacji.

    To deweloperzy ciągle budują na nim kolejne warstwy.

    Czasami najbardziej nowoczesnym krokiem jest usunięcie jednej warstwy zamiast dodania kolejnej.

    Najlepszy JavaScript to nie ten kod, który wykorzystuje najwięcej narzędzi.

    To kod, w którym każde użyte narzędzie naprawdę zasłużyło na swoje miejsce.