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
:
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ę zmiennejuser_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
lubfalse
– to nawet brzmi naturalnie. TutajisNumber
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
iclose
. To bardzo niespójne! Jeśli widzimy funkcję o nazwieopenFile
to można oczekiwać, że będzie również funkcjacloseFile
. Tutaj jednak nazywa się ona tylkoclose
.
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:
Co się stanie, gdy wywołamy naszą funkcję'use strict'; const obiekt = { a: 123, getA() { return this.a; } }; const inneGetA = obiekt.getA;
obiekt.getA(); // 123
inneGetA(); // ??
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 doczytać w tym wpisie:
this
i addEventListener
I teraz sedno tego akapitu. Co zrobi poniższy kod?
Oczywiście rzuci błądconst App = { displayMessage() { console.log('dziala!'); }, handleInputChange() { this.displayMessage(); } };
input.addEventListener('change', App.handleInputChange);
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));