Jeden znak jest warty więcej niż tysiąc ifów
Czasami niektóre błędy są bardzo trudne do zdebugowania, bo występują stosunkowo rzadko, a i czają się w miejscach, w których byśmy się ich nie spodziewali.
Problem
Wyobraźmy sobie, że mamy prostą aplikację, która pokazuje parę divów po naciśnięciu przycisku. Cały kod JS wygląda tak:
const button = document.querySelector( 'button' ); // 1
const box1 = document.querySelector( '#box1' ); // 2
const box2 = document.querySelector( '#box2' ); // 3
button.addEventListener( 'click', ( evt ) => { // 4
evt.preventDefault() // 5
[ box1, box2 ].forEach( ( box ) => { // 6
box.classList.add( 'box_revealed' ); // 7
} );
} );
Na początku pobieramy przycisk (1) i obydwa boksy (2, 3). Następnie dołączamy nasłuchiwanie na kliknięcie przycisku (4). Dla pewności blokujemy domyślną akcję (5) – jakby ktoś wsadził kiedyś ten mechanizm do formularza – a następnie iterujemy po wszystkich boksach (6) i nadajemy każdemu z nich odpowiednią klasę (7). Ot nic specjalnego.
Tylko że nie działa i przeglądarki pokazują jakieś dziwne błędy. W Chrome dostajemy:
Uncaught TypeError: Cannot read properties of undefined (reading '#<HTMLDivElement>') at HTMLButtonElement.<anonymous>
Natomiast w Firefoksie błąd jest jeszcze dziwniejszy:
Uncaught TypeError: evt.preventDefault() is undefined <anonymous>
ASI, czyli skrytobójca perfekcyjny
Sytuacja z błędami jest paradoksalna o tyle, że osobiście uważam, że błąd w Chrome’ie dokładniej opisuje to, co się dzieje, ale jest przez to także mniej przyjazny dla użytkownika i trudno wywnioskować z niego, co wybuchło. Natomiast błąd w Firefoksie na pierwszy rzut oka nie ma sensu, za to wskazuje linijkę, w której doszło do eksplozji:
evt.preventDefault()
Jeśli się ją zakomentuje lub usunie, kod zaczyna działać. Ale naprawić można go też wprowadzając prostą modyfikację do problematycznej linii:
evt.preventDefault();
Po dostawieniu średnika wszystko wraca do normy.
Magia, jaka jest za to odpowiedzialna, nazywa się ASI – Automatic Semicolon Insertion (Automatyczne Wstawianie Średników). To mechanizm obecny w JavaScripcie, który – jak sama nazwa wskazuje – służy do wstawiania średników na koniec poszczególnych wyrażeń i instrukcji w razie, gdyby programista tego nie zrobił. Działa to w miarę dobrze… dopóki nie natrafia na konstrukcje składniowe, które można rozumieć na wiele sposobów. I właśnie w tym wypadku z taką mamy do czynienia. W JS-ie bowiem nawiasy kwadratowe służą do dwóch rzeczy – tworzenia tablic oraz odwoływania się do właściwości obiektów:
const iAmAnArray = [];
someObj[ 'I am just a property name' ];
ASI (jeszcze) nie korzysta ze sztucznej inteligencji i nie jest w stanie rozróżnić między tymi dwoma przypadkami. W naszym przykładzie oczekiwalibyśmy takiego rezultatu:
evt.preventDefault(); // 1
[ box1, box2 ].forEach( ( box ) => {
box.classList.add( 'box_revealed' );
} ); // 2
Średnik powinien być wstawiony po wywołaniu evt.preventDefault()
(1), żeby zaznaczyć, że dalej mamy do czynienia z tablicą. ASI jednak ten średnik omija i wstawia dopiero po całym forEach
(2). Innymi słowy uzyskujemy konstrukcję podobną do:
evt.preventDefault()[ box1, box2 ] // itd.
W tym momencie program działa tak, jakbyśmy chcieli pobrać właściwość z wartości zwracanej przez evt.preventDefault()
. A evt.preventDefault()
nic nie zwraca – czyli próbujemy pobrać właściwość z undefined
. Z kolei wnętrze tablicy traktowane jest jako dwa wyrażenia rozdzielone operatorem przecinka, czyli jako nazwa właściwości traktowany jest ostatni z elementów (box2
). Jeśli podstawimy sobie te wartości do kodu, uzyskamy:
undefined[ box2 ] // itd.
Czyli dokładnie to, o czym mówi błąd w Chrome: próbujemy pobrać z undefined
właściwość o nazwie stworzonej z elementu div
.
Co Bardziej Rozgarnięty Czytelnik zapewne zapyta w tym momencie, czy ten błąd by się pojawił, gdybyśmy zamiast wymieniać poszczególne elementy w tablicy, użyli zapisu [ ...document.querySelectorAll( '.box' ) ]
. Otóż nie, ten błąd by się nie pojawił, za to rzucony byłby błąd składni. Nie można bowiem użyć mechanizmu spread w nazwie właściwości.
Morał
A morał tej bajki jest krótki i niektórym znany: wstawiaj średniki jak poj…
A przynajmniej skonfiguruj sobie lintera tak, aby wskazywał ich ominięcie. No chyba że naprawdę chcesz je omijać, to wtedy musisz stosować się do pewnych zasad.
Komentarze
Przejdź do komentarzy bezpośrednio na Githubie.
Dawne komentarze
Ten blog wcześniej korzystał z systemu komentarzy Disqus. Jednakże pożegnaliśmy się i postanowiłem, że zaimportuję do nowej wersji stare komentarze z niego. Cóż, jego system eksportu na wiele nie pozwala…
Pierwszy raz z tym mechanizmem natknąłem sie czytając ten artykuł w TypeOfWeb.
Też podobny przypadek i wniosek.
Dlatego dla mnie średnik jest "świętą krową" i trzeba zwracać na to uwagę, żeby uniknąć takich smaczków spowodowanych przez ASI.