1. Weekly JavaScript Challenge

Jakiś czas temu zainspirowałem się dość mocno Facebookową grupą Weekly WebDev Challenge i postanowiłem utworzyć grupę siostrzaną: Weekly JavaScript Challenge. Jest to miejsce, w którym średnio raz w tygodniu będę wrzucał nowe zadanie do wykonania. Celem jest wspólna nauka JavaScriptu, code review i wzajemna pomoc.

Kilka liczb…

Pierwszy Weekly JavaScript Challenge dobiegł końca. Na początek muszę powiedzieć, że jestem niezwykle zaskoczony tak pozytywnym odzewem i liczbą komentarzy! Do grupy zapisało się blisko 200 osób, w ankiecie dotyczącej znajomości JS wzięło udział ich prawie 80, a ok. 15 z nich wykonało pierwsze, dość proste zadanie. Uważam to za ogromny sukces!

Podsumowanie

Ponieważ jednak kilka problemów pojawiło się wielokrotnie, postanowiłem napisać krótkie podsumowanie, które można traktować jak bardzo ogólny poradnik pisania kodu.

Komentarze w kodzie

Zdaniem wielu autorytetów, komentarze w kodzie sugerują problem z czytelnością kodu (poza wyjątkami takimi jak np. opis skomplikowanych wyrażeń regularnych czy algorytmów). Jeśli czujesz, że dany fragment kodu wymaga komentarza do jego zrozumienia, to prawdopodobnie należy zmodyfikować sam kod i go poprawić. Komentarze lepiej zamienić w dobrze nazwane zmienne i funkcje. Zamiast

// position
var x = 0;

// start application
function main() { … }  

znacznie czytelniej jest:

var position = 0;

function startApplication() { … }  

Single Responsibility Principle

Ważne jest, aby funkcje wykonywały jedno i tylko jedno zadanie. To znaczy: Funkcja, która coś liczy nie powinna tego wyświetlać. Funkcja, która dokonuje walidacji nie powinna pokazywać błędów itd. Nazywa się to Single Responsibility Principle (Zasada Jednej Odpowiedzialności) i jest bardzo dobrą praktyką, która powinna być stosowana w trakcie pisania dowolnego kodu, w dowolnym języku.

Wiele osób jako kontrargument podaje, że taki kod jest mniej wydajny. Czyżby? Ponadto warto zadać sobie pytania: Ile razy pisałam/em aplikację, w której wydajność była krytyczna? Prawdopodobnie mniej niż 1 na 100 razy. A teraz drugie pytanie: Ile razy pisałam/em aplikację, której kod musiał potem czytać i zmieniać inny programista? Prawdopodobnie 100 na 100 razy. Czytelność i łatwość modyfikacji należy stawiać na pierwszym miejscu.

I tak przykładowo zły kod wygląda mniej więcej tak:

// jedyna funkcja w kodzie
// bardzo długa, robi wszystko
// od pobierania danych, przez obliczenia
// aż do wyświetlania
function main() {  
var l = prompt('Podaj liczbe.');  
var x = parseInt(l);  
var v = isNaN(x);  
    if (v) {
        alert('Error!');
    } else {
        for (var i = 2; i * i <= N; ++i) {
            // … i tak dalej
            // i tak dalej …
        }
    }
}

A kod znacznie lepszy:

function startPrimesApp() {  
    const N = getNumberFromUser();

    if (!isValid(N)) {
        displayError();
        return;
    }

    const result = computePrimeNumbers(N);
    displayResult(result);
}

// reszta funkcji
// …

Od razu widać jasny podział odpowiedzialności pomiędzy funkcje, a kod jest znacznie bardziej czytelny.

onclick, onload

Wiele osób skorzystało z atrybutu onclick=“funkcja()” w HTML-u. Prawdopodobnie dlatego, że to najłatwiejszy sposób i nadal można go często znaleźć w wielu kursach JS. Dlaczego to problem? Głównie dlatego, że skorzystanie z onclick w HTML-u wymaga stworzenia funkcji funkcja() o globalnym zasięgu – a zmienne globalne to coś czego zawsze należy unikać. Dodatkowym problemem jest fakt, że w taki sposób trudno jest dynamicznie dodawać i usuwać funkcje nasłuchujące na zdarzenia w trakcie działania aplikacji.

Jak więc zrobić to lepiej? Nasłuchiwać na zdarzenia najlepiej z poziomu JavaScriptu przy pomocy funkcji addEventListener (lub .on w jQuery).

innerHTML

Wiele wrzuconych rozwiązań wykorzystywało własność innerHTML tylko do tego, aby wyświetlać tekst. To błąd! innerHTML, jak sama nazwa wskazuje, służy do dodania kodu HTML do strony. To znaczy, że jeśli napiszemy taki kod:

el.innerHTML = '<h1>Hej</h1>';  

To zobaczymy:

Hej

Jeśli natomiast użyjemy textContent:

el.textContent = '<h1>Hej</h1>';  

to rezultat będzie inny:

<h1>Hej</h1>

Kod został automatycznie zamieniony na tekst.

innerHTML w większości przypadków jest całkowicie niepotrzebne. Dodatkowo to poważny problem, gdyż łatwo tutaj popełnić błąd (nawet najmniejszy, często niezauważalny) i sprawić, że cała aplikacja stanie się podatna na ataki XSS. W skrócie, zastanówcie się co się stanie, gdy wykonam taki lub podobny kod:

var dane = prompt();  
el.innerHTML = dane;  

To, co użytkownik wpisze w prompt() zostanie dodane do wnętrza elementu. A co jeśli użytkownik wpisze <script>…</script> lub coś podobnego? No właśnie… (więcej na ten temat pod koniec wpisu)

Zamiast innerHTML w większości przypadków do dodawania tekstu można użyć textContent.

Przypadkowe zmienne globalne

Pominięcie słowa kluczowego var (lub let, const) powoduje przypadkowe stworzenie zmiennej globalnej! Musimy o tym pamiętać. Trzeba na to uważać lub skorzystać z tzw. “strict mode”, w którym próba utworzenia takiej zmiennej powoduje błąd:

x = 1; // zmienna globalna  
'use strict';  
x = 1; // Uncaught ReferenceError: x is not defined  

Nawiasy klamrowe

To często kwestia preferencji, ale większość osób jednak zgadza się, że przy każdym if/elsie powinny znaleźć się klamry { … }, nawet jeśli zajmuje on tylko jedną linijkę. Dlaczego? Jeśli ich nie ma to kod automatycznie jest mniej czytelny i łatwo o błąd. Jeśli to Cię nadal nie przekonuje to warto pamiętać, że słynny krytyczny błąd bezpieczeństwa w OS X wynikał właśnie z braku nawiasów klamrowych…

Dlatego taki kod:

if (x)  
    doSomething(x);
else doSomethingElse();  

lepiej przepisać w ten sposób:

if (x) {  
    doSomething(x);
} else {
    doSomethingElse();
}

NaN

Wartość NaN w JavaScripcie jest bardzo wyjątkowa. Porównanie NaN === NaN zwraca false. Dlatego nie da się w ten sposób sprawdzić czy coś zawiera NaN, czy nie. Jak to więc zrobić poprawnie? Aby sprawdzić czy coś przyjęło wartość NaN najlepiej skorzystać z funkcji Number.isNaN.

Istnieje również globalna funkcja isNaN, która jedna zwraca nieco inne wyniki niż Number.isNaN. Zawsze lepiej jest korzystać z Number.isNaN, gdyż globalna funkcja isNaN może dać nieoczekiwane rezultaty…

isNaN('hej'); // true ?  
isNaN(undefined); // true ?  
Number.isNaN('hej'); // false  
Number.isNaN(undefined); // false  

Na koniec

Ogółem: Było całkiem nieźle. Cieszy mnie dobra frekwencja, a mam nadzieję, że w kolejnym zadaniu udział weźmie jeszcze więcej osób. Powoli będę też podnosił poziom Weekly JavaScript Challenge. Zaczęliśmy od czegoś prostego, ale już na kolejny ogień idzie prosta aplikacja do przeliczania miar w JavaScripcie, a następnie wykorzystanie szablonów do tworzenia widoków…

Pomysłów mam całe mnóstwo! Zachęcam gorąco do brania udziału, nawet jeśli zadania będą się wydawać pozornie banalne. Dla wprawionego programisty nie powinny być problemem, a początkujący będą mogli się szybko podszkolić patrząc na rozwiązania bardziej doświadczonych koleżanek i kolegów. I co najważniejsze – jedni i drudzy mogą się czegoś nauczyć. Ja już się nauczyłem całkiem sporo 🙂

*Rozwinięcie dot. XSS i innerHTML

W rzeczywistości w nowych przeglądarkach ten kod nie dałby żadnego rezultatu:

el.innerHTML = '<script>alert(1);</script>';  

Specyfikacja HTML5 mówi, że skrypt dodany w ten sposób nie powinien się wykonać. Są jednak sposoby, aby to obejść i nadal móc zaatakować aplikację przy pomocy innerHTML:

el.innerHTML = '<img src=a onerror=alert(1)>';  

A podobnych metod jest oczywiście znacznie więcej! Między innymi dlatego zapewnienie bezpieczeństwa plikacji jest tak trudne.