JavaScript - konwencje kodowania
Wstęp
JavaScript jest językiem funkcyjnym z klamrami. Ponieważ większość programistów kojarzy klamry z podejściem imperatywnym i językami C-podobnymi, popularne konwencje dla JS oparte są wyłącznie o zasady znane z C czy Javy (dla przykładu Crockford oparł swoje konwencje o dokument napisany właśnie dla Javy). Podejście takie jest niewątpliwie błędne, ponieważ języki funkcyjne pozwalają osiągnąć o wiele większą czytelność kodu. Propozycje zawarte w niniejszym artykule przede wszystkim przyspieszają pracę programisty podczas nawigacji oraz czytania, ale także powodują zmniejszenie objętości kodu co jest ważne ze względu na specyfikę środowiska wykonania przeglądarek internetowych.
Zasadą przewodnią jest minimalizacja liczby linii kodu oraz jak najwyraźniejsze zaznaczanie poszczególnych bloków. Prawdą jest, że programista nie potrafi interpretować więcej niż ośmiu linii na raz wliczając linie puste, ale także nie potrafi szybko rozróżniać poszczególnych części kodu jeśli nie ma odpowiednich oznaczeń początku i końca bloków.
Podstawy
JS pozwala na oddzielanie instrukcji znakiem nowej linii lub średnikiem. Średniki, które nie są obowiązkowe, drastycznie zwiększają prawdopodobieństwo pominięcia tego znaku podczas pisania. Interpreter nie wskaże jego braku (nie jest to błąd składniowy). Jeśli założymy, że wszystkie instrukcje kończą się średnikiem, po minimalizacji kodu usuwającej zbędne białe znaki może okazać się, że program nie działa. Wykrycie gdzie zabrakło średnika jest bardzo uciążliwe. Minimalizacja powinna usuwać wszystkie zbędne białe znaki oprócz znaków nowego wiersza, co jest równoznaczne z tym, że średniki są zbędne.
Warto dbać o poprawność wcięć (styl Pythona jest bardzo wskazany). Ja używam tabulatora o szerokości 2 lub 3, nigdy nie zamieniając znaków na spacje. Używając strzałek do poruszania się po kilkukrotnie zagnieżdżonym kodzie przy 4 spacjach na wcięcie, powoduje znaczne spowolnienie nawigacji po kodzie. Jak łatwo wyliczyć tab przyspiesza tą procedurę czterokrotnie! W Komodo i niektórych innych edytorach można użyć ctrl+lewo i ctrl+prawo do nawigacji po słowach zamiast po literach, co jeszcze bardziej przyspiesza nawigację, jednak ta sztuczka nie działa np. w mcedit.
Większość konwencji odradza pisania znaków rozpoczęcia i zakończenia bloku w jednej kolumnie, natomiast ja uważam, że pisanie klamry otwierającej na końcu linii jest pomyłką. Komodo posiada wizualizację bloków polegającą na rysowaniu pionowej linii biegnącej od klamry otwierającej do zamykającej. Jeśli obie klamry są w tej samej kolumnie, przejrzystość kodu znacznie się zwiększa. Nie ma potrzeby używania klamer dla jedno-liniowego bloku. Wcięcie w zupełności wystarcza. Komodo potrafi poprawnie dodać nawiasy w przypadku rozszerzenia kodu o kolejną linię (wystarczy na początku linii wpisać {, a na jej końcu pojawi się }).
Należy unikać pustych linii kodu wewnątrz bloków. Blok powinien być widoczny na jednym ekranie. Biblioteka YUI ma w wielu miejscach bloki mające ponad 25 linii, które po usunięciu zbędnych linii skracają się do kilkunastu. W przypadku dużego ekranu dobrze jest widzieć kilka bloków na raz (np. całą deklarację prototypu).
Należy oddzielać deklaracje funkcji i obiektów jedną linią przerwy. Zasada ta nie dotyczy metod.
Należy używać języka angielskiego zarówno w nazwach zmiennych jak i komentarzach. Na potrzeby tego dokumentu będę używał języka polskiego.
Przykładowy kod:
E.util.print=function(o)
{
var i, s=""
for (i in o)
s+=i+": "+o[i].toSource()+"\n"
return s
}Komentarze
Dla zaawansowanego programisty komentarze opisujące rzeczy oczywiste są irytujące. Czytając kod źródłowy np. YUI doszedłem do wniosku, że i tak komentarze nie rozjaśniają sprawy. Należy przeczytać i zrozumieć kod linia po linii. Dla mnie usunięcie wszystkich komentarzy było najlepszym rozwiązaniem. Minimalizm jest więc wskazany chyba, że nasz kod czytać będą mało doświadczeni programiści. Myślę jednak, że po przeczytaniu tego dokumentu także i oni powinni czerpać korzyści z "minimalizmu komentarzowego". Komentarze także znacznie spowalniają nawigację po kodzie.
Należy opisać każdą deklarowaną zmienną. Powód jej istnienia, czy jest statyczna, lokalna itp. Jeśli czytelnik będzie znał przeznaczenie zmiennej, nie będzie miał problemu ze zrozumieniem instrukcji ją przetwarzających. Komentarz należy umieścić po deklaracji zmiennej w tej samej linii:
var o={}//Configuration objectKażda funkcja powinna mieć słowo opisu chyba. że jej przeznaczenie wynika bezpośrednio z nazwy (najczęstsza sytuacja). To samo dotyczy obiektów.
Opisany powinien być kod zawierający sztuczki, obejścia i mało znane konstrukcje. Dobrym pomysłem jest tu zamieszczenie odnośnika do szerszego opisu zastosowanej techniki (dokumentacja, blog itp.). Szczególnie się to przydaje podczas kodowania obejść dla IE.
Czasem warto też zostawić notę o złożoności obliczeniowej powolnych kawałków kodu lub też o wąskim gardle algorytmu. Informacje te będą bardzo przydatne dla osób składających większą aplikacje. Będą też kluczem w przyszłej optymalizacji.
Zmienne
W JS słowo kluczowe var powoduje ustawienie zasięgu zmiennej na obecny blok. var należy używać przy deklaracji każdej zmiennej przede wszystkim z powodów optymalizacyjnych. Bardziej ukrytym i zarazem niebezpiecznym powodem jest przypisywanie zasięgu globalnego wszystkim zmiennym inicjowanym bez var.
Nie należy w żadnym wypadku używać zmiennych globalnych. Nawet w najprostszych programach dobrze jest zamknąć kod w funkcję lub obiekt.
Aby uzyskać największą szybkość wykonywania kodu zmienne należy deklarować w najbardziej zagnieżdżonym bloku. Generowanie i inicjowanie nowej zmiennej jest w implementacjach JS szybsze niż sięganie do zmiennej z wyższego bloku dlatego też nie opłaca się deklaracja często używanych zmiennych poza pętlami lub funkcjami. var potrafi zainicjować wiele zmiennych na raz. Przykład poprawnego i czytelnego kodu wygląda następująco:
var i, //komentarz o i
j=0, //komentarz o j
o={}, //komentarz o o
o2=new object(), //komentarz o o2
t //komentarz o tTak jak i w innych językach programowania, pomimo możliwości deklaracji zmiennych w dowolnym miejscu programu, najlepiej robić to na początku bloku. Kiedy funkcja zawiera na początku testy (sprawdzanie blokad itp) warto deklarację podzielić na dwie części. Jedna inicjująca zmienne potrzebne do testów na samym początku funkcji, druga po testach inicjująca właściwe zmienne:
var f=function()
{
var t1,t2,t3
if (!test)
return
var z1,z2,z3
--właściwy kod--
}Nazewnictwo zmiennych
Wbrew pozorom poprzedzanie zmiennych przedrostkiem będącym pierwszą literą typu w JS nie zdaje egzaminu. JavaScript jest językiem silnie typizowanym. Ma bowiem dokładnie jeden typ zmiennej: obiekt, jego prototyp (także obiekt). Same obiekty natomiast mają typ wbudowany (np. String) albo typ zadeklarowany w prototypie (podlegający przekształceniom podczas wykonania). Używając Komodo nie ma problemu z szybkim określeniem typu oraz atrybutów obiektu. Pisanie przedrostków jest więc zbyteczne. Nie należy też używać znaku _ na początku zmiennej. Wielka litera jako pierwszy znak nazwy powinien być zarezerwowany dla nazw konstruktorów.
Każda zmienna służy pewnemu celowi, które bardzo często się powtarzają w różnych kontekstach. Najprostszym przykładem jest iterator, któremu najczęściej przypisuje się nazwę i. Proponowane nazwy są wymienione w poniższej tabelce:
| skrót | pełna nazwa | opis |
| el | element | element drzewa DOM |
| i,j,k... | iterator | iteratory naturalne lub obiektowe |
| a | arguments | lista argumentów funkcji |
| t,t2 | table | obiekt Array |
| o,o2 | obiekt | obiekt o prototypie Object ({}) |
| id | identifier | unikalny identyfikator o prototypie String |
| regex | regular exprassion | napis opisujący wyrażenie regularne |
| s,s2 | string | napis |
| f | function | napis |
| init | initialize | konstruktor |
Jeśli zmienne o nazwach x i x2 mają różny kontekst, ale mają taki sam cel warto je logicznie rozdzielić poprzez zamknięcie ich w dwa osobne kontenery tzn:
var kontekst1={x:""},
kontekst2={x:""}Generowanie obiektów jest w JS bardzo szybkie więc warto dla zwiększenia czytelności kodu zastosować tą technikę.
Instrukcje
Z powodów optymalizacyjnych należy używać konstrukcji switch wszędzie tam gdzie występuje zagnieżdżona instrukcja warunkowa if.
Dla instrukcji for istnieje konstrukcja for... in. Taka iteracja ma swoje wady, ale jest bardzo czytelna i dlatego należy ją używać. Jeśli przyjmiemy prawidłowy dla języka styl programowania,, konstrukcja ta nie będzie miała żadnych skutków ubocznych.
Nie używamy with oraz staramy się nie używać continue.
Obiekty
Warto używać {} zamiast new Object() oraz [] zamiast new Array(). Zmniejsza to objętość kodu, a zwiększa - czytelność.
Spośród wszystkich konwencji najbardziej zwiększającą czytelność jest poprawne zapisywanie kodu obiektów. Obiekty dzielimy na prototypowane oraz "proste" (o prototypie Object). Najbardziej czytelną, a zarazem zwięzłą i dobrą pod względem optymalizacji, metodą jest używanie literałów obiektów. Przykład obiektu prostego:
var o={
a:"aaa",
b:new mojObiekt(),
c:{
d:1.2
}
}Konwencja zapisywania literałów obiektów różni się od innych części kodu w kwestii klamer. W większości przypadków literały są bardzo długie i mają wiele zagnieżdżeń. Przydaje się wtedy minimalizacja pod względem liczby wierszy. Nawet na powyższym przykładzie widać, że w jednej linii obok deklaracji a lub d można dodać inne deklaracje bez uszczerbku na czytelności kodu. Jest tak dlatego, że literały służą jako kontenery danych - struktury, w których dane są najczęściej raz generowane i nie zmieniają się podczas wykonania programu. Dobrym przykładem jest tu JSON.
Obiekty można zagnieżdżać tak samo jak tablice. Atrybutom można też przypisywać funkcje (JS jest językiem, w którym funkcje są obiektami pierwszej klasy - cecha języków funkcyjnych). Stanowi to podstawę konwencji zapisywania obiektów prototypowanych (tych, które można generować przez mechanizm prototypowy). Prototyp zapisujemy jako literał obiektu i dołączamy do niego konstruktor:
var o=function()
{
this.init()
}
o.prototype={
x:1,
init:function()
{
instrukcja1
...
}
}Powyższy kod najlepiej zapisywać w osobnym pliku o nazwie obiektu. W kwestii optymalizacji kod jest bardzo oszczędny. pamięciowo. Funkcje są generowane raz, a poszczególne obiekty na zmiennych f przechowują tylko wskaźniki. Czytelność jest znakomita.
UWAGA: Wszystkie zmienne są podczas tworzenia obiektów kopiowane jako wskaźnik! Oznacza to, że wszystkie obiekty o tym samym prototypie mają wskaźnik na identyczny obiekt i wzajemnie nadpisują sobie dane (w innych językach noszą nazwę zmiennych klasowych lub statycznych).
Biblioteki
Istnieje kilka konwencji zapisywania bibliotek w JS. Różnią się one przede wszystkim zasięgiem zmiennych. YUI używa metody zaproponowanej przez Douglasa Crockforda pozwalającej uzyskać zmienne prywatne. Biblioteki są zamknięte w tzn sandboxy. Dodatkowa przestrzeń nazw jest bardzo przydatna. Można pisać kod tak jakby miał on interpreter na wyłączność a w szczególności stosować zmienne globalne. Są one niewidoczne dla kodu spoza sandboxu. YUI deklaruje tylko jedną zmienną globalną o nazwie YAHOO (w przyszłości nazwa ta będzie ustalana przez programistę). Ponieważ program wewnątrz sandboxu ma dostęp do zmiennych globalnych, może dodać nowe zmienne do obiektu YAHOO. Kod wygląda następująco:
var BIBLIOTEKA={
widget:{}
}
(function(){
var zmiennaPrywatna="widget"
BIBLIOTEKA.widget.newWidget=function()
{
this.init()
}
BIBLIOTEKA.widget.newWidget={
zmiennaPubliczna:""
init:function()
{
...
}
}
})Podejście to nie jest pozbawione wad. Jeśli chcemy dziedziczyć po newWidget, obiekt dziedziczący nie ma dostępu do zmiennaPrywatna! Z tego powodu rozszerzanie YUI jest bardzo utrudnione. Podejście to jest więc zalecane w projektach zamkniętych, w których dystrybucją całego kodu zajmuje się jedna jednostka. W przypadku dostarczania jedynie części (np. zestawu widgetów) oraz utrzymywanie tego kodu, zmienne prywatne skazują zewnętrznych programistów na ograniczenie swojego kodu do wykorzystania dostarczonej funkcjonalności lub wprowadzenia zmian w naszych bibliotekach. Oczywiście w tej drugiej sytuacji programiści zewnętrzni są zmuszeni do nanoszenia swoich poprawek przy każdej nowej wersji biblioteki. Dlatego też zalecam wyjątkową ostrożność w używaniu zmiennych prywatnych.
Innym podejściem jest nie używanie funkcji anonimowych i zapisywanie wszystkich zmiennych jako publiczne. Dla odróżnienia tych, które nie powinny być zmieniane można utworzyć kontener:
...
BIBLIOTEKA.widget.newWidget={
zmiennaPubliczna:""
zmiennePrywatne:{
zmiennaPrywatna1:"",
zmiennaPrywatna2:""
}
init:function()
{
...
}
}
