2. Weekly JavaScript Challenge

Dobiega końca drugi Weekly JavaScript Challenge, którego jestem organizotorem. Jeśli jeszcze nie wiesz o co chodzi, to już wyjaśniam! Jest to grupa na Facebooku, na której mniej-więcej raz w tygodniu pojawia się nowe zadanie do wykonania w JavaScripcie. Celem istnienia grupy jest wzajemna pomoc i nauka JS-a. A wszystko to pro bono, dla powszechnego dobra i całkowicie za darmo. Zachęcam wszystkich początkujących programistów do spróbowania swoich sił w zadaniach!

W 2. Weekly JavaScript Challenge udział wzięło 14 osób. Trzymamy równy poziom :) Przy okazji chciałem bardzo serdecznie podziękować wszystkim początkującym programistkom i programistom, którzy z ogromnym zapałem rozwiązują kolejne zadania, a także pomagają i komentują prace innych. Dzięki!

Pamiętajcie, że w trakcie trwania kolejnych „czelendży” nadal możliwe jest wrzucanie rozwiązań do poprzednich zadań!

Podsumowanie

Ponownie udało mi się zidentyfikować kilka ogólnych problemów, które się powtarzają. Poniżej krótkie podsumowanie, w którym starałem się zawrzeć bardzo ogólne porady i dobre praktyki odnośnie pisania JavaScriptu, a także najczęściej popełnianie błędy. Zapraszam do czytania!

Matematyka w JavaScripcie

W wielu miejscach w Internecie można natknąć się na zaskoczone osoby pytające dlaczego JavaScript nie umie dodawać:

0.1 + 0.2 // 0.30000000000000004  

Co? Ale jak to 0.30000000000000004?

Nie jest to żaden szczególny wymysł twórców JavaScriptu, ani tym bardziej błąd w implementacji. Jest to rezultat podążania za specyfikacją IEEE 754, która definiuje w jaki sposób należy przeprowadzać operacje na liczbach zmiennoprzecinkowych i „problem” ten nie dotyczy tylko JavaScriptu.

Z tego względu nie należy nigdy przeprowadzać obliczeń finansowych na liczbach zmiennoprzecinkowych w JS. Służą do tego specjalne biblioteki takie jak na przykład BigInteger.js. Więcej na ten temat można doczytać na stronie o zabawnym adresie 0.30000000000000004.com

Zdarzenia change i input

Nasłuchiwanie na zdarzenie change na elemencie <input> albo <textarea> nie działa dokładnie tak, jak wiele osób by oczekiwało. Zdarzenie to jest wysyłane dopiero w momencie, gdy zmiany zostały zakończone – czyli np. po opuszczeniu inputa. Aby być informowanym o zmianie każdej literki lepiej podpiąć się pod zdarzenie input:

Zobacz Pen zBadLo – Michał Miszczyszyn (@mmiszy) na CodePen.

Wzorce i antywzorce

Pisałem już o zasadzie jednej odpowiedzialności w poprzednim podsumowaniu. Napisałem tam, że ważne jest, aby funkcje wykonywały jedno i tylko jedno zadanie. Słusznie. Jednak podążanie tylko za tą jedną zasadą nie czyni jeszcze kodu idealnym. Dobrych wzorców programowania jest co najmniej kilka, więcej można poczytać np. o SOLID albo o code smells wg. Jeffa Atwooda.

Przykładowo: Można świetnie rozwiązać ten challenge zgodnie z Single Responsibility Principle, ale gdyby chcieć do aplikacji później dodać nową jednostkę, wymagana byłaby modyfikacja w więcej niż jednym miejscu, gdyż lista jednostek jest przechowywana w HTML-u, ale ich wartości już w kodzie JS. Rozwiązanie? Zmodyfikować kod w taki sposób, aby zmiana jednej funkcji aplikacji wymagała zmiany tylko jednego miejsca w kodzie.

Lakoniczne nazwy

Konfucjusz podobno powiedział, że „mądrość zaczyna się od prawidłowego nazwania rzeczy”. W zasadzie to nigdy nie dowiemy się, czy te słowa naprawdę padły z jego ust, czy tylko miał bystrego PR-owca, niemniej jednak to zdanie sprawdzi się jako świetny wzorzec pisania czytelnego kodu.

Spójrzmy na przykłady źle nazwanych funkcji i zmiennych:

var userString = 'michal';  
var user_name = 'michal';

function isNumber(x) {  
    if (!Number.isFinite(x)) {
        alert('Error!');
    }
}

function toMetres(a, b) {  
    return a * b;
}

class FileHandle {  
    openFile() { … }
    close() { … }
}

Po kolei:

  • userString – nazwa zawiera w sobie typ. To niepotrzebne, nie przekazuje żadnej dodatkowej informacji, a do tego jeśli typ miałby się zmienić to musielibyśmy zmienić również nazwę zmiennej
  • user_name – tutaj w zasadzie nie ma problemu w samym nazewnictwie tej zmiennej, ale w kontekście całego kodu widoczna jest niekonsekwencja – spójniej byłoby nazwać ją userName
  • isNumber – funkcje, który nazwy zaczynają się od “is” lub “has” zwyczajowo powinny coś sprawdzać i zwracać true lub false – to nawet brzmi naturalnie. Tutaj isNumber dokonuje walidacji i wyświetla błąd, więc nazwa nie jest odpowiednia.
  • toMetres – nazwa całkowicie nie oddaje tego, co ta funkcja robi! Nazwy funkcji powinny być czasownikami, wtedy znacznie lepiej przekazują intencje programisty.
  • FileHandle – nazwa tej klasy jest okej, chodzi mi o jej metody: openFile i close. To bardzo niespójne! Jeśli widzimy funkcję o nazwie openFile to można oczekiwać, że będzie również funkcja closeFile. Tutaj jednak nazywa się ona tylko close.

Ogółem: nie bójmy się dłuższych i bardziej opisowych nazw! Programowałem trochę w iOS (Objective-C) gdzie nazywa funkcji są niezwykle rozwlekłe. Nabyłem w tym środowisku kilku nawyków. I tak na przykład ja funkcję toMetres zamieniłbym na calculateMetresFromUnits lub podobną, równie długą.

this

Och, to nieszczęsne this w JavaScripcie. Nierozumiane i niekochane przez nikogo. Mi samemu zajęło bardzo dużo czasu, aby poznać i zrozumieć niuanse z nim związane. W skrócie: funkcje wywoływane są w kontekście. Kontekst może się zmieniać. Trzeba o tym pamiętać. To tyle. To naprawdę tylko tyle i aż tyle. Spójrzmy na przykładowy kod:

'use strict';

const obiekt = {  
    a: 123,
    getA() {
        return this.a;
    }
};

const inneGetA = obiekt.getA;

obiekt.getA(); // 123  
inneGetA(); // ??  

Co się stanie, gdy wywołamy naszą funkcję inneGetA? Wiele osób instynktownie uznaje, że inneGetA jest tylko referencją na obiekt.getA i dlatego powinno zwrócić wartość 123. Tak się jednak nie dzieje. inneGetA rzeczywiście „wskazuje” na getA, jednak wywołanie inneGetA() odbywa się już w innym kontekście. Stąd otrzymujemy błąd: Uncaught TypeError: Cannot read property 'a' of undefined. Sprawa jest jeszcze ciekawsza, gdyż kontekst można zmieniać:

inneGetA.call({a: 1}) // 1  
inneGetA.apply({a: 2}) // 2  
inneGetA.bind({a: 3})() // 3  

Więcej na ten temat można sobie doczytać na przykład na StackOverflow.

this i addEventListener

I teraz sedno tego akapitu. Co zrobi poniższy kod?

const App = {  
    displayMessage() {
        console.log('dziala!');
    },
    handleInputChange() {
        this.displayMessage();
    }
};

input.addEventListener('change', App.handleInputChange);  

Oczywiście rzuci błąd Uncaught TypeError: this.displayMessage is not a function. Funkcja handleInputChange wywoływana jest w innym kontekście – a więc this.displayMessage nie istnieje. Jedno z możliwych rozwiązań to użycie bind:

input.addEventListener('change', App.handleInputChange.bind(App));  

Na koniec

Zachęcam do wzięcia udziału w kolejnym Weekly JavaScript Challenge! Ja też się całkiem sporo teraz uczę dzięki uczestnikom i myślę, że każdy może wynieść coś dla siebie nawet z najprotszych zadań.

Michał Miszczyszyn

Programista z doświadczeniem w JavaScripcie po stronie klienta i serwera. Wielki fan TypeScripta.

Subscribe to Type of Web

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!