You are on page 1of 193

Spis treści

Wstęp ............................................................................................................................11

1. Wprowadzenie ............................................................................................................ 15
Wzorce 15
JavaScript — podstawowe cechy 16
Zorientowany obiektowo 16
Brak klas 17
Prototypy 18
Środowisko 18
ECMAScript 5 18
Narzędzie JSLint 19
Konsola 20

2. Podstawy ..................................................................................................................... 21
Tworzenie kodu łatwego w konserwacji 21
Minimalizacja liczby zmiennych globalnych 22
Problem ze zmiennymi globalnymi 22
Efekty uboczne pominięcia var 24
Dostęp do obiektu globalnego 25
Wzorzec pojedynczego var 25
Przenoszenie deklaracji — problem rozrzuconych deklaracji var 26
Pętle for 27
Pętle for-in 29
Modyfikacja wbudowanych prototypów 31
Wzorzec konstrukcji switch 31
Unikanie niejawnego rzutowania 32
Unikanie eval() 32
Konwertowanie liczb funkcją parseInt() 34

5
Konwencje dotyczące kodu 34
Wcięcia 35
Nawiasy klamrowe 35
Położenie nawiasu otwierającego 36
Białe spacje 37
Konwencje nazewnictwa 38
Konstruktory pisane od wielkiej litery 38
Oddzielanie wyrazów 39
Inne wzorce nazewnictwa 39
Pisanie komentarzy 40
Pisanie dokumentacji interfejsów programistycznych 41
Przykład dokumentacji YUIDoc 42
Pisanie w sposób ułatwiający czytanie 44
Ocenianie kodu przez innych członków zespołu 45
Minifikowanie kodu tylko w systemie produkcyjnym 46
Uruchamiaj narzędzie JSLint 47
Podsumowanie 47

3. Literały i konstruktory .................................................................................................49


Literał obiektu 49
Składnia literału obiektowego 50
Obiekty z konstruktora 51
Pułapka konstruktora Object 51
Własne funkcje konstruujące 52
Wartość zwracana przez konstruktor 53
Wzorce wymuszania użycia new 54
Konwencja nazewnictwa 54
Użycie that 54
Samowywołujący się konstruktor 55
Literał tablicy 56
Składnia literału tablicy 56
Pułapka konstruktora Array 56
Sprawdzanie, czy obiekt jest tablicą 57
JSON 58
Korzystanie z formatu JSON 58
Literał wyrażenia regularnego 59
Składnia literałowego wyrażenia regularnego 60
Otoczki typów prostych 61
Obiekty błędów 62
Podsumowanie 63

6 | Spis treści
4. Funkcje .........................................................................................................................65
Informacje ogólne 65
Stosowana terminologia 66
Deklaracje kontra wyrażenia — nazwy i przenoszenie na początek 67
Właściwość name funkcji 68
Przenoszenie deklaracji funkcji 68
Wzorzec wywołania zwrotnego 70
Przykład wywołania zwrotnego 70
Wywołania zwrotne a zakres zmiennych 72
Funkcje obsługi zdarzeń asynchronicznych 73
Funkcje czasowe 73
Wywołania zwrotne w bibliotekach 74
Zwracanie funkcji 74
Samodefiniujące się funkcje 75
Funkcje natychmiastowe 76
Parametry funkcji natychmiastowych 77
Wartości zwracane przez funkcje natychmiastowe 77
Zalety i zastosowanie 79
Natychmiastowa inicjalizacja obiektu 79
Usuwanie warunkowych wersji kodu 80
Właściwości funkcji — wzorzec zapamiętywania 82
Obiekty konfiguracyjne 83
Rozwijanie funkcji 84
Aplikacja funkcji 84
Aplikacja częściowa 85
Rozwijanie funkcji 87
Kiedy używać aplikacji częściowej 89
Podsumowanie 89

5. Wzorce tworzenia obiektów ...................................................................................... 91


Wzorzec przestrzeni nazw 91
Funkcja przestrzeni nazw ogólnego stosowania 92
Deklarowanie zależności 94
Metody i właściwości prywatne 95
Składowe prywatne 96
Metody uprzywilejowane 96
Problemy z prywatnością 96
Literały obiektów a prywatność 98
Prototypy a prywatność 98
Udostępnianie funkcji prywatnych jako metod publicznych 99

Spis treści | 7
Wzorzec modułu 100
Odkrywczy wzorzec modułu 102
Moduły, które tworzą konstruktory 102
Import zmiennych globalnych do modułu 103
Wzorzec piaskownicy 103
Globalny konstruktor 104
Dodawanie modułów 105
Implementacja konstruktora 106
Składowe statyczne 107
Publiczne składowe statyczne 107
Prywatne składowe statyczne 109
Stałe obiektów 110
Wzorzec łańcucha wywołań 112
Wady i zalety wzorca łańcucha wywołań 112
Metoda method() 113
Podsumowanie 114

6. Wzorce wielokrotnego użycia kodu ..........................................................................115


Klasyczne i nowoczesne wzorce dziedziczenia 115
Oczekiwane wyniki w przypadku stosowania wzorca klasycznego 116
Pierwszy wzorzec klasyczny — wzorzec domyślny 117
Podążanie wzdłuż łańcucha prototypów 117
Wady wzorca numer jeden 119
Drugi wzorzec klasyczny — pożyczanie konstruktora 119
Łańcuch prototypów 120
Dziedziczenie wielobazowe przy użyciu pożyczania konstruktorów 121
Zalety i wady wzorca pożyczania konstruktora 122
Trzeci wzorzec klasyczny — pożyczanie i ustawianie prototypu 122
Czwarty wzorzec klasyczny — współdzielenie prototypu 123
Piąty wzorzec klasyczny — konstruktor tymczasowy 124
Zapamiętywanie klasy nadrzędnej 125
Czyszczenie referencji na konstruktor 125
Podejście klasowe 126
Dziedziczenie prototypowe 129
Dyskusja 129
Dodatki do standardu ECMAScript 5 130
Dziedziczenie przez kopiowanie właściwości 131
Wzorzec wmieszania 132
Pożyczanie metod 133
Przykład — pożyczenie metody od obiektu Array 134
Pożyczenie i przypisanie 134
Metoda Function.prototype.bind() 135
Podsumowanie 136

8 | Spis treści
7. Wzorce projektowe ....................................................................................................137
Singleton 137
Użycie słowa kluczowego new 138
Instancja we właściwości statycznej 139
Instancja w domknięciu 139
Fabryka 141
Wbudowane fabryki obiektów 143
Iterator 143
Dekorator 145
Sposób użycia 145
Implementacja 146
Implementacja wykorzystująca listę 148
Strategia 149
Przykład walidacji danych 150
Fasada 152
Pośrednik 153
Przykład 153
Pośrednik jako pamięć podręczna 159
Mediator 160
Przykład mediatora 160
Obserwator 163
Pierwszy przykład — subskrypcja magazynu 163
Drugi przykład — gra w naciskanie klawiszy 166
Podsumowanie 169

8. DOM i wzorce dotyczące przeglądarek ..................................................................... 171


Podział zadań 171
Skrypty wykorzystujące DOM 172
Dostęp do DOM 173
Modyfikacja DOM 174
Zdarzenia 175
Obsługa zdarzeń 175
Delegacja zdarzeń 177
Długo działające skrypty 178
Funkcja setTimeout() 178
Skrypty obliczeniowe 179
Komunikacja z serwerem 179
Obiekt XMLHttpRequest 180
JSONP 181
Ramki i wywołania jako obrazy 184

Spis treści | 9
Serwowanie kodu JavaScript klientom 184
Łączenie skryptów 184
Minifikacja i kompresja 185
Nagłówek Expires 185
Wykorzystanie CDN 186
Strategie wczytywania skryptów 186
Lokalizacja elementu <script> 187
Wysyłanie pliku HTML fragmentami 188
Dynamiczne elementy <script> zapewniające nieblokujące pobieranie 189
Wczytywanie leniwe 190
Wczytywanie na żądanie 191
Wstępne wczytywanie kodu JavaScript 192
Podsumowanie 194

Skorowidz .................................................................................................................. 195

10 | Spis treści
Wstęp

Wzorce to rozwiązania typowych problemów. Gdyby pójść o krok dalej, można by powie-
dzieć, że wzorce to szablony do rozwiązywania problemów z określonych kategorii.
Wzorce pomagają podzielić problem na bloki przypominające klocki Lego i skupić się na jego
unikatowych aspektach, zapewniając jednocześnie abstrakcję elementów typu „tu byłem, tamto
zrobiłem i dostałem nagrodę”.
Co więcej, pozwalają one zapewnić lepszą komunikację, oferując jednolite i powszechnie
znane słownictwo.
Warto więc rozpoznawać i studiować wzorce.

Docelowi czytelnicy
Niniejsza książka nie jest przeznaczona dla początkujących. Jej docelowymi odbiorcami po-
winni być profesjonalni programiści, którzy chcą zwiększyć swoje umiejętności w posługi-
waniu się językiem JavaScript.
Nie znajdziesz tutaj opisów wielu podstawowych elementów języka (pętli, instrukcji warun-
kowych i domknięć). Jeśli chciałbyś odświeżyć sobie te podstawowe zagadnienia, polecam
książki wymienione w dalszej części wstępu.
Z drugiej strony opisano tu pewne zagadnienia elementarne (na przykład tworzenie obiek-
tów i przenoszenie definicji zmiennych na początek funkcji), które wydają się oczywiste dla
osób znających język JavaScript. Ich opis powstał jednak z myślą o wzorcach, bo w mojej
opinii te elementy języka są wręcz niezbędne do pełnego wykorzystania jego mocy.
Jeśli szukasz najlepszych praktyk i odpowiednich wzorców, by pisać lepszy, bardziej przej-
rzysty i wydajniejszy kod JavaScript, jest to książka dla Ciebie.

11
Konwencje stosowane w książce
Książka wykorzystuje następujące konwencje typograficzne:
Kursywą
oznaczane są adresy URL, adresy e-mail, nazwy plików i ich rozszerzenia.
Pogrubieniem
oznaczane są nowe terminy lub najbardziej istotne fragmenty tekstu.
Czcionką o stałej szerokości
oznaczony jest kod programów, a także nazwy zmiennych, funkcji, typów danych i in-
strukcji umieszczone wewnątrz akapitów.
Pogrubioną czcionką o stałej szerokości
wyróżniono słowa kluczowe.
Czcionką o stałej szerokości z kursywą
oznaczane są teksty wpisywane lub zastępowane przez użytkownika, a także wartości
zależne od aktualnego kontekstu.

Tak oznaczony tekst to wskazówka lub sugestia.

Tak zapisany tekst oznacza ostrzeżenie.

Użycie przykładów zawartych w książce


Książka ma na celu pomóc Ci w rozwiązywaniu problemów programistycznych. Oznacza to,
że możesz stosować zawarty w niej kod we własnych programach lub dokumentacji. Nie
musisz prosić wydawnictwa i autora o pozwolenie, o ile reprodukcja nie obejmuje znacznej
ilości kodu. Przykładowo, napisanie własnego programu przy użyciu kilku fragmentów kodu
zapożyczonych z książki nie wymaga pozwolenia, ale sprzedaż lub dystrybucja płyty CD-ROM
z umieszczonymi tu przykładami już tak. Odpowiedzenie na pytanie cytatem z książki lub
zamieszczenie jako odpowiedzi fragmentu zawartego w niej kodu nie wymaga uzyskania
zgody, ale konieczna jest ona w przypadku skopiowania znacznej części przykładów do wła-
snej dokumentacji.
Choć nie jest to wymagane, ucieszy nas dodanie źródła fragmentu kodu. Jeśli zechcesz tak
uczynić, umieść tytuł książki, imię i nazwisko jej autora, wydawcę oraz numer ISBN.
Jeśli sądzisz, że chcesz użyć kodu zamieszczonego w książce w sposób, który może wykraczać
poza wskazane ramy, skontaktuj się z wydawnictwem.

12 | Wstęp
ROZDZIAŁ 1.

Wprowadzenie

JavaScript to język internetu. Rozpoczął swoją karierę jako sposób na modyfikację kilku wy­
branych elementów stron WWW (na przykład obrazów lub formularzy), ale od tamtego cza­
su znacząco się rozrósł. Obecnie poza skryptami uruchamianymi � rębie przeglądarki in-
ternetowej można korzystać z języka JavaScript również na wielu · platformach. Można
tworzyć kod po stronie serwera (.NET lub Node.js), aplik � opowe (działające we
��
<

wszystkich liczących się systemach operacyjnych), rozszer f>h acji (takich jak Firefox

� Vu
lub Photoshop), aplikacje dla urządzeń przenośnych i skry ersza poleceń.

JavaScript to również język nietypowy. Nie zawiera kl nkcje są w nim pierwszoplano-


wymi obiektami wykorzystywanymi do wielu za � ątkowo wielu programistów uwa-
żało ten język za niepełnowartościowy, ale w ost atach widać wyraźną zmianę podejścia.
Co ciekawe, wiele „poważnych" języków - nnymi Java lub PHP - zaczęło dodawać
�@

elementy takie jak domknięcia lub fu o imowe, choć programiści języka JavaScript

Qi
cieszą się nimi od samego początku i p ją je za standard.

JavaScript jest językiem na tyle elastyc� że w wielu aspektach można go zmodyfikować tak,

zaakceptowanie jego odm ��


by przypominał inny, wcześniej y�ęzyk programowania. Lepszym podejściem jest jednak
- �obre zapoznanie się z jego specyficznymi wzorcami.

Wzorce �
Wzorzec w bardzo ogólnym ujęciu oznacza „schemat złożony z powracających zdarzeń lub
obiektów ... Może to być szablon lub model wykorzystywany do kreowania innych rzeczy"
(http://en.wikipedia.org/wiki/Pattern).

W informatyce wzorzec to rozwiązanie typowego problemu. Nie musi on być przykładowym


kodem gotowym do skopiowania i wklejenia; stanowi raczej najlepszą praktykę, użyteczną
abstrakcję lub szablon do rozwiązywania problemów z pewnej kategorii.

Identyfikacja wzorców jest istotna z kilku powodów.


• Umożliwiają tworzenie lepszego kodu dzięki wykorzystaniu sprawdzonych praktyk za­
miast odkrywania koła na nowo.
• Zapewniają pewien poziom abstrakcji - mózg ma ograniczony zakres postrzegania, więc
jeśli zastanawiamy się nad złożonym problemem, lepiej nie skupiać się na szczegółach
niskopoziomowych, ale skorzystać z gotowych klocków (wzorców).

15
• Poprawiają komunikację w zespole, szczególnie jeśli jego członkowie znajdują się w róż­
nych miejscach na świecie i nie mają możliwości spotkania się twarzą w twarz. Umiesz­
czenie powszechnie znanej etykietki do pewnej techniki programistycznej lub podejścia
do problemu pozwala upewnić się, że wszyscy zrozumieją zagadnienie w ten sam spo­
sób. Lepiej jest przecież powiedzieć (i pomyśleć) „funkcja natychmiastowa" niż „to roz­
wiązanie, w którym umieszczasz funkcję w nawiasach okrągłych i na końcu stosujesz
jeszcze parę nawiasów, aby wywołać ją tuż po jej zdefiniowaniu".

Niniejsza książka opisuje następujące rodzaje wzorców:


• wzorce projektowe,
• wzorce kodowania,
• antywzorce.

Wzorce projektowe to wzorce zdefiniowane po raz pierwszy w książce tak zwanego gangu
czterech (ze względu na czterech autorów). Książka została opublikowana w 1994 roku pod
tytułem Design Patterns: Elements of Reusable Object-Oriented Soft1 a . Przykładami wzorców
i
projektowych są: singleton, fabryka, dekorator, obserwator itp. Pr z powiązaniem ich
z językiem JavaScript polega na tym, że choć nie są one uzależnion d ęzyka programowania,

��
tworzone były z perspektywy języków o silnej kontroli typó "-. ic:l+J C++ lub Java. Czasem
więc nie ma sensu stosować ich co do joty w języku dynami kim jak JavaScript. Niektóre
ą�tfl
�brn.
wzorce projektowe to obejścia pewnych problemów ych przez języki ze statycznie
definiowanymi typami i dziedziczeniem bazującym W JavaScripcie bardzo często ist­
'�
:{i)
nieją prostsze alternatywy. W książce w rozdziale 7. sz opis kilku wzorców projektowych.

Wzorce kodowania są znacznie bardzie · interesu·


� o wzorce specyficzne dla języka JavaScript
i dobre praktyki związane z jego unikatow m echami, na przykład różne sposoby wyko-
rzystywania funkcji. Stanowią one głó niniejszej książki.

W niektórych miejscach książki zna -tl>�


s opisy antywzorców. Określenie „antywzorce"
ma charakter negatywny, a cz se n �
obraźliwy, ale w rzeczywistości nie musi tak być.
ii
Antywzorzec to nie to samo co _Pr
ogramistyczny - to raczej rozwiązanie, które tak na-
prawdę przysporzy więcej n problemów, niż rozwiąże starych. Antywzorce zostały

JavaScript - podstawowe cechy


Prześledźmy pokrótce podstawowe cechy języka, by lepiej zrozumieć treść następnych
rozdziałów.

Zorientowany obiektowo
JavaScript jest językiem zorientowanym obiektowo, co często zaskakuje programistów, którzy
mu się wcześniej przyjrzeli i machnęli na niego ręką. W zasadzie wszystko, co zauważysz w ko­
dzie języka JavaScript, ma sporą szansę być obiektem. Jedynie pięć typów podstawowych nie
jest obiektami: liczba, ciąg znaków, wartość logiczna, null i undefined. Dodatkowo pierwsze
trzy mają swoje obiektowe reprezentacje w postaci otoczek (więcej na ten temat w następnym
rozdziale). Liczba, ciąg znaków i wartość logiczna mogą zostać łatwo zamienione w obiekt
przez programistę, a czasem są nawet zamieniane automatycznie przez interpreter języka.

16 Rozdział 1. Wprowadzenie
Funkcje również są obiektami. Mogą zawierać właściwości i metody.

Najprostszym zadaniem wykonywanym w jakimkolwiek języku jest definiowanie zmiennej.


W JavaScripcie masz wówczas tak naprawdę do czynienia z obiektami. Po pierwsze, zmienna
automatycznie staje się właściwością wewnętrznego obiektu zwanego obiektem aktywacji
(lub właściwością obiektu globalnego, jeśli jest to zmienna globalna). Po drugie, sama zmien­
na również przypomina obiekt, ponieważ zawiera własne właściwości (zwane atrybutami),
które określają, czy można ją zmieniać, usuwać lub wyliczać za pomocą pętli for-in. Atry­
buty te nie są bezpośrednio dostępne w ECMAScript 3, ale wydanie 5. udostępnia metody
zapewniające dostęp do tych właściwości.

Czym więc są obiekty? Skoro wykonują tak wiele zadań, muszą być szczególne. W rzeczywi­
stości są wyjątkowo proste. Obiekt to zbiór nazwanych właściwości - lista par klucz­
wartość (w zasadzie stanowiąca odpowiednik tablicy asocjacyjnej z innych języków progra­
mowania). Niektóre z właściwości mogą być funkcjami (obiektami funkcji), więc nazywamy
je metodami.

Ciekawe jest to, że utworzone obiekty można w dowolnym mom �


ie modyfikować (choć
ECMAScript 5 zapewnia API mogące zapobiegać zmianom). Dla � � �
ego obiektu można
dodać, usunąć lub uaktualnić jego członków (właściwości lu eto N: . Jeśli zastanawiasz się,
jak w takiej sytuacji zachować prywatność, znajdziesz tu o �"'
ełl n e wzorce zapewniające

��
ukrycie wybranych informacji.

�f
Pamiętaj także, że istnieją dwa główne rodzaje ob
rdzenne - zdefiniowane w standardzie ECM� ;
(;h;j::_ omieniowym (na przykład w przeglą-

• gospodarza - zdefiniowane w środowi'A._


darce internetowej).
U
Obiekty rdzenne można podzielić na wane (na przykład Array lub Date) i zdefinio­
wane przez użytkownika (var o = {�
Obiekty gospodarza to między · �i obiekt windowi wszystkie obiekty DOM. Jeśli zasta-
nawiasz się, czy korzystasz z zarządcy, spróbuj uruchomić kod w innym środowisku,
na przykład poza przegląda
tylko i wyłącznic obi �
·nternetową. Jeśli nadal działa prawidłowo, zapewne używasz
cnnych.

Brak klas
To stwierdzenie pojawi się jeszcze w wielu miejscach książki: w języku JavaScript nie ma klas.
To nowość dla programistów z doświadczeniem w innych językach programowania. Oducze­
nie się klas i zaakceptowanie tego, że język JavaScript ich nie posiada, wymaga kilku powtórzeń
i nieco wysiłku.

Brak klas czyni programy krótszymi - nie potrzeba klasy, by utworzyć obiekt. Przyjrzyjmy
się poniższemu zapisowi tworzącemu obiekt wskazanej klasy.
li tworzenie obiektu w języku Java
HelloOO hello_oo = new HelloOO();

Powtarzanie tego samego fragmentu trzykrotnie wydaje się przesadą, jeśli zdamy sobie
sprawę, że tworzymy prosty obiekt. Bardzo często tworzone obiekty nie są złożone.

JavaScript - podstawowe cechy 17


W języku JavaScript zaczynamy od utworzenia pustego obiektu i dodajemy do niego nowych
członków w zależności od potrzeb. Dodawanymi elementami mogą być typy proste, funkcje
lub inne obiekty zawierające własne właściwości. „Pusty" obiekt nie jest tak naprawdę pusty,
bo zawiera kilka wbudowanych właściwości, ale nie są one jego „własnością". Więcej infor­
macji na ten temat znajdziesz w następnym rozdziale.

Jedna z głównych zasad sformułowanych w książce „gangu czworga" brzmi: „preferuj kom­
pozycję obiektów zamiast dziedziczenia klas". Oznacza to, że jeśli można utworzyć obiekt
z istniejących już kawałków, uzyska się lepsze rozwiązanie, niż gdyby skorzystać z dziedzi­
czenia i długich łańcuchów rodzic-dziecko. W JavaScripcie bardzo łatwo postępować zgodnie
z tą zasadą - nie ma przecież klas, więc kompozycja obiektów to jedyne rozwiązanie.

Prototypy
JavaScript posiada mechanizm dziedziczenia, ale to tylko jeden ze sposobów wielokrotnego
użycia tego samego kodu (w książce znajduje się nawet cały rozdział poświęcony temu jed­
\i...
nemu tematowi). Dziedziczenie można uzyskać różnymi metodam �le najczęściej ma ono
postać prototypów. Prototyp jest obiektem (czyli bez zaskoczeni � � "'=i
a tworzona funkcja
automatycznie uzyskuje właściwość prototype, która wska �
e n owy pusty obiekt. Jest
on niemalże taki sam, jak gdyby utworzyć go za pomocą s 1'_
"' . �r conej lub konstruktora
Obj ect( ) , ale właściwość constructor wskazuje na utwo �� nkcję, a nie na wbudowany
obiekt Obj ect ( ) . Do tego nowego i pustego obiektu �(,lę właściwości i funkcje, a inne
(' E�
obiekty dziedziczące po nim mogą z nich korzyst by były one ich własnymi właści-

Szczegółowy opis dziedziczenia pojawi się �j części książki. Na razie zapamiętaj, że


wościami i funkcjami.
..,,,,
,,.,_
prototyp jest obiektem (a nie klasą lub innym � m tworem) i każda funkcja ma właściwość
prototype.

Środowisko \0
�c:omieniowego. Naturalnym środowiskiem dla tego ję­
...

JavaScript wymaga środo is � �


zyka jest przeglądarka in wa, ale nie jest to obecnie jedyne środowisko. Wzorce przed­
stawione w tej książc e.& ą głównie rdzenia JavaScriptu, czyli standardu ECMAScript,
który nie zależy od ś� � ska uruchomieniowego. Wyjątkami są:

• rozdział 8., który przedstawia wzorce związane z przeglądarkami;


• niektóre przykłady ilustrujące praktyczne wykorzystanie wzorca.
Środowisko uruchomieniowe może zapewniać własne obiekty gospodarza, które nie są zde­
finiowane w standardzie ECMAScript i mogą mieć nieoczekiwane i nieokreślone działanie.

ECMAScript 5
Rdzeń języka JavaScript (wyłączając DOM, BOM i inne obiekty gospodarza) bazuje na stan­
dardzie ECMAScript nazywanym w skrócie ES. Wersja 3. standardu została oficjalnie zaak­
ceptowana w 1999 roku i jest wersją jednolicie zaimplementowaną we wszystkich przeglą­
darkach. Wersję 4. porzucono, a wersja 5. została zaakceptowana w grudniu 2009 roku, czyli
10 lat po poprzedniej.

18 Rozdział 1. Wprowadzenie
Wersja S. dodaje do języka kilka wbudowanych obiektów, metod i właściwości, ale największym
dodatkiem jest tak zwany tryb ścisły (ang. strict mode), który tak naprawdę usuwa z języka pewne
.funkcje, co czyni go prostszym i bardziej odpornym na błędy. Przykładem może być polecenie with,
o którego użyteczności debatowano od lat. W ESS w trybie ścisłym jego użycie spowoduje zgło­
szenie błędu, choć można go stosować poza tym trybem. Tryb ścisły włącza zwykły ciąg znaków,
więc starsze implementacje języka po prostu go zignorują. Oznacza to, że tryb ten jest zgodny
wstecz, bo nie spowoduje zgłoszenia błędu w starszych przeglądarkach, które go nie rozumieją.

Dla każdego zakresu zmiennych (czyli na poziomie funkcji, globalnym lub na początku tekstu
przekazywanego do funkcji eval ()) można umieścić następujący tekst:
function my() {
"use strict"
11 pozostała część funkcji...

Powyższy zapis oznacza, że kod funkcji będzie wykonywany w ściślejszej wersji języka. Dla
starszych przeglądarek wygląda to po prostu jak ciąg znaków nieprzypisany do żadnej zmiennej,
więc jest on po prostu pomijany bez zgłaszania jakiegokolwiek błęd '
Plan jest taki, że w przyszłości tryb ścisły będzie jedynym dostępny�� - Można powiedzieć,
że ESS to przejściowa wersja języka - programiści są zach��i � isania kodu w wersji


zgodnej z trybem ścisłym, ale nie jest to wymóg. +

Niniejsza książka nie omawia wzorców związanych z


nymi w ESS, gdyż w momencie pisania tego tekstu i
�� ymi elementami wprowadzo­
�Yn��cja ESS jest dostępna jedynie
w niektórych najnowszych wersjach przeglądarek'1i;,r �towych, więc nie można zastosować
jej globalnie. Książka promuje jednak przesiadkę 'lłlli.,,;
i.. ;;y standard na kilka sposobów:
• Wszystkie prezentowane przykłady nie żadnych błędów w trybie ścisłym.
Unikane i wskazywane są konstru lecane, które za jakiś czas zostaną usunięte,
na przykład arguments. callee. r>�


Wykorzystywane są wzorce ES3� re mają swoje wbudowane odpowiedniki w ESS,
na przykład Obj ect. crea +

Narzędzie JSLint �

JavaScript to język in wany bez testów wykonywanych statycznie na etapie kompilacji.
Można więc umieścić w systemie produkcyjnym program z błędem tak prozaicznym jak lite­
rówka, nawet o tym nie wiedząc. W tym miejscu z pomocą wkracza JSLint.
JSLint (http://jslint.com) to narzędzie do sprawdzania jakości kodu napisane przez Douglasa
Crockforda. Analizuje ono kod i informuje o potencjalnych problemach, więc warto prześle­
dzić własny program za jego pomocą. Zgodnie z ostrzeżeniem autora narzędzie może „zranić
uczucia", ale dzieje się tak tylko na początku. Człowiek szybko uczy się na własnych błędach
i wkrótce wyrabia w sobie nawyki profesjonalnego programisty JavaScript. Brak błędów
zgłoszonych przez JSLint pozwala poczuć się pewniej, bo mamy przekonanie, że nie popeł­
niliśmy przez przypadek bardzo prostej pomyłki.

Od następnego rozdziału nazwa JSLint będzie przewijała się wielokrotnie. Cały kod umiesz­
czony w książce z sukcesem przechodzi testy narzędziem (przy jego domyślnych ustawieniach
z momentu sprawdzania) poza kilkoma wyjątkami jawnie wskazanymi jako antywzorce.

Domyślne ustawienia narzędzia wymuszają, by kod był zgodny z trybem ścisłym.

ECMAScript 5 19
Konsola
Obiekt console pojawia się w książce w wielu miejscach. Nie stanowi on części języka, ale
znajduje się w środowisku zapewnianym przez większość nowoczesnych przeglądarek.
W przeglądarce Firefox stanowi część rozszerzenia Firebug. Konsola tego dodatku zapewnia
interfejs ułatwiający szybkie wpisanie i przetestowanie fragmentu kodu, a także edycję i te­
stowanie kodu aktualnie wczytanej strony WWW (patrz rysunek 1.1). To bardzo dobre na­
rzędzie do nauki i odkrywania sposobu działania stron. Podobną funkcjonalność - jako
część narzędzi dla programistów - oferują przeglądarki WebKit (Safari i Chrome), a także
przeglądarka IE od wersji 8.

• Kon,sol'a ,,. HTML CSS Skrypt DOM Sie< p


Wyrryść Trwałość Czas wykonania I Wszystko Ermrs Warnings Info D'ebug Info

>>> ccm9Dle.l<>g!"tegt", 1, {}, [1,2,3]);


te9t 1 Object I ) [ 1, 2, 3 l
>:>> co-ngole.di:i:::�{jeden: 1, dwa: {tz:zy: 3}}),-
El d'wa Object { trzy=s l
trzy
jed�en
>:>> ::o-:i:: �vaz: i = o,- i < = 50,-

10
20
30
40
50

Najczęściej wykorzy ą metodą jest metoda log (), która po prostu wyświetla wszystkie
przekazane do niej parametry. Kilka razy wykorzystana została także metoda dir(), która
wylicza właściwości przekazanych do niej obiektów. Oto przykład użycia ich obu:
co nsole.log("test", 1, {}, [1,2,3]);
co nsole.dir({jeden: 1, dwa: {trzy: 3}});

W trakcie testowania wpisanego w konsoli kodu nie trzeba korzystać z polecenia console. log ( );
można je pominąć. By uniknąć zmniejszenia czytelności kodu, założono, że niektóre jego frag­
menty są uruchamiane z poziomu konsoli, dzięki czemu pominięto użycie jej metod:
window.name === window[ 'name']; //wynik: true

Powyższy zapis (wykonany z poziomu konsoli) jest równoważny następującemu:


co nsole.log(window.name === window['name'J);

W obu sytuacjach wynikiem jest wyświetlenie wartości true.

20 Rozdział 1. Wprowadzenie
ROZDZIAŁ 2.

Podstawy

Niniejszy rozdział omawia podstawowe najlepsze praktyki, wzorce i zwyczaje dotyczące pi-
sania wysokiej jakości kodu JavaScript. Są to między innymi unikanie zmiennych globalnych,
stosowanie pojedynczej deklaracji var, wcześniejsze zapamiętywanie długości tablicy w pę-
tlach, stosowanie konwencji kodowania i tym podobne. Rozdział omawia także pewne na-
wyki niezwiązane bezpośrednio z kodem, ale z samym procesem jego pisania, czyli tworzenie
dokumentacji dla API, przeprowadzanie oceny kodu przez współpracowników i uruchamia-
nie JSLint. Jeśli wyrobisz w sobie podobne nawyki, będziesz tworzył lepszy oraz łatwiejszy
do zrozumienia i konserwacji kod — kod, z którego będziesz dumny i który będziesz potrafił
łatwo zmodyfikować nawet wiele miesięcy później.

Tworzenie kodu łatwego w konserwacji


Koszt naprawy błędów programistycznych jest wysoki i rośnie z czasem, szczególnie jeśli
błędy zostaną zauważone w już wydanym produkcie. Najlepiej byłoby poprawić błąd od ra-
zu, czyli tuż po jego odnalezieniu — w ten sposób poświęcisz na naprawę najmniej czasu, bo
będziesz pamiętał dokładnie cały algorytm. W przeciwnym razie przejdziesz do wykonywa-
nia innych zadań i całkowicie zapomnisz szczegóły kodu. Jego analiza po dłuższym okresie
wymaga:
• czasu na ponowne zrozumienie rozwiązywanego problemu,
• czasu na zrozumienie kodu, który rozwiązuje ten problem.

Inną kwestią jest fakt, iż bardzo często (szczególnie w dużych firmach) osoba, która po-
prawia błąd, nie jest tą samą osobą, która pisała oryginalny kod (ani tą, która błąd wykryła).
Oznacza to, że należy do minimum zredukować czas niezbędny do zrozumienia kodu — czy
to pisanego przez samego siebie dawno temu, czy tworzonego przez innego członka zespołu.
Taka redukcja zarówno opłaca się firmie, jak i poprawia samopoczucie programisty, gdyż każdy
wolałby tworzyć coś nowego i ekscytującego niż spędzać godziny na analizie starego kodu.
Warto także wspomnieć, że ogólnie w tworzeniu oprogramowania znacznie więcej czasu po-
święca się na jego czytanie niż pisanie. Zdarza się, że po bardzo dobrym poznaniu problemu
i przy tak zwanej wenie twórczej można w jedno popołudnie napisać naprawdę spory ka-
wałek kodu. Tak napisany kod najprawdopodobniej zadziała, ale gdy aplikacja stanie się
bardziej dojrzała, pojawi się wiele sytuacji wymagających jego przejrzenia, dokładnej analizy
i dostosowania. Takie sytuacje mogą mieć miejsce, gdy:

21
• w kodzie znaleziono błąd;
• do aplikacji należy dodać nową funkcjonalność;
• aplikacja musi działać w nowym środowisku (bo na przykład na rynku pojawiła się nowa
przeglądarka);
• zmieniło się zastosowanie kodu;
• kod należy przepisać od podstaw lub przenieść na nową architekturę (a nawet język).

W wyniku takich zmian kilka roboczogodzin spędzonych początkowo na pisaniu kodu owocuje
roboczotygodniami związanymi z jego czytaniem. Z tego powodu tworzenie kodu łatwego
w konserwacji nierzadko stanowi o sukcesie oprogramowania.
Kod łatwy w konserwacji to kod:
• czytelny,
• jednolity,
• przewidywalny,
• wyglądający tak, jakby był pisany przez jedną osobę,
• dobrze udokumentowany.

Pozostała część rozdziału przedstawia niniejszy temat z perspektywy języka JavaScript.

Minimalizacja liczby zmiennych globalnych


Język JavaScript do zarządzania zasięgiem zmiennych wykorzystuje funkcje. Zmienna zdefi-
niowana wewnątrz funkcji jest zmienną lokalną, czyli nie jest widoczna poza ciałem funkcji.
Z drugiej strony zmienna globalna to taka, która została zadeklarowana poza funkcją lub jest
używana bez jakiejkolwiek deklaracji.
Każde środowisko JavaScript zapewnia obiekt globalny udostępniany w momencie użycia
słowa kluczowego this poza funkcją. Każda zmienna globalna staje się właściwością obiektu
globalnego. W przeglądarkach internetowych dla wygody istnieje dodatkowa właściwość
obiektu globalnego o nazwie window, która zazwyczaj wskazuje na sam obiekt globalny.
Poniższy fragment kodu prezentuje sposób tworzenia i korzystania ze zmiennych globalnych
w środowisku przeglądarki.
myglobal = "witaj"; // antywzorzec
console.log(myglobal); // "witaj"
console.log(window.myglobal); // "witaj"
console.log(window["myglobal"]); // "witaj"
console.log(this.myglobal); // "witaj"

Problem ze zmiennymi globalnymi


Zmienne globalne są problemem, ponieważ są współdzielone przez cały kod aplikacji JavaScript
lub strony WWW. Znajdują się w tej samej globalnej przestrzeni nazw, więc zawsze istnieje
ryzyko kolizji nazw, czyli sytuacji, gdy dwie różne części aplikacji mają zdefiniowane zmienne
globalne o tej samej nazwie, ale różnym przeznaczeniu.

22 | Rozdział 2. Podstawy
Często zdarza się, że strona WWW zawiera kod, który nie był pisany przez programistę
związanego z witryną, bo:
• korzysta się z zewnętrznych bibliotek JavaScript,
• uruchamia się skrypty partnera reklamowego,
• wykorzystuje się skrypty śledzące lub analityczne zewnętrznych systemów,
• umieszcza się na stronie widgety, przyciski lub inne dodatki z innych serwisów.

Przypuśćmy, że jeden z zewnętrznych skryptów zdefiniował zmienną globalną o nazwie


result. W innym, dalszym fragmencie kodu — tworzonym przez programistę witryny —
również zostanie użyta zmienna globalna o nazwie result. W efekcie ostatnie przypisanie
nadpisze wcześniejsze i najprawdopodobniej kod zewnętrznego partnera przestanie działać
prawidłowo.
Bardzo ważne jest więc zadbanie o własne podwórko i nieszkodzenie innym skryptom, które
mogą znajdować się na tej samej stronie, przez zastosowanie jak najmniejszej liczby zmien-
nych globalnych. W dalszej części książki zostaną przedstawione rozwiązania ułatwiające
minimalizację liczby tych zmiennych takie jak wzorzec przestrzeni nazw lub funkcje natych-
miastowe. Najważniejsze jest jednak, by zawsze pamiętać o deklarowaniu zmiennych przy
użyciu słowa var.
Przypadkowe utworzenie zmiennej jest wyjątkowo łatwe dzięki dwóm cechom języka JavaScript.
Po pierwsze, można w nim używać zmiennych bez ich deklarowania. Po drugie, w języku
JavaScript istnieją tak zwane dorozumiane zmienne globalne. Polega to na tym, że dowolna
zmienna, która nie zostanie jawnie zadeklarowana, staje się właściwością obiektu globalnego
(i jest dostępna w podobny sposób jak zadeklarowana zmienna globalna). Rozważmy następu-
jący przykład:
function sum(x, y) {
// antywzorzec — dorozumiana zmienna globalna
result = x + y;
return result;
}

W zaprezentowanym przykładzie użyto zmiennej result bez jej zdefiniowania. Kod działa
prawidłowo, ale po jego wykonaniu w globalnej przestrzeni nazw pojawi się jeszcze jedna
zmienna o nazwie result, co może być źródłem przyszłych problemów.
By ustrzec się kłopotów, zawsze deklaruj zmienne słowem kluczowym var w sposób przed-
stawiony w poprawionej wersji funkcji sum().
function sum(x, y) {
var result = x + y;
return result;
}

Innym antywzorcem jest tworzenie dorozumianych zmiennych globalnych w łańcuchu przy-


pisań jako części deklaracji z użyciem var. W poniższym fragmencie kodu zmienna a będzie
lokalna, ale b stanie się zmienną globalną, czego prawdopodobnie nie oczekiwano.
// antywzorzec, nie stosuj go
function foo() {
var a = b = 0;

// …
}

Minimalizacja liczby zmiennych globalnych | 23


Jeśli zastanawiasz się, dlaczego tak się dzieje, przypomnij sobie o zasadzie wykonywania
operacji od prawej strony do lewej. Najpierw zostaje wyliczone wyrażenie b = 0. W prezen-
towanym przykładzie b nie jest zadeklarowane. Wynik całej operacji, czyli wartość 0, jest na-
stępnie przypisywany do nowej, deklarowanej właśnie zmiennej o nazwie a. Wcześniejszy kod
można by zapisać następująco bez wpływania na jego działanie:
var a = (b = 0);

Jeśli zmienne zostały wcześniej zadeklarowane, wykorzystanie łańcucha przypisań nie będzie
już miało efektu ubocznego w postaci utworzenia zmiennych globalnych. Oto przykład:
function foo() {
var a, b;
// …
a = b = 0; // obie zmienne są lokalne
}

Jeszcze jednym powodem do unikania zmiennych globalnych jest przenośność kodu.


Jeśli chcesz, by działał on prawidłowo w innych środowiskach, korzystanie ze zmien-
nych globalnych jest niebezpieczne. Możesz w ten sposób przypadkowo nadpisać
obiekt gospodarza, który nie istnieje w pierwotnym środowisku (przez co założono,
że użycie takiej zmiennej jest bezpieczne), choć istnieje w innych.

Efekty uboczne pominięcia var


Istnieje pewna drobna różnica między dorozumianymi zmiennymi globalnymi a tymi zdefi-
niowanymi globalnie. Polega ona na możliwości usunięcia tych zmiennych za pomocą ope-
ratora delete.
• Zmiennych globalnych zdefiniowanych przy użyciu var (definicja umieszczona poza
jakąkolwiek funkcją) nie można usunąć.
• Dorozumiane zmienne globalne utworzone bez użycia var (niezależnie od tego, czy zo-
stały zdefiniowane poza, czy wewnątrz funkcji) można usuwać.
Ta różnica wyraźnie pokazuje, że dorozumiane zmienne globalne nie są technicznie praw-
dziwymi zmiennymi, a jedynie właściwościami obiektu globalnego. Operator delete umoż-
liwia usuwanie właściwości, ale nie zmiennych.
// definicja trzech zmiennych globalnych
var global_var = 1;
global_novar = 2; // antywzorzec
(function () {
global_fromfunc = 3; // antywzorzec
}());

// próba usunięcia
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true

// test usuwania
typeof global_var; // "number"
typeof global_novar; // "undefined"
typeof global_fromfunc; // "undefined"

W trybie ścisłym ES5 przypisania do niezadeklarowanych zmiennych (czyli oba antywzorce


przedstawione w powyższym przykładzie) spowodują zgłoszenie błędów.

24 | Rozdział 2. Podstawy
Dostęp do obiektu globalnego
W przeglądarkach internetowych obiekt globalny jest dostępny z dowolnego fragmentu ko-
du poprzez właściwość window (chyba że zrobisz coś nieoczekiwanego i zdefiniujesz zmienną
lokalną o nazwie window). W innych środowiskach ta wygodna właściwość może nosić inną
nazwę lub nawet nie być dostępna dla programisty. Jeśli chcesz mieć dostęp do obiektu glo-
balnego bez jawnego stosowania identyfikatora window, skorzystaj z poniższego kodu na do-
wolnym poziomie zagnieżdżeń funkcji.
var global = (function () {
return this;
}());

W ten sposób zawsze uzyskasz obiekt globalny, ponieważ wewnątrz funkcji wykonywanych
jako funkcje (czyli bez użycia new) this powinno zawsze na niego wskazywać. Nie jest to już
jednak prawdą w trybie ścisłym w ECMAScript 5 — w tym przypadku musisz się zastano-
wić nad innym rozwiązaniem. Jeśli piszesz bibliotekę, możesz umieścić jej kod w funkcji na-
tychmiastowej (patrz rozdział 4.), a następnie z poziomu globalnego przekazać referencję do
this jako parametr tej funkcji.

Wzorzec pojedynczego var


Stosując pojedyncze wystąpienie var na początku funkcji, wyświadczysz sobie przysługę.
Rozwiązanie to ma następujące zalety:
• Zapewnia jedno miejsce do poszukiwania wszystkich zmiennych lokalnych wymaganych
przez funkcję.
• Zapobiega błędom logicznym polegającym na tym, że chce się skorzystać ze zmiennej
przed jej zdefiniowaniem (patrz „Przenoszenie deklaracji — problem rozrzuconych de-
klaracji var”).
• Pomaga pamiętać o deklarowaniu zmiennych, więc minimalizuje ryzyko utworzenia
zmiennych globalnych.
• Zmniejsza ilość kodu (zarówno przy jego pisaniu, jak i przesyle).

Wzorzec pojedynczego użycia var wygląda następująco:


function func() {
var a = 1,
b = 2,
sum = a + b,
myobject = {},
i,
j;

// treść funkcji…
}

Można użyć jednego polecenia var do zadeklarowania wielu zmiennych oddzielonych prze-
cinkami. Dobrą praktyką jest również inicjalizacja zmiennej wartością początkową w mo-
mencie deklaracji. Pozwala to uniknąć błędów logicznych (wszystkie zadeklarowane zmienne
bez inicjalizacji mają przypisaną wartość undefined) i poprawia czytelność kodu. Gdy analizuje
się kod po kilku tygodniach, dużo łatwiej jest zrozumieć znaczenie poszczególnych zmien-
nych, jeśli mają przypisane wartości początkowe — nie trzeba już sobie zadawać pytania, czy
to obiekt, czy może liczba całkowita.

Minimalizacja liczby zmiennych globalnych | 25


W momencie deklaracji można również wykonać rzeczywistą pracę, na przykład zsumować
dwie wartości (jak w powyższym kodzie). Innym często spotykanym przykładem jest pobie-
ranie niezbędnych referencji do obiektów DOM (Document Object Model). Nic nie stoi na
przeszkodzie, by jednocześnie zadeklarować zmienną i przypisać jej referencję do obiektu
DOM. Rozwiązanie to obrazuje poniższy kod.
function updateElement() {
var el = document.getElementById("result"),
style = el.style;

// wykonaj działania na el i style…


}

Przenoszenie deklaracji — problem rozrzuconych deklaracji var


JavaScript umożliwia stosowanie wielu poleceń var w dowolnym miejscu funkcji, ale w rze-
czywistości daje to taki sam efekt, jakby wszystkie zmienne zadeklarowano na jej początku.
Jest to tak zwane przenoszenie deklaracji (ang. hoisting). Działanie to może prowadzić do
błędów logicznych polegających na tym, że korzysta się ze zmiennej, a następnie się ją dekla-
ruje. W języku JavaScript, jeśli zmienna znajduje się w tym samym zasięgu zmiennych (w tej
samej funkcji), jest uważana za zadeklarowaną, nawet gdy zostanie użyta przed pojawieniem
się deklaracji z instrukcją var. Prześledźmy poniższy przykład.
// antywzorzec
myname = "global"; // zmienna globalna
function func() {
alert(myname); // "undefined"
var myname = "local";
alert(myname); // "local"
}
func();

Można by sądzić, że w zaprezentowanym przykładzie pierwsze wywołanie funkcji alert()


spowoduje wyświetlenie tekstu „global”, a drugie tekstu „local”. To rozumowanie ma swoje
uzasadnienie: jako że w momencie pierwszego wywołania zmienna myname nie została jesz-
cze zadeklarowana, funkcja powinna prawdopodobnie „widzieć” zmienną globalną myname.
Niestety, kod nie zadziała tak, jak podpowiada intuicja. Pierwsze wywołanie poinformuje
o wartości undefined, ponieważ myname zostanie potraktowana jako zmienna lokalna funkcji
(mimo tego, że deklaracja pojawia się dalej). Wszystkie deklaracje zmiennych zostają przenie-
sione na początek funkcji, więc aby uniknąć tego rodzaju nieporozumień, warto od razu za-
deklarować wszystkie zmienne właśnie tam.
Wcześniejszy fragment kodu zadziała tak, jakby został napisany w poniższy sposób.
myname = "global"; // zmienna globalna
function func() {
var myname; // równoważne zapisowi var myname = undefined;
alert(myname); // "undefined"
myname = "local";
alert(myname); // "local"
}
func();

26 | Rozdział 2. Podstawy
Dla kompletności opisu warto wspomnieć, że w rzeczywistości dla niskopoziomo-
wej implementacji wszystko przebiega w nieco bardziej złożony sposób. Istnieją dwa
etapy analizy kodu. W pierwszym tworzone są zmienne, deklaracje funkcji i para-
metry formalne — jest to etap wchodzenia do kontekstu i analizy składniowej.
W drugim etapie, dotyczącym właściwego wykonania kodu, tworzone są wyraże-
nia funkcji i niezadeklarowane zmienne. Tak naprawdę w standardzie ECMAScript nie
istnieje pojęcie przenoszenia deklaracji, ale opisany algorytm daje w praktyce właśnie
taki efekt.

Pętle for
W pętlach for iteruje się po elementach tablic lub obiektów przypominających tablice takich
jak obiekty arguments i HTMLCollection. Typowa pętla for wygląda następująco:
// pętla niezbyt dobrze zoptymalizowana
for (var i = 0; i < myarray.length; i++) {
// wykonaj działania na myarray[i]
}

Problem polega na tym, że długość tablicy jest pobierana przy każdej iteracji pętli. To może
spowolnić wykonywanie kodu, szczególnie jeśli myarray nie jest zwykłą tablicą, ale obiektem
HTMLCollection.

Obiekty HTMLCollection zwracają między innymi następujące metody DOM:


• document.getElementsByName(),
• document.getElementsByClassName(),
• document.getElementsByTagName().

Istnieje również kilka innych źródeł obiektów HTMLCollection, które powstały co prawda
przed standardem DOM, ale nadal są wykorzystywane przez niektóre witryny. Oto kilka z nich:
• document.images — wszystkie elementy <img> na stronie WWW;
• document.links — wszystkie elementy <a>;
• document.forms — wszystkie formularze;
• document.forms[0].elements — wszystkie pola pierwszego formularza na stronie WWW.

Problem z obiektami kolekcji polega na tym, że są to obsługiwane na bieżąco zapytania doty-


czące aktualnej struktury dokumentu (strony HTML). Oznacza to, że za każdym razem, gdy
pobiera się wartość właściwości length obiektu kolekcji, w rzeczywistości wykonuje się za-
pytanie dotyczące struktury DOM, a takie operacje najczęściej są bardzo kosztowne.
Z podanych powodów lepiej jest więc zapamiętywać długość tablicy (lub kolekcji) w osobnej
zmiennej, co przedstawia poniższy przykład.
for (var i = 0, max = myarray.length; i < max; i++) {
// wykonaj operacje na myarray[i]
}

W ten sposób długość tablicy odczytuje się tylko jeden raz, a następnie wykorzystuje się ją
w każdej iteracji pętli.

Pętle for | 27
Zapamiętanie długości w trakcie iteracji po obiektach HTMLCollection jest szybsze we
wszystkich przeglądarkach internetowych (choć najbardziej dotyczy to ich starszych wersji)
— czasem przyspieszenie jest tylko dwukrotne (Safari 3), a czasem pętla okazuje się 190 razy
szybsza (IE7). Więcej informacji na ten temat znajdziesz w książce: High Performance JavaScript,
Nicholas Zakas (O’Reilly).
Oczywiście pamiętaj o tym, że jeśli chcesz celowo modyfikować w pętli zawartość kolekcji
(na przykład dodawać nowe elementy), prawdopodobnie będzie potrzebny odczyt stale ak-
tualizowanej wartości length.
Wykorzystując wzorzec jednego polecenia var, można usunąć element var z pętli i zapisać ją
następująco:
function looper() {
var i = 0,
max,
myarray = [];

// …

for (i = 0, max = myarray.length; i < max; i++) {


// wykonaj operacje na myarray[i]
}
}

Przedstawiony wzorzec zapewnia spójność kodu, gdyż dopasowuje się do wzorca jednego
polecenia var. Wadą jest nieco utrudnione przenoszenie całych pętli w momencie refaktory-
zacji kodu. Jeśli kopiuje się pętlę z jednej funkcji do drugiej, trzeba pamiętać o przeniesieniu
w nowe miejsce deklaracji zmiennych i oraz max (a także prawdopodobnie o usunięciu ich
z poprzedniej funkcji, jeśli nie są tam wykorzystywane).
Jedną z ostatnich poprawek w pętli mogłoby być zastąpienie fragmentu i++ przez jedno
z poniższych wyrażeń.
i = i + 1
i += 1

JSLint prosi o dokonanie wspomnianej zmiany. Prośba o zmianę elementów ++ i -- wynika


z chęci uniknięcia „zbyt wyrafinowanych sztuczek”. Jeśli nie zgadzasz się z tą propozycją,
możesz ustawić opcję plusplus narzędzia na wartość false (domyślnie ma wartość true).
W dalszej części książki pojawiał się będzie drugi z przedstawionych wzorców, czyli i += 1.
Dwie odmiany pętli for wprowadzają dodatkowe mikrooptymalizacje, ponieważ:
• używają o jedną zmienną mniej (brak max);
• odliczają do 0, co najczęściej jest szybsze, gdyż łatwiej przyrównać coś do zera niż do
długości tablicy lub innej wartości.
Pierwsza z odmian ma postać:
var i, myarray = [];

for (i = myarray.length; i--;) {


// wykonaj operacje na myarray[i]
}

28 | Rozdział 2. Podstawy
Druga wykorzystuje pętlę while:
var myarray = [],
i = myarray.length;

while (i--) {
// wykonaj operacje na myarray[i]
}

Zysk z tych dodatkowych optymalizacji będzie można zauważyć dopiero w pętlach krytycz-
nych ze względu na wydajność. Warto także przypomnieć, że narzędzie JSLint będzie do-
myślnie proponowało zmianę i--.

Pętle for-in
Pętle for-in należy wykorzystywać do iteracji po obiektach niebędących tablicami. Pętla
wykorzystująca tę formę nazywana jest często wyliczeniem.
Z technicznego punktu widzenia pętlę for-in można wykorzystać również dla tablic (po-
nieważ w języku JavaScript są one obiektami), ale nie jest to zalecane. Może to prowadzić do
błędów logicznych, jeśli obiekt tablicy został zmodyfikowany w celu dodania własnej funk-
cjonalności. Co więcej, pętla for-in nie gwarantuje przechodzenia przez właściwości w jed-
nym ustalonym porządku (po kolei). Z tych powodów dla tablic warto stosować zwykłą pętlę
for, a pętlę for-in pozostawić dla obiektów.

W trakcie iteracji po właściwościach obiektu niezwykle istotne jest użycie metody


hasOwnProperty(), by wyfiltrować właściwości pochodzące z łańcucha prototypowego.

Rozważmy następujący przykład:


// obiekt
var man = {
hands: 2,
legs: 2,
heads: 1
};

// w innej części kodu


// do wszystkich obiektów została dodana nowa metoda
if (typeof Object.prototype.clone === "undefined") {
Object.prototype.clone = function () {};
}

W tym przykładzie mamy prosty obiekt o nazwie man zdefiniowany za pomocą składni skró-
conej (literału). Gdzieś przed lub po definicji man do prototypu obiektu Object dodano uży-
teczną metodę o nazwie clone(). Ponieważ łańcuch prototypów działa na bieżąco, wszystkie
obiekty automatycznie uzyskają dostęp do nowej metody. By metoda clone() nie pojawiła się
w momencie wyliczania właściwości obiektu man, należy wykonać metodę hasOwnProperty()
w celu wyfiltrowania właściwości pochodzących z prototypu. Gdyby tego nie uczyniono, metoda
clone() pojawiłaby się na liście wyników, co najczęściej nie jest pożądane.
// 1.
// pętla for-in
for (var i in man) {
if (man.hasOwnProperty(i)) { // filtr
console.log(i, ":", man[i]);
}
}

Pętle for-in | 29
/*
Wynik w konsoli:
hands : 2
legs : 2
heads : 1
*/

// 2.
// antywzorzec:
// pętla for-in bez filtracji przy użyciu metody hasOwnProperty()
for (var i in man) {
console.log(i, ":", man[i]);
}
/*
Wynik w konsoli:
hands : 2
legs : 2
heads : 1
clone: function()
*/

Innym wzorcem jest wywoływanie metody hasOwnProperty() z poziomu obiektu Object.


´prototype, czyli w sposób przedstawiony poniżej.
for (var i in man) {
if (Object.prototype.hasOwnProperty.call(man, i)) { // filtr
console.log(i, ":", man[i]);
}
}

Zaletą tego rozwiązania jest fakt, iż unika się kolizji nazw, jeśli z jakichś powodów obiekt man
przedefiniował hasOwnProperty. Aby uniknąć długiego łańcucha wyszukiwań właściwości,
warto zapamiętać funkcję w zmiennej lokalnej.
var i,
hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
if (hasOwn.call(man, i)) { // filtr
console.log(i, ":", man[i]);
}
}

Z technicznego punktu widzenia nieskorzystanie z hasOwnProperty() nie jest błę-


dem. W zależności od zadania i zaufania do kodu można pominąć test i nieco
przyspieszyć działanie pętli. Jeśli jednak nie ma się pewności co do zawartości obiektu
(lub jego łańcucha prototypów), bezpieczniej jest dodać dodatkowy test w postaci
hasOwnProperty().

Pewną odmianą formatowania (która jednak zgłasza błąd w narzędziu JSLint) jest pominięcie
nawiasów klamrowych i umieszczenie warunku if w tym samym wierszu. Zaletą tego jest
fakt, iż po takiej modyfikacji pętla z warunkiem wygląda jak jedna spójna myśl („dla każdej
własnej właściwości obiektu X wykonaj operację Y”). Dodatkowo można uniknąć jednego
poziomu wcięć.
// ostrzeżenie: zgłasza błąd w narzędziu JSLint
var i,
hasOwn = Object.prototype.hasOwnProperty;
for (i in man) if (hasOwn.call(man, i)) { // filtr
console.log(i, ":", man[i]);
}

30 | Rozdział 2. Podstawy
Modyfikacja wbudowanych prototypów
Modyfikacja właściwości prototype funkcji konstruujących obiekty to wygodny i elastyczny
sposób na dodawanie nowych funkcjonalności. Czasem jednak okazuje się on zbyt potężny.
Modyfikowanie prototypów obiektów wbudowanych takich jak Object, Array lub Function
jest kuszące, ale w praktyce znacząco utrudni konserwację kodu, bo stanie się on mniej
przewidywalny. Inni programiści korzystający z utworzonego kodu zapewne będą oczeki-
wali jednolicie działających obiektów wbudowanych bez żadnych dodatków.
Co więcej, właściwości dodane do prototypu mogą pojawić się w pętlach, które nie zostały
zabezpieczone testem wykorzystującym hasOwnProperty(), co może prowadzić do dodat-
kowej konsternacji.
Z podanych powodów lepiej nie modyfikować wbudowanych prototypów. Wyjątek od tej
reguły stanowią sytuacje, w których spełnione zostaną wszystkie poniższe warunki.
1. Oczekuje się, że wszystkie przyszłe wersje języka ECMAScript lub JavaScript wprowadzą
określoną funkcjonalność jako metodę wbudowaną, a jej implementacje będą działały
identycznie. Przykładowo, można zaimplementować metody opisywane w specyfikacji
standardu ECMAScript w sytuacji, gdy oczekuje się na ich implementację w przeglądarkach.
W ten sposób po prostu przygotowujemy się do wykorzystania dostępnych wkrótce metod
wbudowanych.
2. Sprawdzi się, czy tworzona metoda lub właściwość już nie istnieje — być może została
dodana przez inną wykorzystywaną na stronie bibliotekę lub też została udostępniona
przez przeglądarkę jako część nowszego interpretera JavaScript.
3. Jasno i wyraźnie poinformuje się cały zespół o wprowadzeniu takiej metody lub właściwości.
W przypadku spełnienia tych trzech warunków można dodać własny element do prototypu,
stosując następujący wzorzec:
if (typeof Object.prototype.myMethod !== "function") {
Object.prototype.myMethod = function () {
// implementacja…
};
}

Wzorzec konstrukcji switch


Czytelność kodu i jego odporność na błędy związane z konstrukcją switch zwiększy zasto-
sowanie poniższego wzorca.
var inspect_me = 0,
result = '';

switch (inspect_me) {
case 0:
result = "zero";
break;
case 1:
result = "jeden";
break;
default:
result = "nieznany";
}

Wzorzec konstrukcji switch | 31


Konwencje stylistyczne zastosowane w tym prostym przykładzie są następujące:
• Każdy element case znajduje się na tym samym poziomie co switch (wyjątek od reguły
dotyczącej wcięć wewnątrz nawiasów klamrowych).
• Wcięcia stosowane są dla kodu dotyczącego poszczególnych elementów case.
• Kończenie każdego elementu case jawnym poleceniem break;.
• Unikanie zamierzonych przejść do następnego elementu case (przez pominięcie polece-
nia break); jeśli jednak takie przejścia są najlepszym rozwiązaniem, należy jasno wskazać
ich użycie w komentarzu, ponieważ dla innej osoby mogą one wyglądać jak błąd.
• Kończenie konstrukcji switch elementem default: w celu zyskania pewności, że wynik
zawsze będzie poprawny, nawet jeśli nie znaleziono dopasowania.

Unikanie niejawnego rzutowania


JavaScript niejawnie rzutuje zmienne, gdy są one porównywane. Właśnie z tego powodu po-
równania takie jak false == 0 lub "" == 0 są uznawane za prawdziwe.
Aby uniknąć nieporozumień związanych z niejawnym rzutowaniem, zawsze korzystaj z ope-
ratorów === lub !==, które sprawdzają zarówno wartość, jak i typ porównywanego wyrażenia.
var zero = 0;
if (zero === false) {
// nie wykonuje się, ponieważ zero to 0, a nie false
}

// antywzorzec
if (zero == false) {
// ten blok kodu wykona się…
}

Istnieje jeszcze jedno podejście, które zakłada, że w sytuacjach, w których wystarczy ==, nie
trzeba używać ===. Przykładem takiej sytuacji jest sprawdzanie wyniku operacji typeof, o której
wiadomo, że zawsze zwraca tekst. Narzędzie JSLint wymaga jednak ścisłego trzymania się
zasady równości bez rzutowania. Co więcej, taki kod jest spójny i zmniejsza się wysiłek umy-
słowy związany z jego czytaniem (czy w tym miejscu == to celowe działanie, czy błąd?).

Unikanie eval()
Jeśli zauważysz w kodzie użycie funkcji eval(), pamiętaj, że należy go za wszelką cenę unikać.
Funkcja przyjmuje dowolny kod jako tekst i wykonuje go tak, jakby był kodem JavaScript.
Jeśli kod poddawany takiej operacji jest znany wcześniej (przed uruchomieniem skryptu), nie
ma powodu, by używać funkcji eval(). W przypadku gdy jest on dynamicznie generowany
w trakcie działania skryptu, najczęściej istnieją inne, lepsze sposoby osiągnięcia celu niż
wspomniana funkcja. Przykładowo, uzyskanie dostępu do dynamicznie generowanych wła-
ściwości za pomocą nawiasów kwadratowych to lepsze i prostsze rozwiązanie.
// antywzorzec
var property = "name";
alert(eval("obj." + property));

// rozwiązanie zalecane
var property = "name";
alert(obj[property]);

32 | Rozdział 2. Podstawy
Korzystanie z eval() ma swoje implikacje związane z bezpieczeństwem, ponieważ można
w ten sposób wykonać kod (na przykład pobrany osobnym poleceniem z internetu), nad którym
nie ma się kontroli lub który został zmieniony w trakcie transportu. To typowy antywzorzec
w przypadku korzystania z odpowiedzi w formacie JSON przesłanych techniką Ajax. W ta-
kiej sytuacji najlepiej skorzystać z wbudowanej w przeglądarkę metody konwersji formatu
JSON na obiekt, ponieważ to rozwiązanie jest bezpieczne i prawidłowe. Jeśli przeglądarka
nie zapewnia wbudowanej metody JSON.parse(), skorzystaj z biblioteki dostępnej w witrynie
JSON.org.
Warto również pamiętać, że przekazywanie tekstu do funkcji setInterval() i setTimeout()
oraz konstruktora Function() jest bardzo podobne do użycia funkcji eval(), więc również
należy tego unikać. JavaScript w rzeczywistości musi przekonwertować przekazany tekst na kod,
a następnie go wykonać.
// antywzorzec
setTimeout("myFunc()", 1000);
setTimeout("myFunc(1, 2, 3)", 1000);

// rozwiązania zalecane
setTimeout(myFunc, 1000);
setTimeout(function () {
myFunc(1, 2, 3);
}, 1000);

Użycie konstruktora new Function() jest bardzo podobne do korzystania z eval() i należy
do niego podchodzić ostrożnie. To bardzo elastyczna technika, ale bywa nadużywana. Jeśli
już musisz skorzystać z któregoś z tych dwóch rozwiązań, wybierz new Function(). Zaletą
tej techniki jest fakt, iż uzyskany w ten sposób kod będzie uruchamiany w lokalnej funkcji,
więc wszystkie zmienne zadeklarowane z użyciem var nie staną się od razu zmiennymi glo-
balnymi. Innym rozwiązaniem zapobiegającym automatycznemu tworzeniu zmiennych glo-
balnych jest otoczenie wywołania eval() funkcją natychmiastową (więcej informacji na temat
takich funkcji w rozdziale 4.).
Rozważmy poniższy przykład. Po jego wykonaniu w globalnej przestrzeni nazw znajdzie się
tylko zmienna un.
console.log(typeof un); // "undefined"
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"

var jsstring = "var un = 1; console.log(un);";


eval(jsstring); // wyświetla "1"

jsstring = "var deux = 2; console.log(deux);";


new Function(jsstring)(); // wyświetla "2"

jsstring = "var trois = 3; console.log(trois);";


(function () {
eval(jsstring);
}()); // wyświetla "3"

console.log(typeof un); // "number"


console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"

Inną różnicą między eval() i konstruktorem Function jest fakt, iż eval() może wpływać na
łańcuch zakresów zmiennych, natomiast Function wykonuje kod w bardziej zabezpieczonej
„piaskownicy”. Niezależnie od tego, gdzie wykona się kod uzyskany dzięki Function,

Unikanie eval() | 33
będzie on miał dostęp tylko i wyłącznie do zmiennych globalnych, nie będzie więc w stanie
zanieczyścić lub uszkodzić zmiennych lokalnych. W poniższym przykładzie eval() może
uzyskać dostęp do zmiennej spoza swojego zakresu lub ją zmodyfikować, ale Function nie
daje takiej możliwości (zauważ również, że użycie Function i new Function ma identyczne
skutki).
(function () {
var local = 1;
eval("local = 3; console.log(local)"); // wyświetla 3
console.log(local); // wyświetla 3
}());

(function () {
var local = 1;
Function("console.log(typeof local);")(); // wyświetla "undefined"
}());

Konwertowanie liczb funkcją parseInt()


Funkcja parseInt() umożliwia wydobycie z tekstu wartości liczbowej. Przyjmuje ona także
drugi parametr określający podstawę. Bardzo często się go pomija, choć to duży błąd. Schody
zaczynają się w momencie, gdy tekst do przetworzenia rozpoczyna się od 0, na przykład od
miesiąca wpisanego z początkowym zerem. Funkcja w wersji ze specyfikacji ECMAScript 3
traktuje taki tekst, jakby był zapisany w formacie ósemkowym (podstawa wynosi 8). ES5
zmieniło to domyślne zachowanie. Aby uniknąć nieścisłości i nieoczekiwanych wyników,
zawsze podawaj podstawę jako drugi parametr funkcji.
var month = "06",
year = "09";
month = parseInt(month, 10);
year = parseInt(year, 10);

Jeśli w przedstawionym przykładzie pominie się parametr określający podstawę, czyli napisze
się parseInt(year), zwróconą wartością będzie 0. Wynika to z faktu, iż "09" traktowane jako
wartość ósemkowa (czyli równoważnie z zapisem parseInt(year, 8)) nie jest wartością
poprawną.
Alternatywnymi sposobami konwersji tekstu na liczbę są następujące wiersze kodu:
+"08" // wynikiem jest 8
Number("08") // 8

Co więcej, przedstawione alternatywy są najczęściej szybsze od parseInt(), gdyż — jak sama


nazwa wskazuje — funkcja ta analizuje tekst, a nie tylko go konwertuje. Jeśli jednak oczekuje
się tekstu w postaci „09 witaj”, parseInt() zadziała i zwróci liczbę, natomiast alternatywy
zwrócą wartość NaN.

Konwencje dotyczące kodu


Bardzo istotne jest ustalenie jednej konwencji pisania kodu — dzięki temu tworzony kod jest
jednolity, przewidywalny i znacznie łatwiejszy do czytania. Nowy programista dołączający
do zespołu może przeczytać opis konwencji i znacznie szybciej uzyskać pełną produktywność,
rozumiejąc kod napisany przez innych jego członków.

34 | Rozdział 2. Podstawy
Wiele gorących dyskusji i żywiołowych spotkań spowodowanych było wzajemnym udo-
wadnianiem sobie wyższości jednej konwencji nad drugą (niekończącą się debatę wywołuje
na przykład proste pytanie: spacje czy znaki tabulacji?). Jeśli więc jesteś osobą odpowiedzialną
za określenie i wprowadzenie konwencji, spodziewaj się oporu i wielu przykładów wyższo-
ści innych rozwiązań. Pamiętaj też jednak, że o wiele ważniejsze od szczegółów konwencji
(jakakolwiek by ona nie była) jest jej bezwzględne przestrzeganie.

Wcięcia
Kod bez wcięć jest niemalże niemożliwy do odczytania. Jest jednak coś gorszego: kod z nie-
spójnymi wcięciami, który wydaje się podporządkowywać pewnej konwencji, ale od czasu
do czasu zawiera zdradliwe pułapki. Wcięcia muszą podlegać standaryzacji.
Niektórzy programiści wolą wcięcia złożone ze znaków tabulacji, ponieważ mogą dostoso-
wać ich wielkość do własnych preferencji w edytorze. Inni wolą spacje, najczęściej cztery. To,
które rozwiązanie zostało wybrane, nie ma dużego znaczenia, o ile tylko wszyscy go prze-
strzegają. W niniejszej książce stosowane są cztery znaki spacji, co jest również wartością
domyślną w narzędziu JSLint.
Jakie elementy powinny zostać oznaczone wcięciem? Zasada jest prosta: wszystkie znajdują-
ce się w nawiasach klamrowych. Oznacza to treść funkcji, zawartość pętli (do, while, for,
for-in), instrukcji if, switch oraz właściwości obiektów definiowanych w notacji skróconej.
Poniższy kod przedstawia przykłady poprawnego użycia wcięć.
function outer(a, b) {
var c = 1,
d = 2,
inner;
if (a > b) {
inner = function () {
return {
r: c - d
};
};
} else {
inner = function () {
return {
r: c + d
};
};
}
return inner;
}

Nawiasy klamrowe
Nawiasy klamrowe należy stosować zawsze, nawet jeśli w danej sytuacji są opcjonalne. Teo-
retycznie, jeśli mamy do czynienia tylko z jednym poleceniem wewnątrz instrukcji if lub for,
można je pominąć, są jednak powody, dla których nie warto tego robić: kod z nawiasami jest
bardziej spójny i łatwiejszy do aktualizacji.
Wyobraź sobie, że pętla for zawiera tylko jedno polecenie. W tej sytuacji można pominąć
nawiasy klamrowe bez wprowadzania jakiegokolwiek błędu.

Konwencje dotyczące kodu | 35


// zła praktyka
for (var i = 0; i < 10; i += 1)
alert(i);

Co się jednak stanie, jeśli za jakiś czas do pętli zostanie dodany jeszcze jeden wiersz?
// zła praktyka
for (var i = 0; i < 10; i += 1)
alert(i);
alert(i + " jest " + (i % 2 ? "nieparzyste" : "parzyste"));

Drugie wywołanie funkcji alert() znajduje się poza pętlą, mimo że wcięcie sugeruje coś in-
nego. Z tego powodu lepiej zawsze korzystać z nawiasów klamrowych, nawet w przypadku
„jednolinijkowców”.
// lepiej
for (var i = 0; i < 10; i += 1) {
alert(i);
}

Podobna sytuacja ma miejsce w przypadku instrukcji warunkowych if.


// źle
if (true)
alert(1);
else
alert(2);

// lepiej
if (true) {
alert(1);
} else {
alert(2);
}

Położenie nawiasu otwierającego


Programiści mają również preferencje co do miejsca umieszczenia otwierającego nawiasu
klamrowego (w tym samym wierszu czy w następnym?):
if (true) {
alert("To prawda!");
}

lub
if (true)
{
alert("To prawda!");
}

W tym konkretnym przykładzie wybór rozwiązania zależy od osobistych upodobań, ale są


sytuacje, w których program może działać nieco inaczej w zależności od rozmieszczenia
nawiasów. Wynika to z faktu istnienia w języku JavaScript mechanizmu automatycznego
wstawiania średników — jeśli koniec wiersza nie zawiera średnika, a wygląda na poprawny
koniec polecenia, JavaScript sam wstawi średnik. To zachowanie potrafi sprawiać problemy,
gdy funkcja zwraca obiekt definiowany bezpośrednio, a nawias otwierający nie znajduje się
w tym samym wierszu.
// ostrzeżenie: nieoczekiwany wynik funkcji
function func() {
return

36 | Rozdział 2. Podstawy
{
name: "Batman"
};
}

Jeśli oczekuje się, że funkcja zwróci obiekt z właściwością name, można się rozczarować.
Z powodu niejawnego wstawiania średników zwróci ona wartość undefined. Dla interpretera
powyższy kod jest równoważny następującemu:
// ostrzeżenie: nieoczekiwany wynik funkcji
function func() {
return undefined;
// poniższy kod nie zostanie wykonany...
{
name: "Batman"
};
}

Wniosek jest prosty: zawsze stosuj nawiasy klamrowe i umieszczaj je w tym samym wierszu
co poprzednią instrukcję:
function func() {
return {
name: "Batman"
};
}

Dodatkowa wskazówka dotycząca średników: podobnie jak nawiasy klamrowe,


zawsze stosuj średniki, nawet jeśli są uzupełniane przez JavaScript. Nie tylko pro-
muje to dyscyplinę i jednolitość kodu, ale w niektórych sytuacjach jasno określa in-
tencję programisty.

Białe spacje
Użycie białych spacji również wpływa na czytelność i jednolitość kodu. W języku pisanym
po przecinkach i kropkach występują odstępy. W języku JavaScript mamy do czynienia z po-
dobną logiką i dodawaniem odstępów po wyrażeniach dotyczących list (równoważne prze-
cinkom) oraz na końcu poleceń (równoważne zakończeniu pewnej „myśli”).
Dobrymi przykładami użycia białych spacji są między innymi następujące przypadki:
• Poszczególne części składowe pętli for oddzielane średnikami — for (var i = 0; i < 10;
i += 1) {...}.
• Inicjalizacja wielu zmiennych (i oraz max) w pętli for — for (var i = 0, max = 10;
i < max; i += 1) {...}.
• Przecinki oddzielające elementy tablicy — var a = [1, 2, 3];.
• Przecinki oddzielające definicje właściwości literałów obiektów oraz dwukropki oddzie-
lające nazwę właściwości od jej wartości — var o = {a: 1, b: 2};.
• Przecinki oddzielające argumenty funkcji — myFunc(a, b, c).
• Odstępy przed nawiasami klamrowymi w deklaracji funkcji — function myFunc() {}.
• Odstępy po słowie function w anonimowym wyrażeniu funkcji — var myFunc =
function() {}; .

Konwencje dotyczące kodu | 37


Innym dobrym przykładem użycia białych spacji jest oddzielanie nimi wszystkich operato-
rów od ich operandów. Oznacza to stosowanie spacji przed i po następujących operatorach:
+, -, *, =, <, >, <=, >=, ===, !==, &&, ++, += i tak dalej.
// jednolite i częste stosowanie spacji
// czyni kod bardziej czytelnym,
// zapewniając mu miejsce do "oddychania"
var d = 0,
a = b + 1;
if (a && b && c) {
d = a % c;
a += d;
}

// antywzorzec
// brakujące lub niejednorodne spacje
// czynią kod trudniejszym w analizie
var d= 0,
a =b+1;
if (a&& b&&c) {
d=a %c;
a+= d;
}

Ostatnia uwaga na temat białych spacji dotyczy ich stosowania w obrębie nawiasów klam-
rowych. Dobrze jest stosować spacje:
• przed nawiasami otwierającymi ({) funkcje, instrukcje warunkowe, pętle i literały obiektów;
• między nawiasem zamykającym (}) i instrukcjami else oraz while.

Liberalne korzystanie ze spacji może doprowadzić do zwiększenia rozmiaru pliku, ale mini-
fikacja (omawiana w dalszej części rozdziału) znakomicie rozwiązuje ten problem.

Często niedocenianym sposobem zwiększania czytelności kodu są odstępy między


wierszami. Poszczególne logicznie powiązane jednostki kodu oddziela się pustym wier-
szem na wzór akapitów w literaturze, które służą do oddzielania poszczególnych myśli.

Konwencje nazewnictwa
Kolejnym sposobem zwiększenia czytelności i łatwości konserwacji kodu jest zastosowanie
konwencji nazewnictwa. Oznacza to wybieranie nazw zmiennych i funkcji w sposób jednolity
i logiczny.
Poniżej znajdują się opisy kilku sugerowanych konwencji, które można zastosować w pre-
zentowanej postaci lub dostosować do własnych potrzeb. Pamiętaj, że posiadanie konwencji
i stosowanie jej w jednolity sposób jest ważniejsze od tego, jak dana konwencja wygląda.

Konstruktory pisane od wielkiej litery


W języku JavaScript nie ma klas, ale istnieją funkcje konstruujące wywoływane z operatorem new.
var adam = new Person();

Ponieważ konstruktory nadal są zwykłymi funkcjami, warto, by samo spojrzenie na nazwę


informowało, że mamy do czynienia z konstruktorem, a nie typową funkcją.

38 | Rozdział 2. Podstawy
Pisanie nazw konstruktorów od wielkiej litery zapewnia odpowiednią wskazówkę. Zastoso-
wanie małej litery na początku funkcji i metod wskazuje, że nie należy ich używać w połą-
czeniu z operatorem new.
function MyConstructor() {...}
function myFunction() {...}

W następnym rozdziale przedstawione zostaną wzorce umożliwiające programowe wymu-


szenie użycia konstruktora jako konstruktora, ale ta prosta konwencja nazewnictwa stanowi
odpowiednio wyraźną wskazówkę dla osób czytających kod źródłowy.

Oddzielanie wyrazów
Jeśli nazwa zmiennej lub funkcji składa się z kilku wyrazów, dobrym pomysłem jest stoso-
wanie się do określonej konwencji ich oddzielania. Najczęściej stosowaną konwencją jest tak
zwany styl wielbłądzi. W konwencji tej wyrazów się nie rozdziela, ale pierwszą literę każ-
dego z nich pisze się wielką literą.
W przypadku konstruktorów wszystkie wyrazy powinny mieć dużą pierwszą literę, na
przykład MyConstructor(). W przypadku funkcji i metod pierwszy wyraz pisany jest w ca-
łości małymi literami, na przykład myFunction(), calculateArea() i getFirstName().
A co ze zmiennymi, które nie są funkcjami? Programiści najczęściej stosują dla nich taką sa-
mą konwencję jak w przypadku nazw funkcji, ale istnieje rozwiązanie alternatywne polegają-
ce na pisaniu całych nazw małymi literami i oddzielaniu poszczególnych wyrazów znakami
podkreślenia, na przykład first_name, favorite_bands i old_company_name. Notacja ta ma
tę zaletę, że pozwala wizualnie odróżnić funkcje od innych identyfikatorów — typów pro-
stych i obiektów.
Standard ECMAScript zaleca styl wielbłądzi zarówno dla metod, jak i dla właściwości, choć
właściwości wielowyrazowych jest naprawdę niewiele (lastIndex i ignoreCase z obiektów
wyrażeń regularnych).

Inne wzorce nazewnictwa


Czasem programiści używają pewnych konwencji nazewnictwa, by utworzyć lub zastąpić
pewne funkcje języka.
W JavaScripcie nie ma na przykład możliwości definiowania stałych (choć istnieją stałe wbu-
dowane takie jak Number.MAX_VALUE), więc programiści zaczęli stosować konwencję pisania
wartości, które nie zmieniają się w trakcie działania programu, tylko i wyłącznie wielkimi
literami.
// cenne stałe, nie zmieniać
var PI = 3.14,
MAX_WIDTH = 800;

Istnieje również inna konwencja konkurująca o stosowanie wielkich liter — nazywanie w ten
sposób zmiennych globalnych. Pisanie wszystkich zmiennych globalnych z użyciem wielkich
liter ma wskazać, że nie należy ich nadużywać, i dodatkowo czyni je łatwiej zauważalnymi.
Jeszcze innym przykładem konwencji jest system oznaczania prywatnych składowych obiektów.
Choć w języku JavaScript można uzyskać prawdziwą prywatność zmiennych i składowych,

Konwencje nazewnictwa | 39
niektórzy programiści preferują poprzedzanie „prywatnej” właściwości lub metody znakiem
podkreślenia. Oto przykład:
var person = {
getName: function () {
return this._getFirst() + ' ' + this._getLast();
},
_getFirst: function () {
// ...
},
_getLast: function () {
// ...
}
};

W prezentowanym przykładzie getName() ma być metodą publiczną, czyli częścią stabilnego


API, natomiast _getFirst() i _getLast() powinny pozostać prywatne. Choć formalnie nadal
są metodami publicznymi, zastosowanie podkreślenia sugeruje, że zewnętrzny programista
korzystający z obiektu nie powinien ich stosować bezpośrednio (bo na przykład mogą znik-
nąć w następnym wydaniu biblioteki). Narzędzie JSLint będzie informowało o błędzie doty-
czącym początkowych znaków podkreślenia, chyba że wyłączy się opcję nomen, ustawiając ją
na wartość false.
Poniżej zostało przedstawionych kilka odmian konwencji _prywatne.
• Użycie znaku podkreślenia na końcu nazwy oznacza jej prywatność — name_ i getElements_().
• Użycie pojedynczego znaku podkreślenia dla właściwości _chronionych i podwójnego dla
__prywatnych.
• W przeglądarce Firefox niektóre wewnętrzne właściwości niestanowiące oficjalnej części
języka są dostępne dla programisty, ale są poprzedzone i zakończone dwoma znakami
podkreślenia, na przykład __proto__ lub __parent__.

Pisanie komentarzy
Należy umieszczać komentarze w tworzonym kodzie, nawet jeśli nie będzie do niego zaglą-
dała inna osoba. Gdy ktoś zajmuje się danym problemem od dłuższego czasu, uważa pewne
rozwiązania za oczywiste, ale gdy zajrzy do tego samego kodu po kilku tygodniach, zapewne
będzie miał problem ze zrozumieniem, jak on dokładnie działa.
Oczywiście nie należy przesadzać — komentowanie każdego wiersza kodu nie jest potrzebne.
Warto jednak umieszczać komentarze przy każdej funkcji, podając jej działanie, przyjmowa-
ne argumenty i zwracaną wartość. Dodatkowo warto opisać sposób działania nietypowych
lub interesujących algorytmów. Myśl o komentarzach jak o wskazówkach dla przyszłych
czytelników kodu. Taka osoba powinna mieć ogólne pojęcie o działaniu funkcji po przeczy-
taniu jej nazwy, argumentów i komentarza. Gdy kod składa się z kilku wierszy wykonują-
cych określone zadanie, czytelnik może pominąć dany fragment, jeśli będzie miał do dyspo-
zycji jednowierszowy opis powodu utworzenia kodu i jego działania. Nie istnieje żadna
żelazna zasada określająca stosunek ilości kodu do objętości komentarzy; zdarza się, że pewne
fragmenty (na przykład wyrażenia regularne) wymagają więcej komentarza niż kodu.

40 | Rozdział 2. Podstawy
Najważniejsze jest utrzymywanie aktualności komentarzy, choć z doświadczenia
wiadomo, że nie jest to łatwe. Przestarzałe komentarze mogą zmylić czytelnika
i w efekcie okazać się gorsze od ich braku.

Komentarze mają jeszcze jedną zaletę: jeśli zostaną napisane w określony sposób, mogą po-
służyć do automatycznego generowania dokumentacji.

Pisanie dokumentacji interfejsów programistycznych


Większość programistów uważa pisanie dokumentacji za zadanie nudne i nieprzyjemne. Nie
musi tak być. Dokumentację API można wygenerować automatycznie na podstawie komen-
tarzy zawartych w kodzie. W ten sposób łatwo ją uzyskać, tak naprawdę w ogóle jej nie pisząc.
Sam pomysł podoba się wielu programistom, ponieważ taka automatycznie generowana do-
kumentacja zawiera odnośniki do słów kluczowych i korzysta ze specjalnych „poleceń” for-
matujących, co w pewnym sensie przypomina programowanie.
Tradycyjne dokumentacje API przywędrowały ze świata Javy, gdzie były generowane za
pomocą narzędzia Javadoc dystrybuowanego wraz z pakietem Java SDK (Software Deve-
lopment Kit). Ten pomysł powielono w wielu innych językach. W języku JavaScript istnieją dwa
narzędzia do generowania komentarzy — oba bezpłatne i dostępne na zasadach open source.
Są to JSDoc Toolkit (http://code.google.com/p/jsdoc-toolkit/) i YUIDoc (http://yuilibrary.com/projects/yuidoc).
Proces tworzenia automatycznie generowanej dokumentacji API składa się z trzech etapów:
• napisania w specjalny sposób bloków komentarzy,
• uruchomienia narzędzia analizującego kod i komentarze,
• opublikowania wyników działania narzędzia (najczęściej w postaci strony HTML).

Specjalna składnia, której trzeba się nauczyć, składa się z około tuzina znaczników o nastę-
pującej postaci:
/**
* @znacznik wartość
*/

Przypuśćmy, że komentarz dotyczy funkcji o nazwie reverse(), która odwraca tekst. Jako
parametr przyjmuje ona ciąg znaków i zwraca również ciąg znaków. Dotycząca jej doku-
mentacja mogłaby mieć postać:
/**
* Odwraca ciąg znaków
*
* @param {String} input Ciąg znaków do odwrócenia
* @return {String} Odwrócony ciąg znaków
*/
var reverse = function (input) {
// ...
return output;
};

Znacznik @param określa parametry wejściowe, natomiast znacznik @return dokumentuje


zwracaną wartość. Narzędzie do generowania dokumentacji analizuje kod oraz znaczniki, by
wytworzyć ładnie sformatowaną dokumentację HTML.

Pisanie dokumentacji interfejsów programistycznych | 41


Przykład dokumentacji YUIDoc
Narzędzie YUIDoc początkowo powstało w celu tworzenia dokumentacji dla biblioteki YUI
(Yahoo! User Interface), ale może być stosowane dla dowolnego projektu. Wykorzystuje ono pewne
konwencje, których należy przestrzegać, by narzędzie działało optymalnie, na przykład sto-
sując oznaczenia modułów i klas (choć oczywiście język nie ma wbudowanej obsługi klas).
Przyjrzyjmy się przykładowi pełnej dokumentacji wygenerowanej za pomocą YUIDoc.
Rysunek 2.1 przedstawia podgląd ładnie sformatowanej dokumentacji, którą się uzyskuje po
uruchomieniu narzędzia. W zasadzie można nawet dostosować szablon HTML do własnych
potrzeb, czyniąc go ładniejszym lub lepiej dopasowanym do kolorów firmowych.

Rysunek 2.1. Dokumentacja w wersji anglojęzycznej wygenerowana przez YUIDoc


W pełni działająca demonstracja anglojęzycznej dokumentacji znajduje się pod adresem
http://www.jspatterns.com/book/2/.
W prezentowanym przykładzie cała aplikacja znajduje się w jednym pliku app.js z jednym
modułem myapp. Więcej informacji na temat modułów pojawi się w następnych rozdziałach
— na razie potraktuj go tylko jako znacznik komentarza niezbędny do działania YUIDoc.

42 | Rozdział 2. Podstawy
Zawartość pliku app.js rozpoczyna się od następującego komentarza dokumentującego:
/**
* Moja aplikacja JavaScript
*
* @module myapp
*/

Następnie pojawia się definicja pustego obiektu używana jako przestrzeń nazw.
var MYAPP = {};

Po niej znajduje się definicja obiektu math_stuff zawierającego dwie metody: sum() i multi().
/**
* Narzędzie matematyczne
* @namespace MYAPP
* @class math_stuff
*/
MYAPP.math_stuff = {

/**
* Suma dwóch liczb
*
* @method sum
* @param {Number} a Pierwsza liczba
* @param {Number} b Druga liczba
* @return {Number} Suma dwóch wartości wejściowych
*/
sum: function (a, b) {
return a + b;
},

/**
* Iloczyn dwóch liczb
*
* @method multi
* @param {Number} a Pierwsza liczba
* @param {Number} b Druga liczba
* @return {Number} Iloczyn dwóch wartości wejściowych
*/
multi: function (a, b) {
return a * b;
}
};

Na tym kończy się deklaracja pierwszej „klasy”. Wykorzystano w niej następujące znaczniki:
• @namespace — globalna referencja zawierająca definiowany obiekt.
• @class — nieco myląca informacja (bo w języku JavaScript nie ma klas) oznaczająca
obiekt lub funkcję konstruującą.
• @method — definiuje metodę obiektu i określa jej nazwę.
• @param — określa pojedynczy parametr funkcji i może pojawić się wielokrotnie; typ pa-
rametru znajduje się w nawiasach klamrowych, a za nim podaje się nazwę parametru
i jego opis.
• @return — przypomina @param, ale określa typ i wartość zwracaną przez funkcję bez
podawania nazwy.

Pisanie dokumentacji interfejsów programistycznych | 43


W drugiej „klasie” pojawi się funkcja konstruująca i dodawanie metody do jej prototypu.
Dzięki temu przedstawiony zostanie sposób dokumentowania alternatywnej metody two-
rzenia obiektów.
/**
* Tworzy obiekty Person
* @class Person
* @constructor
* @namespace MYAPP
* @param {String} first Imię
* @param {String} last Nazwisko
*/
MYAPP.Person = function (first, last) {
/**
* Imię osoby
* @property first_name
* @type String
*/
this.first_name = first;
/**
* Nazwisko osoby
* @property last_name
* @type String
*/
this.last_name = last;
};

/**
* Zwraca imię i nazwisko osoby z obiektu Person
*
* @method getName
* @return {String} Imię i nazwisko osoby
*/
MYAPP.Person.prototype.getName = function () {
return this.first_name + ' ' + this.last_name;
};

Rysunek 2.1 przedstawia, jak może wyglądać dokumentacja wygenerowana dla konstruktora
Person. Elementy pogrubione w powyższym kodzie to:
• @constructor — wskazówka informująca, że „klasa” to tak naprawdę funkcja kon-
struująca;
• @property i @type — opisują właściwości obiektu.

System YUIDoc jest niezależny od języka, więc analizuje tylko i wyłącznie bloki komentarza
dokumentującego bez sprawdzania kodu JavaScript. Wadą jest to, że trzeba podawać w ko-
mentarzach nazwy parametrów, właściwości i metod, na przykład @property first_name.
Zaletą jest fakt, iż po opanowaniu tego systemu tworzenia dokumentacji można go wykorzy-
stać dla dowolnego innego języka programowania.

Pisanie w sposób ułatwiający czytanie


Pisanie komentarzy odpowiednich do wygenerowania dokumentacji API to nie tylko sposób
tworzenia dokumentacji dla leniwych, ale także dobra okazja do poprawienia własnego kodu
przez jego ponowną analizę.

44 | Rozdział 2. Podstawy
Każdy pisarz lub redaktor z pewnością potwierdzi istotność etapu korektorskiego i redakcyj-
nego — niejednokrotnie jest on najważniejszy w procesie powstawania dobrej książki lub
artykułu. Zapisanie wszystkiego na papierze lub cyfrowo to tylko pierwszy etap, pierwszy
szkic. Szkic przekaże czytelnikowi pewne informacje, ale zapewne nie odbędzie się to w naj-
bardziej przyjazny, ustrukturyzowany i łatwy w analizie sposób.
To samo dotyczy kodu. Gdy siadamy i rozwiązujemy problem, rozwiązanie to jest jedynie
pierwszym szkicem. Zapewnia pożądany wynik, ale czy czyni to w najlepszy możliwy sposób?
Czy rozwiązanie jest łatwe do zrozumienia, konserwacji, czytania i aktualizacji? Gdy ponow-
nie zagląda się do własnego kodu, szczególnie po jakimś czasie, najczęściej znajduje się wiele
miejsc do usprawnienia — poprawki mogą ułatwić czytanie kodu, zwiększyć jego efektyw-
ność lub wyeliminować zbędne elementy. Odpowiada to pracy redakcyjnej i bardzo często
pozwala osiągnąć kod wysokiej jakości. Niestety, programiści bardzo często mają bardzo na-
pięte terminy („problem jest następujący, a rozwiązania potrzebuję na jutro”) i nie znajdują
czasu na dopieszczenie kodu. Pisanie dokumentacji dla API to dobra okazja, by zapewnić
jego lepszą organizację.
Pisząc komentarz dla dokumentacji, nierzadko ponownie przyglądamy się problemowi. Czasem
ponowna analiza pokazuje, że trzeci parametr jest wykorzystywany częściej niż drugi, a drugi
prawie zawsze ma wartość true, więc zapewne lepiej je zamienić miejscami, optymalizując
interfejs metody.
Pisanie w sposób ułatwiający czytanie oznacza tworzenie kodu, a czasem tylko samego API,
z założeniem, że ktoś inny będzie musiał to przeczytać. Dzięki temu zmuszamy się do znaj-
dowania lepszych (bardziej przyswajalnych) sposobów rozwiązania problemu.
Skoro już jesteśmy przy szkicach: czasem warto zaplanować odrzucenie pierwotnej wersji.
Początkowo rozwiązanie to może wydawać się zbyt ekstremalne, ale — szczególnie w przy-
padku niezwykle istotnych projektów — jest bardzo sensowne (i zależy od niego ludzkie
życie). Zasada jest następująca: odrzuca się pierwsze wymyślone rozwiązanie i zaczyna się
wszystko od początku. Pierwsze rozwiązanie może być w pełni poprawne, ale to tylko szkic,
jeden z przykładów poradzenia sobie z problemem. Drugie rozwiązanie jest zawsze lepsze,
bo lepiej rozumie się istotę problemu. W trakcie pisania drugiego rozwiązania zabronione
jest kopiowanie fragmentów kodu z pierwszego, co zapobiega skrótom i godzeniu się na
rozwiązanie nieidealne.

Ocenianie kodu przez innych członków zespołu


Innym sposobem uczynienia kodu lepszym jest jego analiza przez pozostałych członków
zespołu. Taka analiza powinna przebiegać w sposób sformalizowany i ustandaryzowany,
a nawet powinna być wspomagana przez odpowiednie narzędzia. W ten sposób sprawdza-
nie kodu przez innych programistów może stać się częścią podstawowego procesu progra-
mistycznego. Brak czasu lub narzędzi nie powinien być powodem porzucenia tej metody
usprawniania kodu. Wystarczy poprosić programistę obok o jego przejrzenie i powiedzenie,
co sądzi na jego temat.
Podobnie jak pisanie dokumentacji projektu ocena dokonywana przez innych programistów
pomaga pisać lepszy kod — będzie on przeglądany przez inne osoby, wiadomo więc, że będzie
czytelniejszy.

Ocenianie kodu przez innych członków zespołu | 45


Wzajemna ocena kodu to dobra praktyka nie tylko dlatego, że wynikowy kod jest lepszy, ale także
dlatego, że oceniający i twórca wymieniają się wiedzą i poznają stosowane przez siebie rozwiązania.
Jeśli jesteś wolnym strzelcem lub programujesz w pojedynkę i tym samym nie masz możli-
wości zapewnienia sobie oceny kodu przez pozostałych członków zespołu, nadal istnieją
sposoby, by sobie z tym poradzić. Często można publicznie udostępnić przynajmniej część
kodu lub opisać na blogu interesujący fragment. Dzięki temu programiści z całego świata będą
mieli okazję ocenić powstały kod.
Inną dobrą praktyką jest stosowanie systemu kontroli wersji kodu źródłowego (CSV, Subversion,
Git) skonfigurowanego w taki sposób, by wysyłał do zespołu powiadomienia po każdorazowym
przesłaniu nowej wersji. Większość tych powiadomień pozostanie bez odpowiedzi, ale od
czasu do czasu pojawi się ciekawa ocena, bo ktoś akurat będzie miał chwilę czasu i przyjrzy
się dokładniej nowemu kodowi.

Minifikowanie kodu tylko w systemie produkcyjnym


Minifikacja to proces usuwania białych spacji, komentarzy i innych nieistotnych części kodu
JavaScript w celu zmniejszenia jego rozmiaru w bajtach, przez co zostanie on szybciej prze-
słany z serwera do przeglądarki. Najczęściej za cały proces odpowiada specjalne narzędzie
(minifikator) takie jak Yahoo! YUICompressor lub Closure Compiler firmy Google. Minifika-
cja pozwala zmniejszyć czas oczekiwania na wczytanie się strony WWW. Wykorzystywanie
wspomnianych narzędzi w systemie produkcyjnym jest niezwykle istotne, bo pozwala uzy-
skać spore oszczędności, niejednokrotnie zmniejszając rozmiar kodu o połowę.
Oto, jak wygląda kod JavaScript po poddaniu go minifikacji (kod stanowi część biblioteki YUI2):
YAHOO.util.CustomEvent=function(D,C,B,A){this.type=D;this.scope=C||window;this.silent
=B;this.signature=A||YAHOO.util.CustomEvent.LIST;this.subscribers=[];if(!this.silent)
{}var E="_YUICEOnSubscribe";if(D!==E){this.subscribeEvent=new
YAHOO.util.CustomEvent(E,this,true);}...

Poza usuwaniem białych spacji, znaków nowego wiersza i komentarzy minifikatory zmie-
niają również nazwy zmiennych na ich krótsze odpowiedniki (ale tylko wtedy, gdy taką ope-
rację można bezpiecznie wykonać). Przykładem mogą być parametry D, C, B i A z powyższego
kodu. Minifikatory mogą zmieniać jedynie nazwy zmiennych lokalnych, gdyż zmiana nazw
globalnych mogłaby doprowadzić do błędnego działania kodu. Z tego powodu dobrą prak-
tyką jest stosowanie nazw lokalnych za każdym razem, gdy to możliwe. Jeśli korzysta się
w funkcji ze zmiennej globalnej (na przykład obiektu DOM) więcej niż jeden raz, warto
przypisać ją wcześniej do zmiennej lokalnej. Nie tylko przyspieszy to działanie kodu (szybsze
wyszukiwanie nazwy), ale również zapewni lepszą minifikację i krótszy kod do pobrania
przez docelowego użytkownika.
Warto wspomnieć, że narzędzie Closure Compiler firmy Google potrafi również zmieniać
nazwy zmiennych globalnych (w trybie zaawansowanym), ale wymaga to dodatkowych przy-
gotowań w kodzie i ogólnie jest bardziej ryzykowne, choć wynikowy kod jest jeszcze krótszy.
Minimalizacja kodu produkcyjnego jest ważna ze względu na wydajność stron, ale lepiej po-
zostawić to zadanie wyspecjalizowanym narzędziom. Tworzenie własnego kodu w sposób
taki, jak czyni to minifikator, to bardzo duży błąd. Zawsze warto stosować opisowe nazwy
zmiennych, korzystać z białych spacji i wcięć, pisać komentarze i tak dalej. Tworzony kod
będzie czytany przez ludzi, więc lepiej pozostawić im możliwość jego łatwej analizy —
o końcową redukcję jego rozmiaru niech zatroszczy się odpowiednie narzędzie.

46 | Rozdział 2. Podstawy
Uruchamiaj narzędzie JSLint
Narzędzie JSLint zostało pokrótce omówione w poprzednim rozdziale i pojawiło się kilku-
krotnie w tym. Zapewne nie jest dla nikogo tajemnicą, że stosowanie tego narzędzia to dobra
praktyka programistyczna.
Jakich błędów poszukuje JSLint? Szuka złamania kilku wzorców omówionych w tym roz-
dziale (pojedyncze użycie var, podstawa w parseInt(), każdorazowe stosowanie nawiasów
klamrowych), a także wielu innych potencjalnych problemów:
• nieosiągalnego kodu,
• użycia zmiennych przed ich zadeklarowaniem,
• użycia niebezpiecznych znaków UTF,
• użycia void, with lub eval,
• niebezpiecznego użycia niektórych znaków w wyrażeniach regularnych.

JSLint jest napisany w języku JavaScript (i zapewne bez problemów przeszedłby testowanie
za pomocą JSLint). Dobrą wiadomością jest fakt, iż jest dostępny jako narzędzie w wersji on-
line i jako kod do pobrania dla wielu platform i interpreterów. Można go pobrać i uruchomić
lokalnie, używając WSH (Windows Scripting Host, dostępny we wszystkich wydaniach systemu
Windows), JSC (JavaScriptCore, część systemu Mac OS X) lub Rhino (interpreter JavaScript
autorstwa fundacji Mozilla).
Dobrym pomysłem jest pobranie JSLint i zintegrowanie go z edytorem tekstu, by wyrobić
w sobie nawyk uruchamiania narzędzia po każdym zapisie pliku (dobrym rozwiązaniem
może być też zastosowanie skrótu klawiaturowego).

Podsumowanie
Niniejszy rozdział opisuje, co oznacza tworzyć kod łatwy w konserwacji, czyli porusza temat
istotny nie tylko ze względu na dobro projektu informatycznego, ale również ze względu na
dobre samopoczucie wszystkich uczestniczących w nim osób, głównie programistów. W rozdziale
tym zajęliśmy się również wieloma najlepszymi praktykami i wzorcami, między innymi:
• zmniejszaniem liczby zmiennych globalnych, idealnie do jednej na aplikację;
• używaniem jednej deklaracji var na funkcję, co pozwala mieć oko na wszystkie lokalne
zmienne funkcji i zapobiega niespodziankom związanym z przenoszeniem deklaracji
zmiennych;
• pętlami for i for-in, konstrukcjami switch, przypomnieniem, że „eval() to zło”, i uni-
kaniem zmian prototypów obiektów wbudowanych;
• przestrzeganiem jednolitej konwencji pisania kodu (stosowaniem białych spacji i wcięć,
używaniem nawiasów klamrowych nawet wtedy, gdy są opcjonalne) i konwencji nazew-
nictwa (dla konstruktorów, funkcji i zmiennych).
Rozdział opisuje również kilka dodatkowych praktyk niezwiązanych bezpośrednio z kodem
programu, ale z ogólnym procesem programowania: pisaniem komentarzy, tworzeniem do-
kumentacji API, przeprowadzaniem ocen kodu, unikaniem minifikacji kodu kosztem jego
czytelności i częstym sprawdzaniem kodu narzędziem JSLint.

Podsumowanie | 47
48 | Rozdział 2. Podstawy
ROZDZIAŁ 3.

Literały i konstruktory

Wzorce notacji literałowej dostępne w języku JavaScript zapewniają bardziej spójne, bardziej
zwarte i mniej narażone na błędy definicje obiektów. Niniejszy rozdział omawia literały do-
tyczące obiektów, tablic i wyrażeń regularnych, a także wyjaśnia, dlaczego lepiej stosować je
zamiast wbudowanych funkcji konstruujących takich jak Object() i Array(). W rozdziale
zajmujemy się również formatem JSON, który wykorzystuje literały obiektów i tablic do de-
finiowania elastycznego formatu przesyłu danych. Nie zabraknie też opisu tworzenia wła-
snych konstruktorów oraz sposobów wymuszania użycia new, by konstruktory zachowywały
się zgodnie z oczekiwaniami.
Aby rozszerzyć główny przekaz niniejszego rozdziału (zachęcenie do stosowania literałów
zamiast konstruktorów), zawarto w nim również opis wbudowanych otoczek w postaci kon-
struktorów Number(), String() i Boolean(), a także porównanie ich do odpowiadających im
typów prostych: liczby, tekstu i wartości logicznej. Na końcu znajduje się krótka notka na
temat jeszcze jednego wbudowanego konstruktora — Error().

Literał obiektu
Jeśli myślimy o obiektach w języku JavaScript, najczęściej chodzi nam o tablice mieszające
z parami nazwa-wartość (w wielu innych językach konstrukcja ta nosi nazwę tablicy asocjacyj-
nej). Wartościami mogą być typy proste lub inne obiekty, ale w obu przypadkach mówimy
o właściwościach. Jeżeli wartością jest funkcja, stosuje się nazwę metoda.
Utworzone przez siebie obiekty (czyli obiekty rdzenne zdefiniowane przez użytkownika)
można modyfikować w dowolnym momencie. Co więcej, można też modyfikować wiele
właściwości wbudowanych obiektów rdzennych. Nic nie stoi na przeszkodzie, by utworzyć
pusty obiekt i zacząć dodawać do niego funkcjonalności. Notacja literału obiektu jest wręcz
wymarzonym rozwiązaniem dla tworzenia obiektów na żądanie.
Rozważmy następujący przykład:
// rozpoczęcie od pustego obiektu
var dog = {};

// dodanie jednej właściwości


dog.name = "Benji";

49
// dodanie metody
dog.getName = function () {
return dog.name;
};

W powyższym przykładzie rozpoczynamy od całkowicie czystego stanu — pustego obiektu


— a następnie dodajemy do niego właściwość oraz metodę. W dowolnym momencie w trakcie
działania programu można:
• zmienić wartości właściwości i obiektów, na przykład:
dog.getName = function () {
// zmiana definicji metody
// na zaszytą na stałe wartość
return "Fido";
};
• usunąć właściwość lub metodę:
delete dog.name;
• dodać nowe właściwości lub metody:
dog.say = function () {
return "Hau!";
};
dog.fleas = true;

Nie trzeba jednak zaczynać od obiektu pustego. Wzorzec literału obiektu dopuszcza dodanie
do niego funkcjonalności już na etapie jego tworzenia, co przedstawia poniższy przykład.
var dog = {
name: "Benji",
getName: function () {
return this.name;
}
};

Stwierdzenie „pusty obiekt” pojawi się w książce wielokrotnie. Warto jednak pamiętać,
że jest to jedynie uproszczenie, ponieważ tak naprawdę w języku JavaScript coś ta-
kiego nie istnieje. Nawet najprostszy obiekt {} posiada właściwości i metody odzie-
dziczone po Object.prototype. Przez „pusty” rozumie się obiekt, który nie posiada
żadnych własnych właściwości, a jedynie odziedziczone.

Składnia literału obiektowego


Jeśli nie stosowało się wcześniej notacji literału obiektowego, może ona wydawać się nieco
dziwna, ale im częściej się jej używa, tym bardziej się ją lubi. Ogólne zasady są następujące:
• Treść obiektu znajduje się między nawiasami klamrowymi ({ i }).
• Poszczególne właściwości i metody oddziela się znakiem przecinka. Choć składnia do-
puszcza zastosowanie przecinka po ostatniej parze nazwa-wartość, starsze wersje prze-
glądarki IE reagują na to błędem, więc warto unikać takich sytuacji.
• Nazwę właściwości od jej wartości oddziela znak dwukropka.
• W momencie przypisywania obiektu do zmiennej nie należy zapominać o średniku po
końcowym znaku }.

50 | Rozdział 3. Literały i konstruktory


Obiekty z konstruktora
W języku JavaScript nie ma klas, co zapewnia sporą elastyczność, gdyż nie trzeba nic wie-
dzieć o obiekcie z wyprzedzeniem — nie jest potrzebny „schemat” klasy. Język zapewnia
jednakże funkcje konstruujące, które wykorzystują składnię przypominającą tę stosowaną
przy tworzeniu obiektów w językach takich jak Java.
Obiekty mogą być tworzone za pomocą własnych funkcji konstruujących lub za pomocą
funkcji wbudowanych takich jak Object(), Date() lub String().
Poniżej przedstawione zostały dwa sposoby tworzenia identycznego obiektu.
// sposób pierwszy — wykorzystanie literału
var car = {goes: "daleko"};

// sposób drugi — wbudowany konstruktor


// ostrzeżenie: to jest antywzorzec
var car = new Object();
car.goes = "daleko";

Jak nietrudno zauważyć, oczywistą zaletą notacji literałowej jest jej zwięzłość. Innym powodem
preferowania literałów przy tworzeniu obiektów jest kładzenie nacisku na fakt, iż obiekt to
po prostu edytowalna tablica asocjacyjna, a nie coś, co musi być wypiekane na podstawie
recepty (klasy).
Kolejnym argumentem przemawiającym za literałem jest unikanie wyszukiwania nazwy.
Ponieważ przy korzystaniu z wbudowanej funkcji konstruującej istnieje prawdopodobień-
stwo wystąpienia lokalnego konstruktora o tej samej nazwie (czyli Object), interpreter musi
przeszukać łańcuch zakresu zmiennych i odnaleźć właściwy konstruktor Object (najczęściej
globalny).

Pułapka konstruktora Object


Nie ma powodu, by stosować konstruktor new Object(), gdy to samo zadanie wykonamy
szybciej za pomocą literału. Czasem jednak zdarza się pracować nad kodem napisanym przez
innych, więc warto mieć świadomość dodatkowej cechy konstruktora będącej jeszcze jednym
powodem, by go unikać. Cechą tą jest fakt, iż może on przyjąć dodatkowy parametr i w za-
leżności od przekazanej wartości zdecydować, że przekaże tworzenie obiektu innemu wbu-
dowanemu konstruktorowi, przez co zwróci inny obiekt, niż oczekujemy.
Poniżej znajduje się kilka przykładów przekazywania liczby, tekstu i wartości logicznej do
new Object(); w efekcie powstają obiekty tworzone za pomocą innych konstruktorów.
// ostrzeżenie: poniżej znajdują się antywzorce

// pusty obiekt
var o = new Object();
console.log(o.constructor === Object); // true

// obiekt liczby
var o = new Object(1);
console.log(o.constructor === Number); // true
console.log(o.toFixed(2)); // "1.00"

// obiekt ciągu znaków


var o = new Object("Jestem ciągiem znaków");

Literał obiektu | 51
console.log(o.constructor === String); // true
// standardowe obiekty nie posiadają metody substring(),
// ale jest ona dostępna w obiektach ciągów znaków
console.log(typeof o.substring); // "function"

// obiekt wartości logicznej


var o = new Object(true);
console.log(o.constructor === Boolean); // true

Zachowanie konstruktora Object() może prowadzić do nieoczekiwanych wyników, jeśli


przekazywana do niego wartość jest ustalana dynamicznie w trakcie działania programu.
Wniosek jest prosty: warto zamienić new Object() na prostszy i pewniejszy literał obiektu.

Własne funkcje konstruujące


Poza wzorcem literału i wbudowanymi konstruktorami możliwe jest również tworzenie
obiektów za pomocą własnych funkcji konstruujących, co przedstawia poniższy przykład.
var adam = new Person("Adam");
adam.say(); // "Jestem Adam"

Przedstawiony wzorzec przypomina tworzenie obiektu w języku Java z użyciem klasy o na-
zwie Person. Choć składnia jest bardzo podobna, w języku JavaScript nie ma klas, a Person
to zwykła funkcja.
Oto, w jaki sposób można zdefiniować funkcję konstruującą Person:
var Person = function (name) {
this.name = name;
this.say = function () {
return "Jestem " + this.name;
};
};

W momencie wywołania funkcji za pomocą new JavaScript wykonuje w jej wnętrzu kilka do-
datkowych operacji:
• Powstaje nowy pusty obiekt dostępny poprzez zmienną this i dziedziczący po prototypie
funkcji.
• Do obiektu wskazywanego przez this zostają dodane zdefiniowane właściwości i metody.
• Nowo utworzony obiekt jest niejawnie zwracany jako wynik całej operacji (o ile jawnie
nie zwrócono innego obiektu).
Można powiedzieć, że za plecami programisty JavaScript wykonuje następujące działania:
var Person = function (name) {

// utwórz nowy obiekt,


// używając literału
// var this = {};

// dodaj właściwości i metody


this.name = name;
this.say = function () {
return "Jestem " + this.name;
};

// return this;
};

52 | Rozdział 3. Literały i konstruktory


W celu uproszczenia przykładu metoda say() została dodana do this, co oznacza, że każdo-
razowe wywołanie new Person() tworzy w pamięci nową wersję funkcji. To nieefektywne
rozwiązanie, bo metoda say() nie zmienia swej postaci między poszczególnymi instancjami.
Lepszym wyjściem byłoby dodanie jej do prototypu Person.
Person.prototype.say = function () {
return "Jestem " + this.name;
};

Więcej informacji na temat dziedziczenia i prototypów pojawi się w dalszych rozdziałach, ale
ogólna zasada jest taka, by składowe używane w wielu instancjach trafiały do prototypu.
W tym miejscu warto zasygnalizować pewien fakt, który zostanie dokładniej opisany w dal-
szej części książki. W przykładzie wskazano, że JavaScript wykonuje potajemnie następującą
operację:
// var this = {};

Nie jest to cała prawda, ponieważ ten „pusty” obiekt nie jest w rzeczywistości pusty; dzie-
dziczy po prototypie obiektu Person. Rzeczywistość odpowiada więc poniższej konstrukcji.
// var this = Object.create(Person.prototype);

Opis sposobu działania Object.create() pojawi się w dalszej części książki.

Wartość zwracana przez konstruktor


Wywołana za pomocą new funkcja konstruująca zawsze zwraca obiekt — domyślnie jest to
obiekt wskazywany przez this. Jeśli do this nie zostaną dodane nowe właściwości, zwró-
cony zostanie obiekt „pusty” (w rzeczywistości nadal będzie on dziedziczył po prototypie
konstruktora).
Konstruktor zwraca this niejawnie, nawet jeśli w funkcji nie pojawia się instrukcja return.
Nic jednak nie stoi na przeszkodzie, by zwrócić dowolny inny obiekt. Poniższy kod zwraca
obiekt utworzony w funkcji i przypisany do zmiennej that.
var Objectmaker = function () {

// właściwość name z this zostanie zignorowana,


// ponieważ konstruktor zwróci inny obiekt
this.name = "To jest this";

// utworzenie i zwrócenie nowego obiektu


var that = {};
that.name = "A to jest that";
return that;
};

// test
var o = new Objectmaker();
console.log(o.name); // "A to jest that"

Konstruktor ma pełną swobodę co do zwracanych obiektów. Warunek jest tylko jeden: musi
to być obiekt. Próba zwrócenia czegoś, co nie jest obiektem (tekstu lub wartości logicznej), nie
spowoduje zgłoszenia błędu, ale zostanie zignorowana i JavaScript zamiast wskazanej wartości
zwróci obiekt this.

Własne funkcje konstruujące | 53


Wzorce wymuszania użycia new
Jak wcześniej wspomniano, konstruktory to zwykłe funkcje wywoływane z użyciem słowa
new. Co się stanie, jeśli w momencie wywoływania konstruktora zabraknie tego słówka? Nie
pojawią się żadne błędy składniowe i wykonania, ale najprawdopodobniej pojawi się niespo-
dziewane działanie programu i błędy logiczne. Wynika to z prostego faktu: po pominięciu
new zmienna this wewnątrz konstruktora będzie wskazywała na globalny obiekt (w prze-
glądarkach na obiekt window).
Jeżeli konstruktor zawierający w swoim kodzie fragment this.member wywoła się bez new,
wynikiem będzie powstanie nowej właściwości obiektu globalnego o nazwie member dostęp-
nej dzięki window.member lub po prostu member. Efekt ten jest wyjątkowo niepożądany,
bo zawsze powinno się dążyć do zachowania przestrzeni globalnej w czystości.
// konstruktor
function Waffle() {
this.tastes = "doskonale";
}

// nowy obiekt
var good_morning = new Waffle();
console.log(typeof good_morning); // "object"
console.log(good_morning.tastes); // "doskonale"

// antywzorzec:
// zapomniano new
var good_morning = Waffle();
console.log(typeof good_morning); // "undefined"
console.log(window.tastes); // "doskonale"

To niepożądane zachowanie wyeliminowano w standardzie ECMAScript 5 i w trybie ścisłym


this nie wskazuje już na obiekt globalny. Na szczęście, nawet jeśli ES5 nie jest dostępny, ist-
nieje kilka sposobów zapewnienia, by funkcja konstruująca była zawsze wywoływana z new,
a nawet działała poprawnie pomimo pominięcia tego słowa.

Konwencja nazewnictwa
Najprostsze podejście polega na zastosowaniu konwencji nazewnictwa opisanej w poprzednim
rozdziale, czyli na każdorazowym pisaniu nazwy konstruktora od wielkiej litery (MyConstructor),
a pozostałych funkcji małą literą (myFunction).

Użycie that
Zastosowanie konwencji z pewnością pomaga, ale to jedynie sugestia, która nie gwarantuje
wymuszenia poprawnego działania. Poniższy wzorzec zapewnia, że konstruktor zawsze za-
działa zgodnie z oczekiwaniami, czyli zwróci nowy obiekt. Zamiast dodawać wszystkie
składowe do this, dodaje się je do that, a następnie zwraca that.
function Waffle() {
var that = {};
that.tastes = "doskonale";
return that;
}

54 | Rozdział 3. Literały i konstruktory


W przypadku prostszych obiektów nie trzeba nawet posiłkować się zmienną lokalną taką jak
that — wystarczy zwrócić literał obiektu.
function Waffle() {
return {
tastes: "doskonale"
};
}

Stosując jedną z zaprezentowanych implementacji Waffle(), mamy pewność, że zadziała ona


prawidłowo niezależnie od tego, jak zostanie wywołana.
var first = new Waffle(),
second = Waffle();
console.log(first.tastes); // "doskonale"
console.log(second.tastes); // "doskonale"

Głównym problemem w przypadku przedstawionego wzorca jest to, że zgubione zostaje


powiązanie z prototypem, więc dowolna składowa dodana do prototypu Waffle() nie będzie
dostępna dla utworzonych za jego pomocą obiektów.

Nazwa zmiennej that to jedynie konwencja i nie stanowi ona części języka JavaScript.
Można skorzystać z dowolnej nazwy (innymi popularnymi nazwami stosowanymi
w tej sytuacji są self i me).

Samowywołujący się konstruktor


Aby rozwiązać problemy poprzedniego wzorca i zapewnić dostępność właściwości prototypu
w instancjach obiektu, warto zastosować wzorzec przedstawiony poniżej. Konstruktor sprawdza
w nim, czy this jest jego instancją, i jeśli tak nie jest, wywołuje samego siebie z użyciem new.
function Waffle() {

if (!(this instanceof Waffle)) {


return new Waffle();
}

this.tastes = "doskonale";
}
Waffle.prototype.wantAnother = true;

// testowanie wywołań
var first = new Waffle(),
second = Waffle();

console.log(first.tastes); // "doskonale"
console.log(second.tastes); // "doskonale"

console.log(first.wantAnother); // true
console.log(second.wantAnother); // true

Innym, bardziej uniwersalnym sposobem sprawdzania poprawności instancji jest jej porów-
nanie z arguments.callee — nie trzeba w takiej sytuacji jawnie podawać nazwy konstruktora.
if (!(this instanceof arguments.callee)) {
return new arguments.callee();
}

Wzorce wymuszania użycia new | 55


Wzorzec wykorzystuje fakt, iż wewnątrz każdej funkcji istnieje obiekt o nazwie arguments
zawierający wszystkie parametry przekazane do funkcji w momencie jej wywoływania.
Dodatkowo obiekt ten zawiera właściwość callee, która wskazuje na wywołaną funkcję.
Warto jednakże pamiętać, że w trybie ścisłym ES5 arguments.callee nie jest obsługiwane,
więc najlepiej nie korzystać z niego w nowym kodzie, a także usunąć z już istniejącego.

Literał tablicy
Tablice w języku JavaScript, podobnie jak większość innych elementów, są obiektami. Two-
rzy się je za pomocą wbudowanej funkcji konstruującej Array(), ale istnieje również notacja
literałowa, która — podobnie jak to miało miejsce w przypadku obiektów — jest prostsza
i zalecana.
Oto, w jaki sposób można utworzyć dwie tablice o takiej samej zawartości — jedną za pomocą
konstruktora Array(), a drugą za pomocą literału.
// tablica trzech wartości
// ostrzeżenie: antywzorzec
var a = new Array("to", "jest", "pajączek");

// taka sama tablica


var a = ["to", "jest", "pajączek"];

console.log(typeof a); // "object", ponieważ tablica to obiekt


console.log(a.constructor === Array); // true

Składnia literału tablicy


O literale tablicy nie można powiedzieć zbyt wiele — to po prostu lista oddzielonych prze-
cinkami elementów tablicy otoczona nawiasami kwadratowymi. Każdy z elementów może być
dowolnego typu, włączając w to obiekty i inne tablice.
Składnia literału tablicy jest prosta i elegancka — przecież tablica to jedynie lista wartości in-
deksowana od zera. Nie trzeba niczego komplikować (i pisać więcej kodu), dołączając kon-
struktor i stosując operator new.

Pułapka konstruktora Array


Jednym z powodów, dla których warto unikać konstruktora Array(), jest pewna pozosta-
wiona w nim pułapka.
Jeśli konstruktor ten otrzyma tylko jeden parametr i będzie to liczba, to nie stanie się ona
pierwszym elementem tablicy, ale ustawi jej długość. Oznacza to, że new Array(3) tworzy
tablicę o długości równej 3, która nie zawiera żadnych elementów. Próba uzyskania dostępu
do elementów spowoduje zwrócenie wartości undefined, ponieważ elementy nie istnieją.
Poniższy przykład ilustruje różnicę w działaniu literału i konstruktora w przypadku poje-
dynczej wartości liczbowej.

56 | Rozdział 3. Literały i konstruktory


// tablica jednoelementowa
var a = [3];
console.log(a.length); // 1
console.log(a[0]); // 3

// tablica trójelementowa
var a = new Array(3);
console.log(a.length); // 3
console.log(typeof a[0]); // "undefined"

Choć to zachowanie wydaje się nieco nieoczekiwane, gdy do new Array() przekaże się war-
tość zmiennoprzecinkową zamiast całkowitej, jest jeszcze gorzej. Wynikiem jest błąd, gdyż
wartość zmiennoprzecinkowa nie jest poprawną długością tablicy.
// użycie literału
var a = [3.14];
console.log(a[0]); // 3.14

var a = new Array(3.14); // RangeError: invalid array length


console.log(typeof a); // "undefined"

Aby uniknąć potencjalnych błędów przy tworzeniu dynamicznie generowanych tablic, znacz-
nie bezpieczniej jest stosować notację literałową.

Istnieją pewne sprytne zastosowania konstruktora Array(). Można na przykład


użyć go do tworzenia powtarzającego się ciągu znaków. Poniższy kod zwróci tekst
zawierający 255 znaków spacji. (Dlaczego nie 256? Pozostawiam to do wyjaśnienia
dociekliwemu Czytelnikowi).
var white = new Array(256).join(' ');

Sprawdzanie, czy obiekt jest tablicą


Użycie operatora typeof dla tablicy spowoduje zwrócenie wartości object:
console.log(typeof [1, 2]); // "object"

Choć to zachowanie ma sens (tablica jest obiektem), nie jest pomocne. Potrzeba sprawdzenia,
czy przekazana wartość jest rzeczywiście tablicą, pojawia się często. Czasem można w tym
celu odnaleźć kod, który sprawdza istnienie właściwości length lub metody ogólnie koja-
rzonej z tablicami (na przykład slice()). Przedstawione testy mogą jednak bardzo łatwo
zawieść, bo nie ma żadnego powodu, dla którego obiekt niebędący tablicą nie mógłby stoso-
wać metod i właściwości o identycznych nazwach. Niestety, zdecydowanie lepsze rozwiąza-
nie w postaci testu instanceof Array nie działa prawidłowo w niektórych wersjach prze-
glądarki IE, gdy jest stosowane między ramkami.
ECMAScript 5 definiuje nową metodę o nazwie Array.isArray(), która zwraca jako wartość
prawdę, jeśli przekazany argument jest tablicą. Oto przykład:
Array.isArray([]); // true

// próba oszukania narzędzia


// obiektem przypominającym tablicę
Array.isArray({
length: 1,
"0": 1,
slice: function () {}
}); // false

Literał tablicy | 57
Jeśli nowa metoda nie jest jeszcze dostępna w wykorzystywanym środowisku, do testu
można wykorzystać metodę Object.prototype.toString(). Wywołanie metody call() dla
toString w kontekście tablic spowoduje zwrócenie tekstu „[object Array]”. W przypadku
standardowego obiektu wywołanie zwróci wartość „[object Object]”. Oznacza to, że dosyć
łatwo jest zasymulować nową metodę za pomocą poniższego kodu.
if (typeof Array.isArray === "undefined") {
Array.isArray = function (arg) {
return Object.prototype.toString.call(arg) === "[object Array]";
};
}

JSON
Po zapoznaniu się z literałami obiektu oraz tablicy nadszedł czas na przyjrzenie się formatowi
przesyłu danych JSON (JavaScript Object Notation). To bardzo lekki i wygodny format prze-
syłania informacji działający w wielu różnych językach, szczególnie w języku JavaScript.
W zasadzie w kwestii JSON nie trzeba uczyć się niczego nowego, bo tak naprawdę jest to
połączenie notacji literału obiektu i literału tablicy. Oto przykładowe dane:
{"nazwa": "wartość", "coś": [1, 2, 3]}

Jedyną istotną różnicą składniową między JSON i literałem obiektu jest to, że nazwy właści-
wości trzeba w tym formacie umieszczać w cudzysłowach, by uzyskać poprawny zapis. W przy-
padku literału obiektu cudzysłowy są wymagane tylko wtedy, gdy nazwy właściwości nie
są poprawnymi identyfikatorami, czyli na przykład zawierają spacje: {"pierwsze imię":
"Damian"}.

Format JSON nie dopuszcza stosowania funkcji lub literałów wyrażeń regularnych.

Korzystanie z formatu JSON


Jak wskazano w poprzednim rozdziale, nie zaleca się bezwarunkowego przetwarzania tekstu
JSON za pomocą eval() ze względu na ryzyko wykonania potencjalnie szkodliwego kodu.
Najlepiej w tym celu wykorzystać metodę JSON.parse(), która stanowi część języka ES5, ale
jest również dostępna w wielu nowoczesnych przeglądarkach (mimo że nie wspierają one
całości ES5). W przypadku starszych przeglądarek można skorzystać z biblioteki dostępnej
w witrynie JSON.org (http://www.json.org/json2.js), by uzyskać dostęp do obiektu JSON i jego
metod.
// wejściowy ciąg znaków JSON
var jstr = '{"klucz": "moja wartość"}';

// antywzorzec
var data = eval('(' + jstr + ')');

// rozwiązanie preferowane
var data = JSON.parse(jstr);
console.log(data.klucz); // "moja wartość"

Jeśli korzysta się z biblioteki JavaScript, istnieje spora szansa, że zawiera ona wbudowane
narzędzie do bezpiecznego przetwarzania formatu JSON i nie jest potrzebna dodatkowa
biblioteka. Przykładowo, korzystając z biblioteki YUI3, można napisać:

58 | Rozdział 3. Literały i konstruktory


// wejściowy ciąg znaków JSON
var jstr = '{"klucz": "moja wartość"}';

// przetwórz tekst i zamień go na obiekt,


// używając instancji YUI
YUI().use('json-parse', function (Y) {
var data = Y.JSON.parse(jstr);
console.log(data.klucz); // "moja wartość"
});

Biblioteka jQuery zawiera z kolei metodę parseJSON().


// wejściowy ciąg znaków JSON
var jstr = '{"klucz": "moja wartość"}';

var data = jQuery.parseJSON(jstr);


console.log(data.klucz); // "moja wartość"

Przeciwieństwem metody przetwarzającej format JSON ( JSON.parse() ) jest metoda


JSON.stringify(), która przyjmuje obiekt lub tablicę (a nawet typ prosty) i zamienia je na
ciąg znaków w formacie JSON.
var dog = {
name: "Fido",
dob: new Date(),
legs: [1, 2, 3, 4]
};

var jsonstr = JSON.stringify(dog);

// jsonstr ma wartość:
// {"name":"Fido","dob":"2010-04-11T22:36:22.436Z","legs":[1,2,3,4]}

Literał wyrażenia regularnego


Wyrażenia regularne w języku JavaScript to także obiekty. Istnieją dwa sposoby ich tworzenia:
• za pomocą konstruktora new RegExp(),
• przy użyciu literału wyrażenia regularnego.

Poniższy kod definiuje na dwa różne sposoby wyrażenie regularne dopasowujące się do le-
wego ukośnika.
// literał wyrażenia regularnego
var re = /\\/gm;

// konstruktor
var re = new RegExp("\\\\", "gm");

Jak nietrudno zauważyć, literał wyrażenia regularnego jest krótszy i nie zmusza do myślenia
w kategoriach konstruktorów i klas. Wniosek jest prosty: literał to lepsze rozwiązanie.
Warto pamiętać o tym, że konstruktor RegExp() wymaga stosowania znaków ucieczki dla
cudzysłowów i korzystania z podwójnych lewych ukośników zamiast z pojedynczych. Przed-
stawiony powyżej przykład zawiera cztery ukośniki zamiast dwóch, co czyni wzorzec mniej
czytelnym i trudniejszym w modyfikacji. Wyrażenia regularne generalnie nie należą do naj-
prostszych, więc warto wspierać każde rozwiązanie promujące notację literałową.

Literał wyrażenia regularnego | 59


Składnia literałowego wyrażenia regularnego
Notacja literałowego wyrażenia regularnego wykorzystuje ukośniki do otoczenia właściwego
wzorca. Po drugim ukośniku umieszcza się modyfikatory wzorca w postaci liter bez cudzy-
słowów:
• g — dopasowanie globalne;

• m — dopasowanie wielowierszowe;

• i — dopasowanie bez uwzględniania wielkości liter.

Modyfikatory wzorca mogą wystąpić w dowolnej kolejności.


var re = /wzorzec/gmi;

Literały wyrażeń regularnych upraszczają kod w przypadku stosowania metod takich jak
String.prototype.replace(), które przyjmują wyrażenia regularne jako parametry.
var no_letters = "abc123XYZ".replace(/[a-z]/gi, "");
console.log(no_letters); // 123

W zasadzie jedynymi przypadkami uzasadniającymi korzystanie z new RegExp() są sytuacje,


w których wyrażenie regularne nie jest znane z wyprzedzeniem, ale powstaje w trakcie
działania programu.
Inną różnicą między literałem a konstruktorem, o której warto pamiętać, jest fakt, iż literał
tworzy tylko jeden obiekt w momencie analizy składniowej kodu. Jeśli wyrażenie regular-
ne o takiej samej treści powstaje w pętli, tworzony obiekt jest zwracany ze wszystkimi wła-
ściwościami (na przykład lastIndex) ustawionymi tak jak na końcu jej poprzedniej iteracji.
Niech poniższy przykład posłuży jako ilustracja dwukrotnego zwrócenia tego samego
obiektu.
function getRE() {
var re = /[a-z]/;
re.foo = "bar";
return re;
}

var reg = getRE(),


re2 = getRE();

console.log(reg === re2); // true


reg.foo = "baz";
console.log(re2.foo); // "baz"

ES5 zmieniło omówione zachowanie literałów i w najnowszym wydaniu języka tak-


że one każdorazowo tworzą nowy obiekt. W efekcie zachowanie to wyeliminowano
także w wielu przeglądarkach internetowych, więc nie należy na nim polegać przy
tworzeniu sztuczek podobnych do przedstawionych w przykładzie.

Ostatnia uwaga: wywołanie RegExp() bez new (czyli jako funkcji, a nie konstruktora) działa
identycznie jak wywołanie z new.

60 | Rozdział 3. Literały i konstruktory


Otoczki typów prostych
JavaScript ma pięć typów prostych, którymi są: liczba, tekst, wartość logiczna, null i undefined.
Pierwsze trzy z nich posiadają obiektowe odpowiedniki w postaci tak zwanych otoczek
typów prostych. Obiekty otoczek powstają po zastosowaniu wbudowanych konstruktorów
Number(), String() i Boolean().

Aby zrozumieć różnicę między liczbą prostą i liczbą obiektem, przyjrzyjmy się następującemu
przykładowi:
// liczba jako typ prosty
var n = 100;
console.log(typeof n); // "number"

// obiekt Number
var nobj = new Number(100);
console.log(typeof nobj); // "object"

Obiekty otoczek zapewniają użyteczne właściwości i metody. Obiekt liczby ma na przykład


metody takie jak toFixed() i toExponential(), a obiekt tekstu ma właściwości substring(),
charAt(), toLowerCase()oraz length (a także wiele innych). Metody te są użyteczne i mogą
być dobrym powodem do utworzenia obiektu zamiast typu prostego. Co istotne, działają one
poprawnie również dla typów prostych — JavaScript w takiej sytuacji tymczasowo konwer-
tuje typ prosty na obiekt, by móc wywoływać odpowiednią metodę.
// typ prosty tekstu zastosowany jako obiekt
var s = "witaj";
console.log(s.toUpperCase()); // "WITAJ"

// także wartość może działać jak obiekt


"małpka".slice(3, 6); // "pka"

// tak samo dla liczb


(22 / 7).toPrecision(3); // "3.14"

Ponieważ typy proste działają jak obiekty, gdy tylko wymaga tego sytuacja, najczęściej nie
ma powodu, by stosować znacznie dłuższe konstruktory otoczek. Innymi słowy, nie trzeba
pisać new String("witaj"), gdy wystarczy samo "witaj".
// unikaj następujących zapisów:
var s = new String("tekst");
var n = new Number(101);
var b = new Boolean(true);

// stosuj prostsze wersje:


var s = "tekst";
var n = 101;
var b = true;

Jedną z sytuacji, w których obiekty otoczek bywają przydatne, jest zmiana wartości i zacho-
wanie stanu. Ponieważ typy proste nie są obiektami, nie mogą być zmieniane przy użyciu
właściwości.
// tekst jako typ prosty
var greet = "Witaj, kolego";

// typ prosty zostaje zamieniony na obiekt,


// by można było wywołać metodę split()
greet.split(' ')[0]; // "Witaj"

Otoczki typów prostych | 61


// próba zmiany właściwości typu prostego nie zgłasza błędu,
greet.smile = true;

// ale tak naprawdę nie działa zgodnie z oczekiwaniami


typeof greet.smile; // "undefined"

W przedstawionym przykładzie zmienna greet została tylko tymczasowo przekonwertowa-


na na obiekt, by próba dostępu do właściwości i metod zadziałała i nie zgłosiła błędu. Z dru-
giej strony, gdyby zdefiniować greet jako obiekt za pomocą new String(), zmodyfikowana
(a w zasadzie dodana) właściwość smile zadziałałaby prawidłowo. Dodawanie własnych
właściwości do liczby, tekstu lub wartości logicznej to jednak bardzo rzadko spotykana
praktyka i jeśli nie jest wymagana, lepiej nie korzystać z obiektów otoczek.
Gdy konstruktory otoczek zostają użyte bez new, konwertują przekazany do nich argument
na typ prosty.
typeof Number(1); // "number"
typeof Number("1"); // "number"
typeof Number(new Number()); // "number"
typeof String(1); // "string"
typeof Boolean(1); // "boolean"

Obiekty błędów
Język JavaScript oferuje kilka wbudowanych konstruktorów dotyczących błędów: Error(),
SyntaxError(), TypeError() i tak dalej. Są one stosowane w połączeniu z instrukcją throw.
Obiekty błędów utworzone przez powyższe konstruktory mają następujące właściwości:
• name — nazwa konstruktora tworzącego obiekt błędu; może zawierać słowo Error
w przypadku błędu ogólnego lub bardziej specyficzny tekst, na przykład RangeError;
• message — tekst przekazany do konstruktora w momencie tworzenia obiektu.

Obiekty błędów mają również dodatkowe właściwości informujące o pliku i numerze wiersza,
w którym błąd wystąpił, ale te informacje to rozszerzenia wprowadzone niejednolicie przez
różne przeglądarki, więc nie można na nich polegać.
Z drugiej strony instrukcja throw działa prawidłowo nie tylko dla obiektów utworzonych za
pomocą konstruktorów błędów, ale pozwala na zgłoszenie dowolnego obiektu. Taki obiekt
może zawierać właściwości name, message lub dowolne inne, które powinny trafić do in-
strukcji catch. Okazuje się, że można to wykorzystać w bardzo kreatywny sposób i nierzadko
przywrócić po błędzie aplikację do stanu początkowego.
try {
// stało się coś złego, zgłoś błąd
throw {
name: "MyErrorType", // własny typ błędu
message: "Ojej",
extra: "To coś wstydliwego",
remedy: genericErrorHandler // kto powinien obsłużyć błąd
};
} catch (e) {
// poinformuj użytkownika
alert(e.message); // "Ojej"
// obsłuż błąd w przewidziany wcześniej sposób
e.remedy(); // wywołuje genericErrorHandler()
}

62 | Rozdział 3. Literały i konstruktory


Konstruktory błędów wywołane jako funkcje (bez new) zachowują się dokładnie tak samo jak
wersje wywołane jako konstruktory (z new), czyli zwracają obiekt błędu.

Podsumowanie
W niniejszym rozdziale przedstawiono różne wzorce dotyczące literałów, które są prostszy-
mi alternatywami dla funkcji konstruujących. Rozdział omawia następujące tematy:
• Notacja literału obiektu — elegancki sposób tworzenia obiektów jako oddzielonych prze-
cinkami par nazwa-wartość otoczonych nawiasami klamrowymi.
• Funkcje konstruujące — konstruktory (które prawie zawsze mają swoje lepsze i krótsze
odpowiedniki literałowe) i funkcje własne.
• Metody tworzenia konstruktorów własnych w taki sposób, by zawsze zachowywały się
tak, jakby zostały wywołane z użyciem new.
• Notacja literału tablicy — lista oddzielonych przecinkami wartości otoczona nawiasami
kwadratowymi.
• JSON — format danych składający się z literałów obiektów i tablic.
• Literały wyrażeń regularnych.
• Inne konstruktory wbudowane, których należy unikać: String(), Number(), Boolean()
i różne konstruktory Error().
Z wyjątkiem konstruktora Date() rzadko zachodzi potrzeba stosowania innych wbudowa-
nych konstruktorów. Poniższa tabela zestawia konstruktory i ich preferowane odpowiedniki.

Konstruktory wbudowane (unikaj) Literały i typy proste (preferuj)


var o = new Object(); var o = {};
var a = new Array(); var a = [];
var re = new RegExp( var re = /[a-z]/g;
"[a-z]",
"g"
);
var s = new String(); var s = "";
var n = new Number(); var n = 0;
var b = new Boolean(); var b = false;
throw new Error("och"); throw {
name: "Error",
message: "och"
};
lub
throw Error("och");

Podsumowanie | 63
64 | Rozdział 3. Literały i konstruktory
ROZDZIAŁ 4.

Funkcje

Doskonałe posługiwanie się funkcjami to niezbędna umiejętność dla programisty JavaScript,


gdyż język używa ich niemal na każdym kroku. Są wykorzystywane do zadań, dla których
wiele innych języków ma specjalną składnię.
W tym rozdziale przedstawione zostaną różne sposoby definiowania funkcji — deklaracje
i wyrażenia funkcji —lokalny zakres zmiennych i przenoszenie deklaracji na początek funkcji.
Nie zabraknie również omówienia wzorców przydatnych przy tworzeniu interfejsów pro-
gramistycznych, inicjalizacji kodu (ze zmniejszeniem liczby zmiennych globalnych) i uzyski-
waniu większej wydajności (lub unikaniu dodatkowej pracy).
Zanim jednak poruszone zostaną tematy zaawansowane, kilka podstaw.

Informacje ogólne
Dwie cechy funkcji w języku JavaScript powodują, że są one szczególne: po pierwsze, są
pełnoprawnymi obiektami, a po drugie, określają zakres zmiennych.
Funkcje są obiektami i dlatego:
• mogą być tworzone dynamicznie w trakcie działania programu;
• mogą być przypisywane do zmiennych, ich referencje można przekazywać do innych
zmiennych, można przypisywać im właściwości i poza kilkoma wyjątkami można je
usuwać;
• mogą być przekazywane jako argumenty do innych funkcji, a także być z innych funkcji
zwracane;
• mogą mieć własne właściwości i metody.

Zdarza się, że funkcja A, będąc obiektem, ma właściwości i metody, a jedną z nich jest inna
funkcja B. Ta przyjmuje funkcję C jako argument i po wykonaniu zwraca jako wynik funkcję
D. Na pierwszy rzut oka może to przytłaczać, bo trzeba śledzić wiele dróg działania, jednak
po pewnym czasie zaczyna się doceniać tę elastyczność, siłę ekspresji i moc kryjącą się za
funkcjami. Najogólniej rzecz biorąc, funkcję w języku JavaScript należy traktować tak samo
jak każdy inny obiekt, który jednak dodatkowo posiada pewną istotną cechę — może zostać
wykonany.

65
To, że funkcje są obiektami, w bardzo dobitny sposób uwidacznia konstruktor new Function().
// antywzorzec
// przedstawione jedynie w celach poglądowych
var add = new Function('a, b', 'return a + b');
add(1, 2); // zwraca 3

Nie mamy wątpliwości, że w zaprezentowanym kodzie add() to obiekt — został przecież


utworzony za pomocą konstruktora. Konstruktor Function() nie jest najlepszym rozwiąza-
niem, ponieważ przekazywany do niego kod jest tekstem i zostaje przetworzony w podobny
sposób jak przez funkcję eval(). Oczywiście problemów jest znacznie więcej, bo trzeba pa-
miętać o odpowiednich znakach ucieczki przed cudzysłowami i trudno jest korzystać z wcięć
zapewniających czytelność kodu.
Drugą bardzo istotną cechą funkcji jest określanie przez nie zakresu (zasięgu) zmiennych.
W języku JavaScript nie istnieje lokalny zakres zmiennych określany przez nawiasy klamrowe
— bloki kodu go nie definiują. Jedynym sposobem wyznaczania zakresu są funkcje. Każda
zmienna definiowana wewnątrz nich za pomocą słowa kluczowego var jest dostępna jedynie
lokalnie, ale w całej funkcji. Brak zakresu określanego przez nawiasy klamrowe oznacza, że
jeśli zdefiniuje się zmienną lokalną (używając var) w warunku if lub w pętli for, nie będzie
ona lokalna tylko dla tego bloku if lub for. Będzie lokalna względem otaczającej ją funkcji,
a jeśli nie będzie istniała funkcja otaczająca, stanie się zmienną globalną. Zgodnie z rozdzia-
łem 2. warto ograniczać do minimum użycie zmiennych globalnych, a funkcje okazują się
w tym temacie wprost niezastąpione.

Stosowana terminologia
Poświęćmy chwilę na ustalenie terminologii dotyczącej kodu tworzonego za pomocą funkcji,
ponieważ dokładne i dobrze rozumiane nazwy są istotne, gdy mówi się o wzorcach.
Rozważmy następujący fragment kodu:
// nazwane wyrażenie funkcyjne
var add = function add(a, b) {
return a + b;
};

Powyższy kod przedstawia funkcję, która używa nazwanego wyrażenia funkcyjnego.


Jeśli pominie się nazwę (drugie add w przykładzie) w wyrażeniu funkcyjnym, uzyska się
nienazwane wyrażenie funkcyjne określane w skrócie wyrażeniem funkcyjnym lub częściej
funkcją anonimową. Oto przykład:
// wyrażenie funkcyjne nazywane powszechnie funkcją anonimową
var add = function (a, b) {
return a + b;
};

Ogólnym terminem jest wyrażenie funkcyjne. Nazwane wyrażenie funkcyjne to jedynie konkret-
ny przypadek wyrażenia funkcyjnego, który określa dodatkowo opcjonalną nazwę funkcji.
Pominięcie drugiego add i powstanie nienazwanego wyrażenia funkcyjnego nie wpłynie na
definicję i sposób działania funkcji. Jedyna różnica polegać będzie na tym, że jej właściwość
name będzie pustym tekstem. Właściwość ta stanowi rozszerzenie języka (nie jest częścią
standardu ECMA), ale jest powszechnie stosowana w wielu środowiskach. Zachowanie dru-
giego add spowoduje, że właściwość add.name będzie zawierała tekst add. Przydaje się to

66 | Rozdział 4. Funkcje
w momencie testowania kodu na przykład narzędziem Firebug lub w przypadku wielokrot-
nego wywoływania przez funkcję samej siebie (rekurencja); w innych sytuacjach właściwość
name można z czystym sumieniem pominąć.

Drugą konstrukcją są deklaracje funkcji, które składniowo przypominają rozwiązania znane


z innych języków programowania.
function foo() {
// tu znajduje się treść funkcji
}

W aspekcie składniowym nazwane wyrażenia funkcyjne i deklaracje funkcji wyglądają bardzo


podobnie, szczególnie jeśli wynik wyrażenia funkcyjnego nie jest przypisywany do zmiennej
(przykłady takiej konstrukcji znajdują się w dalszej części rozdziału w opisie wzorca wywo-
łania zwrotnego). Czasem jedynym sposobem określenia rodzaju konstrukcji jest przyjrzenie
się jej kontekstowi, bo deklaracje funkcji mają ściśle określoną składnię.
Jedną z różnic składniowych jest stosowanie średnika kończącego treść funkcji. W przypadku
deklaracji funkcji jest on zbędny, ale wymaga się go w wyrażeniach funkcyjnych (przy zało-
żeniu, że stosujemy go dla wszystkich poleceń i nie polegamy na mechanizmie automatycz-
nego wstawiania średników).

Niejednokrotnie można się również spotkać z terminem literał funkcji, który może
oznaczać zarówno deklarację funkcji, jak i nazwane wyrażenie funkcyjne. Z powodu
tej niejednoznaczności powyższy termin nie jest stosowany w książce.

Deklaracje kontra wyrażenia


— nazwy i przenoszenie na początek
Które z rozwiązań stosować: deklaracje funkcji czy wyrażenia funkcyjne? W sytuacjach,
w których ze względów składniowych deklaracje nie są możliwe, dylemat rozwiązuje się sam.
Przykładami mogą być przekazywanie funkcji jako parametru lub definiowanie metod w li-
terałach obiektów.
// to jest wyrażenie funkcyjne
// przekazane jako parametr do funkcji callMe
callMe(function () {
// Jestem nienazwanym wyrażeniem funkcyjnym
// znanym powszechnie jako funkcja anonimowa.
});

// to jest nazwane wyrażenie funkcyjne


callMe(function me() {
// Jestem nazwanym wyrażeniem funkcyjnym
// i moja nazwa to "me".
});

// inne wyrażenie funkcyjne


var myobject = {
say: function () {
// Jestem wyrażeniem funkcyjnym.
}
};

Informacje ogólne | 67
Deklaracje funkcji mogą pojawiać się jedynie w „kodzie programu”, czyli wewnątrz innych
funkcji lub w przestrzeni globalnej. Definicji nie można przypisać do zmiennych lub właści-
wości albo wykorzystać w wywołaniach funkcji jako parametru. Poniżej znajduje się kilka
przykładów prawidłowo napisanych deklaracji funkcji foo(), bar() i local(). Wszystkie
korzystają ze wzorca deklaracji funkcji.
// zakres globalny
function foo() {}

function local() {
// zakres lokalny
function bar() {}
return bar;
}

Właściwość name funkcji


Zastanawiając się, czy skorzystać ze wzorca definicji funkcji, warto wziąć pod uwagę wyko-
rzystanie właściwości name. Właściwość ta nie stanowi części standardu, ale jest dostępna w wielu
środowiskach po zastosowaniu deklaracji funkcji lub nazwanego wyrażenia funkcyjnego.
Funkcje anonimowe (nienazwane wyrażenia funkcyjne) w zależności od implementacji albo
mają tę właściwość niezdefiniowaną (IE), albo ustawioną na pusty tekst (Firefox, WebKit).
function foo() {} // deklaracja
var bar = function () {}; // wyrażenie
var baz = function baz() {}; // nazwane wyrażenie

foo.name; // "foo"
bar.name; // ""
baz.name; // "baz"

Właściwość name okazuje się przydatna w momencie testowania kodu w narzędziu Firebug
lub innym debuggerze. Gdy musi on wyświetlić informację o powstaniu błędu w określonej
funkcji, może wykorzystać zawartość jej właściwości name. Nazwa funkcji przydaje się także
do jej rekurencyjnego wywoływania. Jeśli jednak oba zastosowania mają małą szansę zaist-
nienia, nienazwane wyrażenie funkcyjne będzie prostsze i krótsze.
Przeciwko deklaracjom funkcji przemawia fakt, iż niejako ukrywają one to, że funkcje są tak
naprawdę obiektami ze wszystkimi tego konsekwencjami — deklaracja zbyt mocno sugeruje
istnienie specjalnej konstrukcji językowej.

Z technicznego punktu widzenia nic nie stoi na przeszkodzie, by użyć nazwanego wy-
rażenia funkcyjnego, a następnie przypisać jego wynik do zmiennej o innej nazwie:
var foo = function bar() {};

Niestety, rozwiązanie to nie zostało poprawnie zaimplementowane w starszych wer-


sjach przeglądarki IE, więc lepiej z niego nie korzystać.

Przenoszenie deklaracji funkcji


Po przeczytaniu poprzednich akapitów można by odnieść wrażenie, że działanie deklaracji
funkcji jest w zasadzie równoważne działaniu nazwanego wyrażenia funkcyjnego. Niestety
nie jest to prawda — różnica tkwi w zachowaniu dotyczącym przenoszenia kodu funkcji na
początek zakresu.

68 | Rozdział 4. Funkcje
Przenoszenie deklaracji zmiennych i funkcji na początek kodu funkcji je zawierają-
cych jest w literaturze angielskiej określane terminem hoisting (podnoszenie). Co cie-
kawe, termin ten nie pojawia się w standardzie ECMAScript, choć jest powszechnie
wykorzystywany do obrazowania zachowania języka w tym zakresie.

Jak już wcześniej wspomniano, wszystkie zmienne, niezależnie od ich położenia w treści
funkcji, są w rzeczywistości automatycznie przenoszone na jej początek. To samo dzieje się
z funkcjami, ponieważ są one jedynie obiektami przypisanymi do zmiennych. Pewien niuans
pojawia się w przypadku deklaracji funkcji, bo przeniesienie dotyczy nie tylko deklaracji, ale
także definicji funkcji. Rozważmy następujący przykład:
// antywzorzec
// przedstawiony tylko w celach ilustracyjnych

// funkcje globalne
function foo() {
alert('globalne foo');
}
function bar() {
alert('globalne bar');
}

function hoistMe() {

console.log(typeof foo); // "function"


console.log(typeof bar); // "undefined"

foo(); // "lokalne foo"


bar(); // TypeError: bar is not a function

// deklaracja funkcji:
// zmienna foo i jej implementacja zostały przeniesione na początek
function foo() {
alert('lokalne foo');
}

// wyrażenie funkcyjne:
// przeniesiona została jedynie zmienna bar
// bez implementacji
var bar = function () {
alert('lokalne bar');
};
}
hoistMe();

Przykład pokazuje, że podobnie jak ma to miejsce w przypadku zwykłych zmiennych, już


samo istnienie foo i bar wewnątrz hoistMe() przenosi deklaracje na początek funkcji i przy-
słania globalne foo i bar. Różnica polega na tym, że w przypadku lokalnego foo() także de-
finicja została przeniesiona na początek i działa prawidłowo. Jeśli chodzi o bar(), przenie-
sieniu uległa jedynie deklaracja bez implementacji. Oznacza to, że do momentu osiągnięcia
przez kod definicji zmienna ma wartość undefined i nie może być wykorzystywana jako
funkcja (blokuje również możliwość wykorzystania globalnej wersji bar()).
Po zapoznaniu się z odpowiednią terminologią i sposobami tworzenia funkcji możemy przy-
stąpić do omówienia polecanych wzorców związanych z funkcjami dostępnymi w języku
JavaScript. Zaczniemy od wzorca wywołania zwrotnego. Warto cały czas pamiętać o dwóch
istotnych cechach funkcji języka JavaScript:
• są to obiekty,
• są sposobem określania lokalnego zakresu zmiennych.

Informacje ogólne | 69
Wzorzec wywołania zwrotnego
Funkcje to obiekty, co oznacza, że mogą one być przekazywane jako argumenty do innych
funkcji. Przekazanie introduceBugs() jako parametru do funkcji writeCode() spowoduje
prawdopodobnie, że w pewnym momencie writeCode() wykona (wywoła) introduceBugs().
W takiej sytuacji introduceBugs() nosi nazwę funkcji wywołania zwrotnego lub jest okre-
ślana po prostu wywołaniem zwrotnym.
function writeCode(callback) {
// wykonaj zadania...
callback();
// ...
}

function introduceBugs() {
// ... dodaj błędy
}

writeCode(introduceBugs);

Funkcja introduceBugs() została przekazana jako argument do writeCode() bez użycia na-
wiasów. Nawiasy powodują wykonanie funkcji, a zadaniem kodu było jedynie przekazanie
jej jako referencji i pozwolenie writeCode() na zdecydowanie, czy i kiedy należy ją wykonać.

Przykład wywołania zwrotnego


Zacznijmy od przykładu, który początkowo nie będzie korzystał z wywołania zwrotnego,
a następnie zostanie przerobiony. Wyobraźmy sobie pewną ogólną funkcję, która wykonuje
złożone zadania i jako wynik zwraca duży zbiór danych. Ta ogólna funkcja może nosić nazwę
findNodes() i przeglądać drzewo DOM w poszukiwaniu interesujących elementów, które
następnie zwraca jako tablicę.
var findNodes = function () {
var i = 100000, // duża i zasobożerna pętla
nodes = [], // zapamiętanie wyniku
found; // kolejny znaleziony węzeł
while (i) {
i -= 1;
// złożona logika...
nodes.push(found);
}
return nodes;
};

Dobrze byłoby, aby funkcja pozostała jak najbardziej ogólna i po prostu zwracała listę wę-
złów DOM bez przeprowadzania na nich dodatkowych operacji. Logika odpowiedzialna za
modyfikację węzłów może znajdować się w innej funkcji, na przykład hide(), która ukrywa
na stronie znalezione węzły.
var hide = function (nodes) {
var i = 0, max = nodes.length;
for (; i < max; i += 1) {
nodes[i].style.display = "none";
}
};

// wykonanie funkcji
hide(findNodes());

70 | Rozdział 4. Funkcje
Implementacja ta jest nieefektywna, ponieważ hide() musi ponownie przejść w pętli przez
wszystkie węzły zwrócone przez findNodes(). Znacznie lepiej byłoby uniknąć tej dodatko-
wej pętli i ukrywać węzły, gdy tylko zostaną znalezione w findNodes(). Implementacja logiki
ukrywania w findNodes() nie jest dobrym rozwiązaniem, bo funkcja przestałaby być uni-
wersalna. To doskonała okazja, by użyć wzorca wywołania zwrotnego przez zaszycie logiki
ukrywania w funkcji wywołania zwrotnego przekazywanej do findNodes().
// findNodes() po dodaniu obsługi funkcji zwrotnej
var findNodes = function (callback) {
var i = 100000,
nodes = [],
found;

// sprawdzenie, czy parametr callback jest funkcją


if (typeof callback !== "function") {
callback = false;
}

while (i) {
i 3= 1;

// złożona logika...

// wywołanie zwrotne:
if (callback) {
callback(found);
}

nodes.push(found);
}
return nodes;
};

Powyższa implementacja jest prosta — jedynym dodatkowym zadaniem wykonywanym przez


findNodes() jest sprawdzenie, czy przekazano opcjonalne wywołanie zwrotne, i jeśli tak
uczyniono, wykonanie go. Przekazanie funkcji wywołania zwrotnego jest opcjonalne, więc
funkcja po modyfikacjach nadal działa prawidłowo ze starszym kodem korzystającym ze
starszego API.
Implementacja funkcji hide() będzie obecnie znacznie prostsza, bo nie wymaga tworzenia
pętli przechodzącej przez wszystkie węzły.
// funkcja wywołania zwrotnego
var hide = function (node) {
node.style.display = "none";
};

// znajdź węzły i ukryj je


findNodes(hide);

Wywołanie zwrotne może być istniejącą funkcją (jak w powyższym kodzie) lub funkcją ano-
nimową tworzoną w momencie wywoływania głównej funkcji. Poniżej znajduje się przykład
wykorzystania tej samej funkcji findNodes() wraz z funkcją anonimową.
// przekazanie anonimowego wywołania zwrotnego
findNodes(function (node) {
node.style.display = "block";
});

Wzorzec wywołania zwrotnego | 71


Wywołania zwrotne a zakres zmiennych
W poprzednich przykładach wywołanie zwrotne było wykonywane w bardzo prosty sposób:
callback(parameters);

Jest to rozwiązanie sprawdzające się w wielu sytuacjach, ale czasem pojawiają się przypadki,
w których funkcja zwrotna nie jest prostą funkcją anonimową lub funkcją globalną, ale sta-
nowi metodę obiektu. Jeśli metoda zwrotna wykorzystuje this, by odnieść się do obiektu, do
którego przynależy, mogą pojawić się nieoczekiwane efekty uboczne.
Załóżmy, że wywołanie zwrotne jest funkcją paint() będącą jednocześnie metodą obiektu
o nazwie myapp.
var myapp = {};
myapp.color = "green";
myapp.paint = function (node) {
node.style.color = this.color;
};

Funkcja findNodes() wykonuje następującą operację:


var findNodes = function (callback) {
// ...
if (typeof callback === "function") {
callback(found);
}
// ...
};

Wywołanie findNodes(myapp.paint) nie zadziała prawidłowo, ponieważ this.color nie jest


zdefiniowane. Obiekt this jest tak naprawdę obiektem globalnym, ponieważ findNodes() to
funkcja globalna. Gdyby findNodes() było metodą obiektu o nazwie dom (dom.findNodes()),
to this odnosiłoby się do obiektu dom zamiast do spodziewanego obiektu myapp.
Rozwiązaniem problemu jest przekazanie funkcji zwrotnej wraz z referencją do obiektu, do
którego należy funkcja zwrotna.
findNodes(myapp.paint, myapp);

Pozostaje jeszcze zmodyfikować funkcję findNodes(), by przyjmowała dodatkowy parametr.


var findNodes = function (callback, callback_obj) {
// ...
if (typeof callback === "function") {
callback.call(callback_obj, found);
}
// ...
};

Więcej informacji na temat dowiązań i użycia call() oraz apply() pojawi się w dalszych
rozdziałach.
Innym rozwiązaniem jest przekazanie obiektu w sposób standardowy, a metody jako tekstu.
Dzięki temu nie trzeba powtarzać nazwy obiektu. Innymi słowy:
findNodes(myapp.paint, myapp);

zmienia się w
findNodes("paint", myapp);

72 | Rozdział 4. Funkcje
W tej sytuacji findNodes() wykona następującą operację:
var findNodes = function (callback, callback_obj) {

if (typeof callback === "string") {


callback = callback_obj[callback];
}

// ...
if (typeof callback === "function") {
callback.call(callback_obj, found);
}
// ...
};

Funkcje obsługi zdarzeń asynchronicznych


Obecnie wzorzec wywołania zwrotnego ma wiele zastosowań. Przykładowo, przypisanie
funkcji obsługi zdarzenia do elementu na stronie to tak naprawdę przekazanie wskaźnika
do funkcji, która zostanie wykonana po zajściu zdarzenia. Poniższy przykład ilustruje, w jaki
sposób można przekazać funkcję console.log() jako funkcję zwrotną dla obsługi wszystkich
zdarzeń kliknięcia wewnątrz dokumentu.
document.addEventListener("click", console.log, false);

Spora część programowania wewnątrz przeglądarek internetowych bazuje na obsłudze zdarzeń.


Po wczytaniu strony przeglądarka generuje zdarzenie load. Gdy użytkownik wchodzi w in-
terakcję ze stroną, wymusza zgłoszenie kilku zdarzeń takich jak click, keypress, mouseover
czy mousemove. Język JavaScript bardzo dobrze sprawdza się w programowaniu opartym na
zdarzeniach, ponieważ wzorzec wywołania zwrotnego umożliwia pracę programu w sposób
asynchroniczny (czyli poza jedną ustaloną kolejnością).
W Hollywood często słyszy się: „Nie dzwoń do nas, to my do ciebie zadzwonimy”, gdy jest
się uczestnikiem castingu do roli w filmie. Gdyby zespół odpowiedzialny za znalezienie od-
powiednich aktorów cały czas zajmował się odbieraniem telefonów, nie wykonałby właści-
wych zadań. Asynchroniczny, bazujący na zdarzeniach JavaScript działa na podobnej zasadzie.
Zamiast przekazywać swój numer telefonu, przekazujemy funkcję wywołania zwrotnego
wywoływaną przez odbiorcę w odpowiednim momencie. Czasem przekazuje się więcej wy-
wołań zwrotnych, niż potrzeba, bo niektóre z nich mogą nigdy nie wystąpić. Jeśli użytkownik
nigdy nie kliknie przycisku „Kup teraz!”, funkcja sprawdzająca poprawność numeru karty
kredytowej nigdy się nie wykona.

Funkcje czasowe
Innym często spotykanym przykładem użycia wywołań zwrotnych jest korzystanie z funkcji
czasowych zapewnianych przez obiekt window przeglądarki: setTimeout() i setInterval().
Metody te również przyjmują i wykonują funkcje wywołań zwrotnych.
var thePlotThickens = function () {
console.log('500 ms później...');
};
setTimeout(thePlotThickens, 500);

Wzorzec wywołania zwrotnego | 73


Funkcja thePlotThickens zostaje przekazana jako zmienna bez nawiasów, więc nie zostanie
wykonana od razu. Do obiektu przekazana została tylko referencja, którą setTimeout() wy-
kona w odpowiednim momencie. Przekazanie tekstu thePlotThickens() zamiast funkcji to
typowy antywzorzec podobny do eval().

Wywołania zwrotne w bibliotekach


Wywołanie zwrotne to prosty i bardzo elastycznych wzorzec, więc często przydaje się przy pro-
jektowaniu własnych bibliotek. Kod umieszczany w bibliotece powinien być możliwie ogólny
i ułatwiać wykorzystanie go w różnych sytuacjach. Wywołania zwrotne okazują się tu doskona-
łym pomocnikiem. Nie trzeba wymyślać i przewidywać wszystkich możliwych implementacji
pewnej funkcjonalności, bo zwiększy to jedynie rozmiar biblioteki, a większość użytkowników
i tak z tych rozwiązań nigdy nie skorzysta. Zamiast tego skupiamy się na funkcjonalności pod-
stawowej i dodajemy „haczyki” w postaci funkcji wywołań zwrotnych. Dzięki temu biblioteka
pozostaje lekka i elastyczna, a użytkownik może ją dowolnie rozszerzać według potrzeb.

Zwracanie funkcji
Funkcje to obiekty, więc mogą również stanowić wynik działania innych funkcji. Oznacza to,
że funkcja nie musi jedynie zwracać pewnych danych lub tablic danych jako wyniku swego
wykonania. Może zwrócić inną, bardziej wyspecjalizowaną funkcję lub nawet utworzyć
funkcję na żądanie w zależności od przekazanych parametrów początkowych.
Oto prosty przykład: funkcja wykonuje pewne zadanie (najprawdopodobniej pewną jednora-
zową inicjalizację), a następnie przetwarza dane, by zwrócić wartość wynikową. Ta wynikowa
wartość również jest funkcją, którą można wykonać.
var setup = function () {
alert(1);
return function () {
alert(2);
};
};

// wykorzystanie funkcji inicjalizującej


var my = setup(); // wyświetla 1
my(); // wyświetla 2

Ponieważ funkcja setup() otacza zwróconą funkcję, tworzy domknięcie, które można wyko-
rzystać do przechowywania pewnych prywatnych danych dostępnych dla zwróconej funkcji,
ale nie dla świata zewnętrznego. Przykładem może być licznik, który zwiększa swoją wartość
po każdym wywołaniu funkcji.
var setup = function () {
var count = 0;
return function () {
return (count += 1);
};
};

// użycie
var next = setup();
next(); // zwraca 1
next(); // zwraca 2
next(); // zwraca 3

74 | Rozdział 4. Funkcje
Samodefiniujące się funkcje
Funkcje można definiować dynamicznie i przypisywać do zmiennych. Jeśli utworzy się nową
funkcję i przypisze do zmiennej, która przechowuje już inną funkcję, nowa funkcja nadpisze
starą. Można powiedzieć, że wielokrotnie wykorzystujemy tę samą zmienną do różnych celów.
Co ciekawe, cała opisana sytuacja może zajść wewnątrz starej funkcji. Wówczas funkcja przede-
finiowuje samą siebie, zapewniając nową implementację. Prawdopodobnie wydaje się to bar-
dziej skomplikowane, niż jest w rzeczywistości, więc przyjrzyjmy się prostemu przykładowi.
var scareMe = function () {
alert("Buu!");
scareMe = function () {
alert("Podwójne buu!");
};
};

// użycie samodefiniującej się funkcji


scareMe(); // Buu!
scareMe(); // Podwójne buu!

Wzorzec ten przydaje się, gdy funkcja ma do wykonania pewne podstawowe zadania inicja-
cyjne, ale są one przeprowadzane tylko jednokrotnie. Ponieważ nie ma potrzeby ich powta-
rzać, odpowiedzialną za nie część kodu można usunąć. W takich sytuacjach samodefiniująca
się funkcja może uaktualnić własną implementację.
Wykorzystanie tego wzorca z pewnością pomoże uzyskać lepszą wydajność aplikacji, bo
funkcja po prostu wykonuje mniej zadań.

Inna nazwa tego wzorca to leniwa definicja funkcji, ponieważ funkcja nie jest w peł-
ni zdefiniowana aż do momentu jej pierwszego użycia. Najczęściej po wstępnej ini-
cjalizacji wykonuje też mniej zadań.

Wadą zaprezentowanego rozwiązania jest fakt, iż właściwości dodane do oryginalnej wersji


funkcji zostaną utracone w momencie przypisania nowej. Co więcej, jeśli funkcja jest stoso-
wana pod inną nazwą (na przykład została przypisana do innej zmiennej lub trafiła jako
metoda do obiektu), zmiana definicji nie powiedzie się i cały czas będzie stosowana wersja
oryginalna.
Prześledźmy sytuację, w której funkcja scareMe() zostanie użyta jako pełnoprawny obiekt, czyli:
1. Zostanie dodana nowa właściwość.
2. Obiekt funkcji trafi do nowej zmiennej.
3. Funkcja zostanie użyta jako metoda.
Oto przykładowy kod:
// 1. Dodanie nowej właściwości.
scareMe.property = "prawidłowo";

// 2. Przypisanie do innej zmiennej.


var prank = scareMe;

// 3. Użycie w charakterze metody.


var spooky = {
boo: scareMe

Samodefiniujące się funkcje | 75


};

// wywołanie pod nową nazwą


prank(); // "Buu!"
prank(); // "Buu!"
console.log(prank.property); // "prawidłowo"

// wywołanie w charakterze metody


spooky.boo(); // "Buu!"
spooky.boo(); // "Buu!"
console.log(spooky.boo.property); // "prawidłowo"

// użycie samomodyfikującej się funkcji


scareMe(); // "Podwójne buu!"
scareMe(); // "Podwójne buu!"
console.log(scareMe.property); // undefined

Jak można zauważyć, zmiana na samomodyfikującą się wersję nie powiodła się w przypadku
funkcji przypisanej do nowej zmiennej. Wszystkie wywołania prank() powodowały wy-
świetlenie wartości Buu!. Przy okazji została nadpisana globalna wersja funkcji scareMe(),
ale prank() nadal wyświetlało stary tekst, włączając w to zawartość właściwości property.
Ta sama sytuacja miała miejsce w przypadku metody boo() obiektu spooky. Wszystkie wy-
wołania nadpisywały globalną funkcję scareMe(), więc gdy ta została wywołana po raz
pierwszy, od razu zwróciła tekst „Podwójne buu!”. Co więcej, utracona została właściwość
scareMe.property.

Funkcje natychmiastowe
Wzorzec funkcji natychmiastowej to składnia pozwalająca wykonać funkcję tuż po jej zdefi-
niowaniu. Oto przykład:
(function (){
alert('Uważaj!');
}());

Przedstawiony wzorzec to zwykłe wyrażenie funkcyjne (nazwane lub anonimowe), które jest
wykonywane, gdy tylko zostanie zdefiniowane. Termin funkcja natychmiastowa nie pojawia
się w standardzie ECMAScript, ale jest bardzo zwięzły i dobrze opisuje rzeczywistość.
Wzorzec składa się z następujących części:
• definicji funkcji sformułowanej za pomocą wyrażenia funkcyjnego (forma deklaracyjna
nie zadziała);
• nawiasów okrągłych, które pojawiają się po definicji funkcji i powodują jej natychmia-
stowe wykonanie;
• nawiasów, które otaczają całą funkcję (są one niezbędne, gdy nie przypisuje się funkcji
do zmiennej).
Popularna jest również poniższa alternatywna wersja składni (zmienia się położenie nawiasu
zamykającego), ale JSLint preferuje pierwszą wersję.
(function (){
alert('Uważaj!');
})();

76 | Rozdział 4. Funkcje
Wzorzec ten jest bardzo przydatny, bo zapewnia ograniczenie zakresu zmiennych związa-
nych z kodem inicjującym. Rozważmy następujący scenariusz: kod musi przeprowadzić tuż
po wczytaniu strony pewne operacje początkowe takie jak przypisanie funkcji obsługi zda-
rzeń i utworzenie obiektów. Cała praca musi zostać wykonana tylko jeden raz, więc nie ma
potrzeby tworzenia nazwanej funkcji wielokrotnego użytku. Z drugiej strony kod wymaga
pewnych zmiennych tymczasowych, które po fazie inicjalizacji stają się zbędne. Czynienie
z nich zmiennych globalnych nie jest dobrym pomysłem. Właśnie z tego powodu warto za-
stosować funkcję natychmiastową — pozwoli ona na umieszczenie wszystkich zmiennych
w zakresie lokalnym bez zaśmiecania części globalnej.
(function () {

var days = ['niedz.', 'pon.', 'wt.', 'śr.', 'czw.', 'pt.', 'sob.'],


today = new Date(),
msg = 'Dziś jest ' + days[today.getDay()] + ', ' + today.getDate();

alert(msg);

}()); // "Dziś jest pt., 13"

Gdyby kod nie został otoczony funkcją natychmiastową, zmienne days, today i msg stałyby
się zmiennymi globalnymi, choć tak naprawdę to tylko pozostałości po kodzie inicjującym.

Parametry funkcji natychmiastowych


Do funkcji natychmiastowych można także przekazać argumenty, co przedstawia poniższy
przykład.
// wyświetla:
// Spotkałem Jana Kowalskiego w dniu Tue Sep 20 2011 07:08:46 GMT+0200 (Środkowoeuropejski czas letni)

(function (who, when) {


console.log("Spotkałem " + who + " w dniu " + when);
}("Jana Kowalskiego", new Date()));

Bardzo często jako argument przekazuje się do funkcji natychmiastowej obiekt globalny, by
był on dostępny wewnątrz funkcji bez potrzeby korzystania z nazwy window. Rozwiązanie to
czyni kod bardziej przenośnym, bo działa prawidłowo w środowiskach innych niż przeglą-
darka internetowa.
(function (global) {

// dostęp do obiektu globalnego uzyskiwany za pomocą global

}(this));

Do funkcji natychmiastowych nie warto przekazywać zbyt wielu parametrów, bo bardzo szybko
okaże się, że trzeba często przewijać kod do góry i na dół, by dowiedzieć się, co oznacza
która zmienna.

Wartości zwracane przez funkcje natychmiastowe


Podobnie jak każda inna funkcja, także funkcja natychmiastowa może zwrócić wartość, a ta
może zostać przypisana do zmiennej.
var result = (function () {
return 2 + 2;
}());

Funkcje natychmiastowe | 77
Ten sam rezultat można uzyskać prościej, pomijając nawiasy otaczające funkcję natychmia-
stową, ponieważ w przypadku przypisywania jej wyniku do zmiennej są one opcjonalne.
Po usunięciu zbędnych nawiasów kod wygląda następująco:
var result = function () {
return 2 + 2;
}();

Składnia jest prostsza, ale nieco myląca. Jeżeli nie zauważy się nawiasów okrągłych na końcu
funkcji, można pomyśleć, że result zawiera funkcję, którą można w dowolnym momencie
wykonać. W rzeczywistości jednak result wskazuje na wartość zwróconą przez funkcję na-
tychmiastową — w tym przypadku na liczbę 4.
Oto jeszcze jedna składnia dająca identyczny wynik:
var result = (function () {
return 2 + 2;
})();

Poprzednie przykłady jako wynik zwracały zwykłą liczbę, ale nic nie stoi na przeszkodzie,
by funkcja natychmiastowa zwróciła dowolną inną wartość, w tym również inną funkcję.
Można w ten sposób wykorzystać zakres funkcji natychmiastowej do przechowywania da-
nych dostępnych tylko i wyłącznie dla zwróconej przez nią funkcji.
W następnym przykładzie wartością zwróconą przez funkcję natychmiastową jest funkcja
przypisywana do zmiennej getResult. Funkcja ta zwraca po prostu wartość res , która
została wcześniej wyliczona i zapamiętana w domknięciu funkcji natychmiastowej.
var getResult = (function () {
var res = 2 + 2;
return function () {
return res;
};
}());

Funkcje natychmiastowe można również wykorzystać do definiowania właściwości obiek-


tów. Przypuśćmy, że musimy zdefiniować właściwość, która prawdopodobnie nigdy się nie
zmieni w trakcie życia obiektu, ale określenie jej wartości początkowej wymaga kilku dodat-
kowych zabiegów. Niezbędne zadania można otoczyć funkcją natychmiastową, a zwróconą
wartość przypisać do właściwości. Poniższy kod przedstawia przykład właśnie takiej operacji.
var o = {
message: (function () {
var who = "mnie",
what = "zadzwoń do";
return what + " " + who;
}()),
getMsg: function () {
return this.message;
}
};
// użycie
o.getMsg(); // "zadzwoń do mnie"
o.message; // "zadzwoń do mnie"

W przykładzie o.message jest właściwością tekstową, a nie funkcją, ale wymaga funkcji, by
zdefiniować swoją wartość w momencie wczytania skryptu.

78 | Rozdział 4. Funkcje
Zalety i zastosowanie
Wzorzec funkcji natychmiastowej jest stosowany powszechnie. Pozwala na wykonanie okre-
ślonych zadań bez zaśmiecania przestrzeni globalnej zmiennymi tymczasowymi. Wszystkie
zdefiniowane zmienne są lokalne względem funkcji natychmiastowej i nie wyjdą poza nią,
chyba że programista zadecyduje inaczej.

Inne często spotykane nazwy funkcji natychmiastowej to funkcja samowywołująca


się i funkcja samowykonująca się, ponieważ wykonywana jest ona tuż po jej zdefi-
niowaniu.

Przedstawiony wzorzec bardzo często stosuje się również w bookmarkletach, ponieważ


mogą być one wykonywane na dowolnej stronie i pozostawienie przestrzeni globalnej czystą
(przez niedodawanie żadnych własnych elementów) jest niezbędne.
Wzorzec pozwala również umieścić poszczególne zestawy funkcjonalności w szczelnych
modułach. Wyobraźmy sobie, że strona jest w pełni statyczna i działa prawidłowo bez jakie-
gokolwiek kodu JavaScript. W duchu progresywnego rozszerzania dodajemy do niej ele-
menty dynamiczne. Kod tej funkcjonalności (można go nazwać modułem) umieszczamy
w funkcji natychmiastowej i mamy pewność, że strona działa poprawnie z nim i bez niego.
Następnie dodajemy kolejne rozszerzenia, usuwamy je oraz włączamy lub wyłączamy
według potrzeb.
Poniższy szablon posłuży do zdefiniowania pojedynczego fragmentu funkcjonalności (nazwij-
my go module1).
// module1 zdefiniowany w module1.js
(function () {
// cały kod modułu...
}());

W podobny sposób można utworzyć inne moduły. Gdy nadejdzie czas umieszczenia witryny
w systemie produkcyjnym i pokazania jej całemu światu, sami zdecydujemy, które funkcjo-
nalności włączymy, dodając odpowiednie pliki do skryptu budującego jej kod JavaScript.

Natychmiastowa inicjalizacja obiektu


Innym sposobem ochrony przed zanieczyszczeniem przestrzeni globalnej podobnym do opi-
sanego wcześniej wzorca funkcji natychmiastowej jest wzorzec natychmiastowej inicjalizacji
obiektu. Korzysta on z obiektu zawierającego metodę init(), która zostaje wykonana tuż po
utworzeniu obiektu. Funkcja init() zajmuje się całą inicjalizacją.
Oto przykład wzorca natychmiastowej inicjalizacji obiektu:
({
// w tym miejscu mogą pojawić się standardowe ustawienia,
// na przykład stałe konfiguracyjne
maxwidth: 600,
maxheight: 400,

// można również definiować metody pomocnicze


gimmeMax: function () {
return this.maxwidth + "x" + this.maxheight;

Natychmiastowa inicjalizacja obiektu | 79


},

// inicjalizacja
init: function () {
console.log(this.gimmeMax());
// dalsze polecenia inicjalizacji...
}
}).init();

Pod względem składni wzorzec przypomina tworzenie standardowego obiektu za pomocą


literału. Literał trzeba otoczyć nawiasami okrągłymi, by poinformować interpreter języka, że
nawiasy klamrowe są literałem obiektu, a nie blokiem kodu do wykonania (na przykład kodem
pętli for). Po nawiasie zamykającym następuje natychmiastowe wykonanie metody init().
Nawiasami okrągłymi można również otoczyć całą konstrukcję łącznie z wywołaniem init().
Innymi słowy, oba poniższe zapisy zadziałają prawidłowo.
({...}).init();
({...}.init());

Zalety przedstawionego wzorca są takie same jak wzorca funkcji natychmiastowej — ochro-
na globalnej przestrzeni nazw przy jednoczesnym zapewnieniu jednorazowej inicjalizacji.
Wzorzec ten wygląda na nieco bardziej zaawansowany pod względem składniowym niż na-
pisanie kawałka kodu i otoczenie go funkcją anonimową, ale jeśli zadania inicjalizacji są zło-
żone (co nie jest rzadkością), dodatkowa struktura ułatwi analizę kodu. Przykładem mogą
być prywatne funkcje pomocnicze, które będzie można łatwo wychwycić, bo stanowią wła-
ściwości obiektu tymczasowego. We wzorcu funkcji natychmiastowej najczęściej będą one
luźno porozrzucanymi funkcjami.
Wadą tego wzorca jest to, że większość minifikatorów JavaScript nie zmniejszy rozmiaru ko-
du tak efektywnie, jak miałoby to miejsce w przypadku otoczenia go funkcją. Prywatne wła-
ściwości i metody nie zostaną zamienione na ich krótsze odpowiedniki, ponieważ dla minifi-
katora nie jest to operacja bezpieczna. W chwili obecnej jedynym minifikatorem, który potrafi
zamienić nazwy właściwości w przedstawionym wzorcu, jest Closure Compiler firmy Google.
Co istotne, czyni to tylko w trybie zaawansowanym, zamieniając wcześniejszy przykład na kod:
({d:600,c:400,a:function(){return this.d+"x"+this.c},b:function(){console.log(this.
a())}}).b();

Wzorzec służy przede wszystkim do jednorazowego wykonywania wybranych czyn-


ności, ponieważ po zakończeniu metody init() utracony zostaje dostęp do obiektu.
Aby zachować referencję do niego, wystarczy na końcu metody init() umieścić
wiersz return this;.

Usuwanie warunkowych wersji kodu


Usuwanie warunkowych wersji kodu to wzorzec optymalizacyjny polegający na wyborze
i stosowaniu różnych wersji kodu na podstawie danych znanych dopiero na etapie jego
wczytywania lub inicjalizacji. Jeśli wiem, że pewne warunki nie zmienią się na dalszym eta-
pie pracy programu, sprawdzenie ich tylko jeden raz przyspieszy jego wykonywanie. Typo-
wym przykładem jest wykrywanie funkcjonalności dostępnych w przeglądarce.

80 | Rozdział 4. Funkcje
Jeśli przykładowo wykryjemy, że przeglądarka udostępnia wbudowany obiekt XMLHttpRequest,
raczej nie istnieje ryzyko zniknięcia tego obiektu w trakcie wykonywania programu i ma-
gicznego zastąpienia go obiektem ActiveX. Ponieważ środowisko uruchomieniowe nie ulega
zmianie, nie ma potrzeby, by kod sprawdzał ten sam warunek i zawsze dochodził do takiego
samego wniosku, gdy potrzebuje utworzyć obiekty XHR.
Określanie wyliczonych stylów elementu DOM lub dołączanie funkcji obsługi zdarzeń to
kolejne przykłady sytuacji, w których można skorzystać ze wzorca usuwania warunkowych
wersji kodu. Większość programistów JavaScript przynajmniej raz w życiu tworzyła kod
pomocniczy przypisujący lub usuwający funkcje obsługi zdarzeń w sposób przedstawiony
poniżej.
// DAWNIEJ
var utils = {
addListener: function (el, type, fn) {
if (typeof window.addEventListener === 'function') {
el.addEventListener(type, fn, false);
} else if (typeof document.attachEvent === 'function') { // IE
el.attachEvent('on' + type, fn);
} else { // starsze przeglądarki
el['on' + type] = fn;
}
},
removeListener: function (el, type, fn) {
// bardzo podobny kod...
}
};

Problem polega na tym, że zaprezentowany kod nie jest efektywny. Każde wywołanie
utils.AddListener() lub utils.removeListener() wykonuje te same testy.

Usuwanie warunkowych wersji kodu pozwala wykonać test tylko raz, w trakcie wczytywa-
nia skryptu. Po sprawdzeniu, której wersji należy użyć, kod odpowiedniej wersji jest przypi-
sywany do biblioteki, a następnie jest stosowany bez dodatkowych warunków w trakcie
działania programu. Oto, jak mógłby wyglądać powyższy przykład po poprawkach:
// OBECNIE

// interfejs
var utils = {
addListener: null,
removeListener: null
};

// implementacja
if (typeof window.addEventListener === 'function') {
utils.addListener = function (el, type, fn) {
el.addEventListener(type, fn, false);
};
utils.removeListener = function (el, type, fn) {
el.removeEventListener(type, fn, false);
};
} else if (typeof document.attachEvent === 'function') { // IE
utils.addListener = function (el, type, fn) {
el.attachEvent('on' + type, fn);
};
utils.removeListener = function (el, type, fn) {
el.detachEvent('on' + type, fn);
};

Usuwanie warunkowych wersji kodu | 81


} else { // starsze przeglądarki
utils.addListener = function (el, type, fn) {
el['on' + type] = fn;
};
utils.removeListener = function (el, type, fn) {
el['on' + type] = null;
};
}

W tym miejscu warto dodać kilka słów ostrzeżenia dotyczącego wykrywania funkcji prze-
glądarek. Stosując ten wzorzec, nie zakładajmy więcej, niż jest w stanie wykonać konkretna
przeglądarka. Jeśli z kolei kod wykryje, że nie obsługuje ona window.addEventListener, nie
zakładajmy od razu, że jest to IE i nie obsługuje również obiektów XMLHttpRequest, choć było
to prawdą w starszych jej wersjach. Czasem można bezpiecznie przyjąć, że niektóre funkcjo-
nalności są dostępne jednocześnie — na przykład addEventListener i removeEventListener
— ale stanowi to raczej wyjątek niż regułę. Najlepszym rozwiązaniem jest osobne testowanie
każdej funkcjonalności w trakcie wczytywania strony i usuwanie warunkowych wersji kodu.

Właściwości funkcji — wzorzec zapamiętywania


Funkcje są obiektami, więc mogą mieć właściwości. W zasadzie to nawet domyślnie posiadają
metody i właściwości. Przykładowo, każda funkcja, niezależnie od sposobu jej utworzenia,
automatycznie otrzymuje właściwość length informującą o oczekiwanej liczbie argumentów:
function func(a, b, c) {}
console.log(func.length); // 3

Własne właściwości można dodawać do funkcji w dowolnym momencie. Jednym ze sposo-


bów ich użycia jest zapamiętywanie wyników (zwracanych wartości), by przy następnym
wywołaniu z tymi samymi argumentami funkcja nie musiała przeprowadzać złożonych obliczeń.
To tak zwany wzorzec zapamiętywania.
W poniższym przykładzie funkcja myFunc tworzy właściwość cache dostępną jako myFunc.cache.
Właściwość cache to obiekt (tablica asocjacyjna), dla którego parametr param przekazany do
funkcji służy jako klucz. Wynik wykonania właściwych działań to wartość przypisywana
kluczowi. Wynikiem może być dowolnie złożona struktura danych.
var myFunc = function (param) {
if (!myFunc.cache[param]) {
var result = {};
// kosztowna operacja
myFunc.cache[param] = result;
}
return myFunc.cache[param];
};

// obiekt służący do zapamiętywania wyników


myFunc.cache = {};

Przedstawiony kod zakłada, że funkcja przyjmuje tylko jeden argument (param), który jest
typu prostego (na przykład tekst). Jeśli istnieje więcej parametrów lub są one bardziej złożo-
ne, uniwersalnym rozwiązaniem będzie ich serializacja. Parametry funkcji można zserializo-
wać do formatu JSON, a następnie wykorzystać jako klucze w obiekcie cache.

82 | Rozdział 4. Funkcje
var myFunc = function () {
var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)),
result;

if (!myFunc.cache[cachekey]) {
result = {};
// kosztowna operacja
myFunc.cache[cachekey] = result;
}
return myFunc.cache[cachekey];
};

// obiekt służący do zapamiętywania wyników


myFunc.cache = {};

Pamiętajmy, że serializacja obiektów powoduje tracenie przez nie „tożsamości”. Jeśli dwa
różne obiekty mają takie same właściwości, oba będą współdzieliły ten sam wpis w obiekcie
zapamiętanych wyników.
Innym sposobem napisania poprzedniej funkcji jest użycie arguments.callee, co pozwala
uniknąć wpisywania na sztywno nazwy funkcji. Niestety, arguments.callee nie jest dostępne
w trybie ścisłym w ECMAScript 5.
var myFunc = function (param) {

var f = arguments.callee,
result;

if (!f.cache[param]) {
result = {};
// kosztowna operacja
f.cache[param] = result;
}
return f.cache[param];
};

// obiekt służący do zapamiętywania wyników


myFunc.cache = {};

Obiekty konfiguracyjne
Wzorzec obiektu konfiguracyjnego to sposób na zapewnienie czystszego interfejsu programi-
stycznego, szczególnie jeśli tworzy się bibliotekę lub inny kod, który będzie wykorzystywany
przez inne programy.
Wymagania dotyczące tworzonego oprogramowania ulegają częstym zmianom w trakcie
prac nad kodem. Zdarza się, że rozpoczyna się jego pisanie z myślą o jednej funkcjonalności,
ale z czasem dochodzą nowe.
Wyobraźmy sobie, że piszemy funkcję o nazwie addPerson(), która przyjmuje imię i nazwisko,
a następnie dodaje osobę do listy.
function addPerson(first, last) {...}

Nieco później okazuje się, że data urodzenia również musi zostać zapamiętana, a dane o płci
i adresie są opcjonalne. Funkcja ulega modyfikacji polegającej na dodaniu nowych parametrów
(parametry opcjonalne przezornie umieszczane są na końcu listy).
function addPerson(first, last, dob, gender, address) {...}

Obiekty konfiguracyjne | 83
W tym momencie sygnatura funkcji staje się nieco za długa. Po kilku dniach okazuje się, że
trzeba jeszcze dodać parametr nazwy użytkownika i że jest on wymagany, a nie opcjonalny.
Od tego momentu kod wykorzystujący funkcję musi przekazywać do niej nawet parametry
opcjonalne. Programista musi niezwykle uważać, by przypadkowo nie zmienić kolejności
parametrów.
addPerson("Bruce", "Wayne", new Date(), null, null, "batman");

Przekazywanie dużej liczby parametrów nie jest wygodne. Lepszym rozwiązaniem jest za-
stąpienie ich wszystkich tylko jednym parametrem — obiektem z parami nazwa-wartość.
Nadajmy mu nazwę conf.
addPerson(conf);

Dzięki temu programista korzystający z funkcji będzie mógł napisać:


var conf = {
username: "batman",
first: "Bruce",
last: "Wayne"
};
addPerson(conf);

Obiekty konfiguracyjne mają kilka zalet:


• nie trzeba pamiętać parametrów i ich kolejności;
• można bezpiecznie pominąć parametry opcjonalne;
• są bardziej klarowne;
• łatwiej jest dodać lub usunąć parametry.

Mają także parę wad:


• trzeba pamiętać nazwy parametrów;
• nazw właściwości nie da się zminimalizować.

Przedstawiony wzorzec bywa szczególnie użyteczny, gdy funkcja tworzy elementy DOM lub
ustawia właściwości CSS, ponieważ elementy i style mają najczęściej sporą liczbę opcjonal-
nych atrybutów i właściwości.

Rozwijanie funkcji
Pozostała część rozdziału omawia rozwijanie funkcji i częściowe aplikacje funkcji. Zanim
jednak zagłębimy się w ten temat, zastanówmy się, co tak naprawdę oznacza termin aplikacja
funkcji.

Aplikacja funkcji
W niektórych wyłącznie funkcyjnych językach programowania nie mówi się o wywołaniu
funkcji, ale o jej aplikacji (zastosowaniu). W języku JavaScript mamy do czynienia z tym
samym — możemy zaaplikować funkcję, używając metody Function.prototype.apply(),
ponieważ funkcje w JavaScripcie to tak naprawdę posiadające metody obiekty.

84 | Rozdział 4. Funkcje
Oto przykład zastosowania (aplikacji) funkcji:
// definicja funkcji
var sayHi = function (who) {
return "Witaj" + (who ? ", " + who : "") + "!";
};

// wywołanie funkcji
sayHi(); // "Witaj"
sayHi('świecie'); // "Witaj, świecie!"

// aplikacja funkcji
sayHi.apply(null, ["witaj"]); // "Witaj, witaj!"

Jak można zauważyć, zarówno wywołanie funkcji, jak i jej aplikacja dają taki sam efekt.
Metoda apply() przyjmuje dwa parametry: pierwszym jest obiekt, który wewnątrz funkcji
będzie dostępny pod zmienną this, a drugim lista argumentów, która wewnątrz funkcji
będzie dostępna pod zmienną arguments. Jeśli pierwszy parametr będzie miał wartość null,
this w funkcji będzie wskazywało na obiekt globalny, czyli uzyska się sytuację taką jak
w przypadku wywołania funkcji nieprzypisanej do obiektu.
Jeśli funkcja jest metodą obiektu, wartość null nie jest przekazywana (jak to miało miejsce
w powyższym przykładzie). W takiej sytuacji pierwszym argumentem metody apply() jest
obiekt.
var alien = {
sayHi: function (who) {
return "Witaj" + (who ? ", " + who : "") + "!";
}
};

alien.sayHi('świecie'); // "Witaj, świecie!"


sayHi.apply(alien, ["człowieku"]); // "Witaj, człowieku!"

W powyższym kodzie this wewnątrz funkcji sayHi() wskazuje na obiekt alien. W przy-
kładzie poprzednim this wskazywało na obiekt globalny.
Jak pokazują dwa zaprezentowane przykłady, wywołanie funkcji to nic innego jak tylko do-
datek składniowy, który w zasadzie zawsze można zamienić na aplikację funkcji. Poza meto-
dą apply() istnieje jeszcze metoda call() obiektu Function.prototype, ale to również tylko
dodatek składniowy do apply(). Czasem warto skorzystać z wersji alternatywnej — gdy
funkcja przyjmuje tylko jeden parametr, nie ma potrzeby tworzyć dla niego obiektu tablicy.
// drugie rozwiązanie jest wydajniejsze; nie jest tworzona tablica
sayHi.apply(alien, ["człowieku"]); // "Witaj, człowieku!"
sayHi.call(alien, "człowieku"); // "Witaj, człowieku!"

Aplikacja częściowa
Skoro wiadomo już, że wywołanie funkcji to tak naprawdę aplikacja zestawu argumentów
dla funkcji, pojawia się pytanie, czy można przekazać jedynie część argumentów. W zasadzie
jest to bardzo podobne do podejścia, które zastosowalibyśmy, gdybyśmy mieli do czynienia
z funkcją matematyczną.
Przypuśćmy, że istnieje funkcja add(), która dodaje do siebie dwie wartości: x i y. Poniższy
kod pokazuje, jak wyglądałaby sytuacja, gdyby x było równe 5, a y równe 4.

Rozwijanie funkcji | 85
// prezentowane tylko w celach poglądowych
// to nie jest poprawny kod JavaScript

// mamy funkcję
function add(x, y) {
return x + y;
}

// i znamy jej argumenty


add(5, 4);

// krok pierwszy: zastępujemy pierwszy argument


function add(5, y) {
return 5 + y;
}

// krok drugi: zastępujemy drugi argument


function add(5, 4) {
return 5 + 4;
}

W przedstawionym przykładzie kroki 1. i 2. nie są poprawnym kodem JavaScript, ale ręcznie


właśnie w taki sposób rozwiązalibyśmy problem. Bierzemy wartość pierwszego argumentu
i zastępujemy nią wszystkie wystąpienia zmiennej x. Następnie powtarzamy ten sam zabieg
dla drugiego argumentu i zmiennej y.
Krok 1. w zaprezentowanym przykładzie można by nazwać aplikacją częściową, ponieważ
zaaplikowaliśmy jedynie pierwszy argument. Po tej operacji nie uzyskujemy wyniku (roz-
wiązania), ale inną funkcję.
Następny fragment przedstawia użycie wyimaginowanej metody partialApply().
var add = function (x, y) {
return x + y;
};

// aplikacja pełna
add.apply(null, [5, 4]); // 9

// aplikacja częściowa
var newadd = add.partialApply(null, [5]);

// aplikacja argumentu dla nowej funkcji


newadd.apply(null, [4]); // 9

Jak można zauważyć, aplikacja częściowa udostępnia inną funkcję, którą następnie można
wywołać z innymi argumentami. Przedstawiony kod jest tak naprawdę równoważny zapi-
sowi add(5)(4), ponieważ add(5) zwraca funkcję, którą można wywołać, używając (4).
Można więc potraktować przedstawiony zapis jako dodatek składniowy tożsamy z zapisem
add(5, 4).

A teraz powrót na ziemię: nie ma metody partialApply() i funkcje w JavaScripcie domyśl-


nie nie zachowują się w ten sposób. Nic jednak nie stoi na przeszkodzie, by tak zrobić —
JavaScript jest wystarczająco elastyczny.
Proces tworzenia funkcji obsługującej aplikację częściową nazywamy rozwijaniem funkcji.

86 | Rozdział 4. Funkcje
Rozwijanie funkcji
Rozwijanie funkcji w języku angielskim nosi nazwę currying, która jednak nie ma nic wspól-
nego z przyprawą — jest hołdem złożonym matematykowi Haskellowi Curry’emu (jego
imieniem został również nazwany język programowania Haskell). Rozwijanie funkcji to
przekształcenie, któremu podlega funkcja. W zasadzie alternatywną nazwą mogłaby również być
schönfinkelizacja — bazowałaby ona na nazwisku innego matematyka Mosesa Schönfinkela,
oryginalnego twórcy transformacji.
W jaki sposób rozwijamy funkcję? Inne języki funkcyjne mogą mieć tę funkcjonalność wbu-
dowaną, tak że wszystkie ich funkcje domyślnie obsługują rozwijanie. W języku JavaScript
możemy zmodyfikować funkcję add(), doprowadzając ją do wersji rozwijalnej, która obsłuży
aplikację częściową.
Przeanalizujmy następujący przykład:
// funkcja add() po rozwinięciu
// obsługuje częściową listę argumentów
function add(x, y) {
var oldx = x, oldy = y;
if (typeof oldy === "undefined") { // aplikacja częściowa
return function (newy) {
return oldx + newy;
};
}

// aplikacja pełna
return x + y;
}
// test
typeof add(5); // "function"
add(3)(4); // 7

// utworzenie i zapamiętanie nowej funkcji


var add2000 = add(2000);
add2000(10); // 2010

W przedstawionym przykładzie pierwsze wywołanie add() tworzy domknięcie wokół funkcji


wewnętrznej zwracanej jako wynik. Domknięcie zapamiętuje oryginalne wartości x i y w pry-
watnych zmiennych oldx i oldy. Pierwsza z nich, oldx, jest wykorzystywana w momencie
wykonania funkcji wewnętrznej. Jeśli nie następuje aplikacja częściowa, funkcja po prostu
przechodzi do właściwych działań i dodaje obie wartości. Ta implementacja add() jest w ce-
lach ilustracyjnych nieco bardziej rozbudowana, niż mogłaby być. Skróconą wersję przed-
stawia następny przykład, w którym nie ma oldx i oldy, ponieważ oryginalne x niejawnie
trafia do domknięcia. Co więcej, ponownie korzystamy z y jako zmiennej lokalnej, zamiast
tworzyć nową zmienną newy, jak to miało miejsce w poprzednim przykładzie.
// rozwijanie funkcji
// add przyjmuje częściowo określoną listę argumentów
function add(x, y) {
if (typeof y === "undefined") { // aplikacja częściowa
return function (y) {
return x + y;
};
}
// aplikacja pełna
return x + y;
}

Rozwijanie funkcji | 87
W zaprezentowanych przykładach sama funkcja add() zajmowała się zapewnieniem aplikacji
częściowej. Czy można uzyskać ten sam efekt w sposób bardziej ogólny? Innymi słowy, czy
możemy przekształcić dowolną funkcję w nową, która przyjmuje tylko część parametrów?
Następny przykład przedstawia funkcję ogólnego zastosowania o nazwie schonfinkelize(),
która zapewnia generyczną aplikację częściową. Użyliśmy nazwy schonfinkelize(), bo
z jednej strony nie jest łatwa do wymówienia, a z drugiej brzmi jak czasownik (nazwa „curry”
byłaby zbyt dwuznaczna). Potrzebujemy czasownika, bo funkcja dokonuje transformacji
innej funkcji.
Oto funkcja dodająca do dowolnej funkcji aplikację częściową:
function schonfinkelize(fn) {
var slice = Array.prototype.slice,
stored_args = slice.call(arguments, 1);
return function () {
var new_args = slice.call(arguments),
args = stored_args.concat(new_args);
return fn.apply(null, args);
};
}

Funkcja schonfinkelize() jest prawdopodobnie nieco bardziej złożona, niż mogłaby być,
ale tylko dlatego, że arguments nie jest w języku JavaScript prawdziwą tablicą. Pożyczenie
metody slice() z Array.prototype pomaga zamienić arguments na tablicę i w tym cha-
rakterze z niej korzystać. Pierwsze wywołanie schonfinkelize() zapamiętuje w zmiennej
prywatnej referencję do metody slice() (o nazwie slice), a także wszystkie przekazane
(w stored_args) argumenty poza pierwszym (bo jest nim funkcja podlegająca aplikacji czę-
ściowej). Następnie schonfinkelize() zwraca funkcję. Gdy utworzona funkcja zostanie wy-
konana, będzie miała dostęp do informacji zapamiętanych wcześniej w zmiennych prywat-
nych (stored_args i slice). Nowa funkcja musi połączyć stare parametry ze stored_args
z nowymi new_args, a następnie zaaplikować je dla oryginalnej funkcji fn (także dostępnej
prywatnie dzięki domknięciu).
Uzbrojeni w ogólny mechanizm aplikacji częściowej wykonajmy kilka testów.
// zwykła funkcja
function add(x, y) {
return x + y;
}

// aplikacja częściowa zwracająca nową funkcję


var newadd = schonfinkelize(add, 5);
newadd(4); // 9

// inne rozwiązanie — bezpośrednie wywołanie nowej funkcji


schonfinkelize(add, 6)(7); // 13

Funkcja przekształcająca nie jest ograniczona do pojedynczych parametrów lub pojedynczej


aplikacji częściowej. Oto kilka dodatkowych przykładów:
// zwykła funkcja
function add(a, b, c, d, e) {
return a + b + c + d + e;
}

// działa poprawnie z dowolną liczbą argumentów


schonfinkelize(add, 1, 2, 3)(5, 5); // 16

88 | Rozdział 4. Funkcje
// dwustopniowa aplikacja częściowa
var addOne = schonfinkelize(add, 1);
addOne(10, 10, 10, 10); // 41
var addSix = schonfinkelize(addOne, 2, 3);
addSix(5, 5); // 16

Kiedy używać aplikacji częściowej


Jeżeli tę samą funkcję wywołuje się wielokrotnie w większości z tymi samymi parametrami,
jest ona prawdopodobnie dobrym kandydatem do aplikacji częściowej. Nową funkcję można
utworzyć dynamicznie, aplikując częściowo niektóre z parametrów. Powstała funkcja zapa-
mięta powtarzane parametry (by nie trzeba ich było przekazywać każdorazowo) i wykorzy-
sta je do zbudowania pełnej listy argumentów wymaganych przez oryginalną funkcję.

Podsumowanie
W języku JavaScript pełna wiedza na temat funkcji i ich zastosowania jest niezbędna. Niniej-
szy rozdział omawia podstawy i terminy związane z funkcjami. Najważniejszym jest, by pa-
miętać o dwóch istotnych cechach funkcji w języku JavaScript:
1. Funkcje są pełnoprawnymi obiektami, więc mogą być przekazywane jako wartości,
a nawet posiadać własne właściwości i metody.
2. Funkcje, w odróżnieniu od nawiasów klamrowych, zapewniają lokalny zakres zmiennych.
Warto także pamiętać o tym, że deklaracje zmiennych lokalnych zostają przeniesione na sam
początek zakresu lokalnego.
Istnieją trzy wersje składni do tworzenia funkcji:
1. Nazwane wyrażenia funkcyjne.
2. Wyrażenia funkcyjne (takie same jak powyższe, ale bez nazwy) nazywane również
funkcjami anonimowymi.
3. Deklaracje funkcji przypominające składniowo konstrukcje znane z innych języków.
Po przedstawieniu podstaw zajęliśmy się kilkoma użytecznymi wzorcami, które można by po-
dzielić na kilka kategorii.
1. Wzorce API, które pomagają uzyskać lepszy i czystszy interfejs dla funkcji. Wzorce te to:
• wzorzec wywołania zwrotnego — przekazanie funkcji jako argumentu;
• obiekty konfiguracyjne — utrzymywanie niewielkiej liczby parametrów funkcji;
• zwracanie funkcji — wartością zwracaną przez funkcję jest inna funkcja;
• rozwijanie funkcji — nowe funkcje powstają na podstawie istniejących z częściową
aplikacją niektórych parametrów.
2. Wzorce inicjalizacyjne, które pomagają przeprowadzić inicjalizację i wstępną konfigurację
(bardzo częsta sytuacja w przypadku stron internetowych i aplikacji) w czystszy i bar-
dziej ustrukturyzowany sposób, bez zaśmiecania globalnej przestrzeni nazw zmiennymi
tymczasowymi. Wzorce te to:
• funkcja natychmiastowa — wykonywana tuż po zdefiniowaniu;

Podsumowanie | 89
• natychmiastowa inicjalizacja obiektu — zadania inicjalizacyjne umieszczone w obiek-
cie anonimowym wraz z metodą, która jest wywoływana tuż po powstaniu obiektu;
• usuwanie warunkowych wersji kodu — operacje warunkowe wykonywane tylko raz
na etapie inicjalizacji zamiast wielokrotnie w trakcie życia aplikacji.
3. Wzorce optymalizacyjne, które poprawiają wydajność kodu. Wzorce te to:
• zapamiętywanie wyników — wykorzystanie właściwości funkcji do zapamiętania wy-
ników, by nie trzeba ich było wyliczać wielokrotnie;
• samodefiniujące się funkcje — nadpisywanie treści funkcji nowymi wersjami, by drugie
i kolejne wywołania wykonywały mniej zadań.

90 | Rozdział 4. Funkcje
ROZDZIAŁ 5.

Wzorce tworzenia obiektów

Tworzenie obiektów w języku JavaScript jest proste — albo stosuje się literały obiektów, albo
funkcje konstruujące. W tym rozdziale przyjrzymy się bardziej zaawansowanym technikom
tworzenia obiektów.
Język JavaScript jest bardzo prosty i najczęściej nie posiada specjalnej składni dla pewnych
funkcji, którą można znaleźć w wielu innych językach programowania, na przykład dotyczącej
przestrzeni nazw, modułów, pakietów, właściwości prywatnych i składowych statycznych.
W tym rozdziale przyjrzymy się implementacjom związanych z nimi wzorców i ich alterna-
tywnymi wersjami lub po prostu spojrzymy na te funkcje w inny sposób.
Zajmiemy się wzorcami przestrzeni nazw, deklaracji zależności, modułami i tak zwanymi
piaskownicami. Wszystkie one pomagają uzyskać lepszą strukturę kodu i zminimalizować
efekt zaśmiecania globalnej przestrzeni nazw własnymi zmiennymi. Innymi omawianymi
tematami będą: składowe prywatne i uprzywilejowane, składowe statyczne i statyczno-prywatne,
stałe obiektów, tworzenie łańcuchów wywołań i sposób definiowania konstruktorów wzo-
rowany na klasach.

Wzorzec przestrzeni nazw


Przestrzenie nazw pomagają redukować liczbę zmiennych globalnych wymaganych przez
programy, a jednocześnie zapobiegają zbyt częstym kolizjom nazw i stosowaniu dla nich
długich przedrostków.
JavaScript nie posiada wbudowanej obsługi przestrzeni nazw na poziomie składniowym, ale
tę funkcjonalność dosyć łatwo da się uzyskać. Zamiast umieszczać w przestrzeni globalnej
mnóstwo funkcji, obiektów i innych zmiennych, można utworzyć jeden (idealnie tylko jeden)
obiekt globalny dla aplikacji lub biblioteki. Następnie całą funkcjonalność umieszcza się wła-
śnie w nim.
Rozważmy następujący przykład:
// DAWNIEJ: 5 zmiennych globalnych
// uwaga: antywzorzec

// konstruktory
function Parent() {}
function Child() {}

91
// zmienna
var some_var = 1;

// pewne obiekty
var module1 = {};
module1.data = {a: 1, b: 2};
var module2 = {};

Taki kod można poddać refaktoryzacji przez utworzenie pojedynczego obiektu globalnego,
na przykład o nazwie MYAPP, a następnie zmianie wszystkich funkcji i zmiennych w taki spo-
sób, by stały się jego właściwościami.
// OBECNIE: 1 zmienna globalna

// obiekt globalny
var MYAPP = {};

// konstruktory
MYAPP.Parent = function () {};
MYAPP.Child = function () {};

// zmienna
MYAPP.some_var = 1;

// kontener na obiekty
MYAPP.modules = {};

// zagnieżdżone obiekty
MYAPP.modules.module1 = {};
MYAPP.modules.module1.data = {a: 1, b: 2};
MYAPP.modules.module2 = {};

Jako nazwę obiektu globalnego można wybrać nazwę aplikacji lub biblioteki albo nazwę do-
meny lub firmy. Często programiści piszą nazwę takiej zmiennej globalnej wielkimi literami,
by wyróżniała się w kodzie. Warto jednak pamiętać, że taka sama konwencja jest również
stosowana dla stałych.
Taki wzorzec to dobry sposób na jednoznaczne przydzielenie tworzonego kodu do jednej
przestrzeni nazw i uniknięcie kolizji nazw nie tylko we własnym kodzie, ale również z ko-
dem innych firm, który znajdzie się na tej samej stronie z powodu zastosowania dodatko-
wych bibliotek lub widgetów. Korzystanie z tego wzorca jest wysoce zalecane i może on być
stosowany w wielu sytuacjach, ale ma również kilka wad:
• Nieco więcej pisania — poprzedzanie każdej zmiennej i funkcji nazwą przestrzeni zwiększa
ilość kodu do pobrania.
• Tylko jedna globalna instancja obiektu powoduje, że dowolny kod może ją zmodyfikować,
a cała reszta kodu będzie od razu widziała tę zmianę.
• Im więcej zagnieżdżeń nazw, tym wolniejsze ich wyszukiwanie.

Wzorzec piaskownicy omówiony w dalszej części rozdziału ma za zadanie wyeliminować


wymienione wady.

Funkcja przestrzeni nazw ogólnego stosowania


Gdy złożoność programu wzrasta i niektóre jego części trafiają do osobnych plików wczyty-
wanych warunkowo, nie można dłużej bezpiecznie zakładać, że określony kod jest pierwszym,
który definiuje określoną przestrzeń nazw. Pewne właściwości dodawane do przestrzeni mogą

92 | Rozdział 5. Wzorce tworzenia obiektów


już istnieć, więc ryzykuje się ich nadpisanie. Przed dodaniem właściwości lub utworzeniem
przestrzeni nazw lepiej jest sprawdzić, czy ta już istnieje, co przedstawia poniższy przykład.
// niebezpieczne
var MYAPP = {};
// lepiej
if (typeof MYAPP === "undefined") {
var MYAPP = {};
}
// lub krócej
var MYAPP = MYAPP || {};

Nietrudno zauważyć, że te wszystkie sprawdzenia mogą bardzo szybko doprowadzić do pisania


sporej ilości bardzo podobnego kodu. Definiując MYAPP.modules.module2, najprawdopodobniej
trzeba będzie wykonać trzy sprawdzenia, po jednym dla każdej właściwości lub obiektu. Właśnie
z tego powodu lepiej korzystać z funkcji pomocniczej zajmującej się szczegółami dotyczącymi
przestrzeni nazw. Nadajmy tej funkcji nazwę namespace() i użyjmy jej w następujący sposób:
// wykorzystanie funkcji do tworzenia przestrzeni nazw
MYAPP.namespace('MYAPP.modules.module2');

// równoważne kodowi:
// var MYAPP = {
// modules: {
// module2: {}
// }
// };

Następny przykładowy kod ilustruje jedną z możliwych implementacji funkcji do tworzenia


przestrzeni nazw. Implementacja ta jest niedestrukcyjna, co oznacza, że jeśli przestrzeń nazw
już istnieje, nie zostanie utworzona ponownie.
var MYAPP = MYAPP || {};

MYAPP.namespace = function (ns_string) {


var parts = ns_string.split('.'),
parent = MYAPP,
i;

// pominięcie wspólnej części początkowej


if (parts[0] === "MYAPP") {
parts = parts.slice(1);
}

for (i = 0; i < parts.length; i += 1) {


// utworzenie właściwości, jeśli ta nie istnieje
if (typeof parent[parts[i]] === "undefined") {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
};

Przedstawiona implementacja umożliwia wykonanie wszystkich poniższych operacji.


// przypisanie zwróconej wartości do lokalnej zmiennej
var module2 = MYAPP.namespace('MYAPP.modules.module2');
module2 === MYAPP.modules.module2; // true

// pominięcie początkowego MYAPP


MYAPP.namespace('modules.module51');

// bardzo długa przestrzeń nazw


MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');

Wzorzec przestrzeni nazw | 93


Rysunek 5.1 przedstawia wygląd przestrzeni nazw po jej wyświetleniu w narzędziu Firebug.

Rysunek 5.1. Przestrzeń nazw MYAPP w narzędziu Firebug

Deklarowanie zależności
Biblioteki JavaScript są często modułowe i stosują przestrzenie nazw, co umożliwia wczyty-
wanie tylko niezbędnych modułów. Przykładowo, w bibliotece YUI2 istnieje globalna zmienna
YAHOO, która służy jako przestrzeń nazw. Poszczególne moduły — na przykład YAHOO.util.Dom
(moduł obsługi DOM) i YAHOO.util.Event (moduł obsługi zdarzeń) — stanowią właściwości
tego globalnego obiektu.
Na początku pliku lub funkcji warto wskazać moduły, które są niezbędne do działania two-
rzonego kodu. Deklaracja wymaga jedynie utworzenia zmiennej lokalnej i przypisania jej po-
żądanego modułu.
var myFunction = function () {
// zależności
var event = YAHOO.util.Event,
dom = YAHOO.util.Dom;

// wykorzystanie zmiennych dom i event


// w pozostałym kodzie funkcji...
};

To bardzo prosty wzorzec mający przy okazji kilka dodatkowych zalet:


• Jawna deklaracja zależności informuje użytkowników kodu, że do poprawnego działania
wymaga on dołączenia do niego plików zawierających określone funkcjonalności.
• Deklarowanie zależności na początku funkcji ułatwia ich odnalezienie i ewentualną
modyfikację.
• Korzystanie ze zmiennych lokalnych (takich jak dom) jest zawsze szybsze niż używanie
zmiennych globalnych (takich jak YAHOO), a nawet jeszcze szybsze, jeśli trzeba uzyskać
dostęp do właściwości zmiennych globalnych (na przykład YAHOO.util.Dom). Uzyskuje
się więc większą wydajność, bo stosując przedstawiony wzorzec deklaracji zależności,
wyszukiwanie globalnej zmiennej przeprowadza się tylko jeden raz na całe wykonanie
funkcji. Wszystkie następne użycia dotyczą zmiennej lokalnej, która jest znacznie szybsza.

94 | Rozdział 5. Wzorce tworzenia obiektów


• Zaawansowane narzędzia do minifikacji kodu (takie jak YUI Compressor lub Google
Closure) zmienią nazwy zmiennych lokalnych (na przykład zmienna event zostanie za-
mieniona na jednoznakową zmienną A), co przyczyni się do zmniejszenia wynikowego
kodu. W przypadku zmiennych globalnych taka operacja nie byłaby możliwa, bo nie można
przeprowadzić jej w sposób bezpieczny.
Poniższy fragment stanowi ilustrację wpływu wzorca deklaracji zależności na proces minifi-
kacji kodu. Choć funkcja test2() stosująca wzorzec wydaje się być początkowo dłuższa, bo
wymaga większej liczby wierszy kodu i dodatkowej zmiennej, w rzeczywistości jej zastoso-
wanie owocuje uzyskaniem krótszego kodu po minifikacji, a więc mniejszą ilością danych do
pobrania przez użytkownika.
function test1() {
alert(MYAPP.modules.m1);
alert(MYAPP.modules.m2);
alert(MYAPP.modules.m51);
}

/*
funkcja test1 po minifikacji:
alert(MYAPP.modules.m1);alert(MYAPP.modules.m2);alert(MYAPP.modules.m51)
*/

function test2() {
var modules = MYAPP.modules;
alert(modules.m1);
alert(modules.m2);
alert(modules.m51);
}

/*
funkcja test2 po minifikacji:
var a=MYAPP.modules;alert(a.m1);alert(a.m2);alert(a.m51)
*/

Metody i właściwości prywatne


Język JavaScript w odróżnieniu od takich języków jak Java nie posiada wbudowanej składni
pozwalającej określić, czy właściwość lub metoda jest prywatna, chroniona, czy publiczna.
Wszystkie składowe obiektów są zawsze dostępne.
var myobj = {
myprop: 1,
getProp: function () {
return this.myprop;
}
};
console.log(myobj.myprop); // myprop jest dostępna publicznie
console.log(myobj.getProp()); // getProp() również jest dostępna publicznie

Sytuacja wygląda identycznie, gdy do tworzenia obiektów wykorzystuje się funkcje konstru-
ujące — wszystkie składowe nadal są publiczne.
function Gadget() {
this.name = 'iPod';
this.stretch = function () {
return 'iPad';
};

Metody i właściwości prywatne | 95


}

var toy = new Gadget();


console.log(toy.name); // name jest dostępna publicznie
console.log(toy.stretch()); // stretch() również jest dostępna publicznie

Składowe prywatne
Choć język nie zapewnia specjalnej składni dla składowych prywatnych, można je zasymu-
lować za pomocą domknięcia. Funkcja konstruująca tworzy domknięcie i żadna zmienna
zadeklarowana jako jego część nie będzie dostępna poza konstruktorem. Z drugiej strony
zmienne prywatne są dostępne dla metod publicznych, czyli metod zdefiniowanych w kon-
struktorze i udostępnianych jako część zwróconego obiektu. Prześledźmy przykład, w którym
name jest zmienną prywatną niedostępną poza konstruktorem.
function Gadget() {
// zmienna prywatna
var name = 'iPod';
// funkcja publiczna
this.getName = function () {
return name;
};
}
var toy = new Gadget();

// name jest niezdefiniowane, bo jest zmienną prywatną


console.log(toy.name); // undefined
// metoda publiczna ma dostęp do name
console.log(toy.getName()); // "iPod"

Łatwo zauważyć, że uzyskanie prywatności w języku JavaScript nie jest trudne. Wystarczy
otoczyć dane, które mają pozostać prywatne, funkcją, by mieć pewność, że są dla tej funkcji
zmiennymi lokalnymi i nie wyciekają na zewnątrz.

Metody uprzywilejowane
Tak zwane metody uprzywilejowane nie wymagają stosowania żadnej dodatkowej składni
— to po prostu nazwa stosowana dla metod publicznych, które mają dostęp do zmiennych
prywatnych (więc mają większe przywileje).
W poprzednim przykładzie getName() jest metodą uprzywilejowaną, ponieważ ma szcze-
gólną własność — ma dostęp do zmiennej prywatnej name.

Problemy z prywatnością
Istnieją pewne szczególne sytuacje, które mogą zachwiać prywatnością:
• Niektóre wcześniejsze wersje przeglądarki Firefox dopuszczały przekazanie do metody
eval() drugiego parametru, który określał obiekt kontekstu. Dawało to możliwość prze-
ślizgnięcia się do zakresu prywatnego funkcji. Podobnie, właściwość __parent__ inter-
pretera Mozilla Rhino zapewnia dostęp do zakresu lokalnego. Na szczęście te przypadki
szczególne nie dotyczą powszechnie stosowanych obecnie przeglądarek.

96 | Rozdział 5. Wzorce tworzenia obiektów


• Jeśli zawartość zmiennej prywatnej zostanie zwrócona przez metodę uprzywilejowaną
bezpośrednio i zmienna ta jest tablicą lub obiektem, zewnętrzny kod będzie mógł ją
zmodyfikować jako przekazaną przez referencję.
Przyjrzyjmy się nieco dokładniej drugiej z wymienionych sytuacji. Poniższa implementacja
obiektu Gadget wygląda niewinnie.
function Gadget() {
// zmienna prywatna
var specs = {
screen_width: 320,
screen_height: 480,
color: "white"
};

// funkcja publiczna
this.getSpecs = function () {
return specs;
};
}

Problemem jest fakt zwracania przez getSpec() referencji do obiektu specs. Dzięki temu
użytkownik obiektu Gadget może zmodyfikować ten ukryty i teoretycznie prywatny obiekt.
var toy = new Gadget(),
specs = toy.getSpecs();
specs.color = "black";
specs.price = "bezpłatny";

console.dir(toy.getSpecs());

Wynik wykonania kodu w konsoli narzędzia Firebug przeglądarki Firefox przedstawia rysunek 5.2.

Rysunek 5.2. Zmienna prywatna została zmodyfikowana


Rozwiązaniem zapobiegającym temu nieoczekiwanemu zachowaniu jest ostrożne obchodze-
nie się z takimi zmiennymi i nieprzekazywanie prywatnych obiektów oraz tablic w sposób
bezpośredni. Jednym ze sposobów jest zmodyfikowanie metody getSpecs() w taki sposób,
by zwracała nowy obiekt tylko z tymi danymi, które są niezbędne dla wywołującego. To tak
zwana zasada najmniejszego przywileju, która głosi, że nigdy nie daje się innym obiektom
więcej, niż potrzebują. Oznacza to, że jeśli kod wykorzystujący Gadget zainteresowany jest
jedynie informacją o wymiarach, powinien otrzymać tylko i wyłącznie wymiary. Zamiast
więc dawać wszystko, lepiej utworzyć metodę getDimensions(), która zwróci nowy obiekt
zawierający jedynie wysokość i szerokość. Co więcej, może się okazać, że w ogóle nie trzeba
implementować getSpecs().
Gdy trzeba przekazać wszystkie dane, można skorzystać z innego rozwiązania i utworzyć
kopię obiektu specs za pomocą ogólnej funkcji klonowania obiektów. Dwie takie funkcje
znajdują się w następnym rozdziale. Pierwsza z nich nosi nazwę extend() i wykonuje płytką
kopię danego obiektu (tworzy klony tylko właściwości pierwszego poziomu). Druga nosi na-
zwę extendDeep() i wykonuje kopię głęboką, czyli rekurencyjnie powiela wszystkie właści-
wości i ich zagnieżdżenia.

Metody i właściwości prywatne | 97


Literały obiektów a prywatność
Do tej pory zajmowaliśmy się jedynie prywatnością dotyczącą konstruktorów. Czy można
uzyskać podobną prywatność, gdy obiekty tworzy się za pomocą literałów? Czy w takiej
sytuacji mogą w ogóle pojawić się składowe prywatne?
Jak już wcześniej wskazano, wszystko, co trzeba zrobić, to otoczyć prywatne dane funkcją.
W tym przypadku literał obiektu należy otoczyć funkcją anonimową wywoływaną natych-
miast po zadeklarowaniu. Oto przykład:
var myobj; // to będzie obiekt
(function () {
// składowa prywatna
var name = "ojej";

// implementacja części publicznej


// uwaga -- brak var
myobj = {
// metoda uprzywilejowana
getName: function () {
return name;
}
};
}());

myobj.getName(); // "ojej"

Ten sam pomysł, ale w nieco innym wykonaniu przedstawia poniższy kod.
var myobj = (function () {
// składowe prywatne
var name = "ojej";

// implementacja części publicznej


return {
getName: function () {
return name;
}
};
}());

myobj.getName(); // "ojej"

Przedstawiony przykład stanowi podstawę tak zwanego wzorca modułu, który zostanie do-
kładniej opisany w dalszej części rozdziału.

Prototypy a prywatność
Jedną z wad składowych prywatnych używanych w konstruktorach jest fakt, iż są one two-
rzone przy każdym wywołaniu konstruktora (przy każdym utworzeniu nowego obiektu).
W zasadzie problem ten dotyczy wszystkich składowych dodawanych do this wewnątrz kon-
struktorów. Aby uniknąć powielania i zaoszczędzić pamięć, można wspólne właściwości i meto-
dy dodać do właściwości prototype konstruktora. W ten sposób wspólne elementy będą współ-
dzielone przez wszystkie egzemplarze obiektów utworzone za jego pomocą. Co więcej, wszystkie
egzemplarze mogą stosować te same zmienne prywatne. Aby to uzyskać, musimy połączyć dwa
wzorce: zmiennych prywatnych w konstruktorze i właściwości prywatnych w literałach obiek-
tów. Ponieważ właściwość prototype to tylko obiekt, można ją utworzyć za pomocą literału.

98 | Rozdział 5. Wzorce tworzenia obiektów


Oto przykład połączenia wzorców:
function Gadget() {
// zmienna prywatna
var name = 'iPod';
// funkcja publiczna
this.getName = function () {
return name;
};
}

Gadget.prototype = (function () {
// zmienna prywatna
var browser = "Mobile WebKit";
// prototyp składowych publicznych
return {
getBrowser: function () {
return browser;
}
};
}());

var toy = new Gadget();


console.log(toy.getName()); // uprzywilejowane metody własne
console.log(toy.getBrowser()); // uprzywilejowane metody z prototypu

Udostępnianie funkcji prywatnych jako metod publicznych


Wzorzec udostępniania polega na przekazywaniu metod prywatnych jako publicznych.
Takie rozwiązanie może być użyteczne, jeśli cała funkcjonalność obiektu jest niezbędna do
jego prawidłowej pracy, w związku z czym trzeba chronić metody w jak najlepszy sposób.
Z drugiej strony niektóre z metod prywatnych zapewniają operacje użyteczne również dla
świata zewnętrznego. Metody publiczne są jednak narażone na zmianę — dowolny inny kod
może je podmienić celowo lub nieświadomie. W standardzie ECMAScript 5 istnieje możli-
wość zamrożenia obiektu, ale nie ma jej w starszych wersjach języka. Z pomocą przychodzi
wzorzec udostępniania zwany również wzorcem odkrywania (Christian Heilmann użył ory-
ginalnie terminu odkrywczy wzorzec modułu).
Przyjrzyjmy się przykładowi bazującemu na jednym ze wzorców prywatności — składowych
prywatnych w literałach obiektów.
var myarray;

(function () {

var astr = "[object Array]",


toString = Object.prototype.toString;

function isArray(a) {
return toString.call(a) === astr;
}

function indexOf(haystack, needle) {


var i = 0,
max = haystack.length;
for (; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}

Metody i właściwości prywatne | 99


return 1;
}

myarray = {
isArray: isArray,
indexOf: indexOf,
inArray: indexOf
};
}());

Przykład zawiera dwie zmienne prywatne i dwie funkcje prywatne: isArray() i indexOf().
W końcowej części funkcji natychmiastowej do obiektu myarray trafia funkcjonalność, która
powinna być dostępna publicznie. W tym przypadku ta sama prywatna metoda indexOf()
udostępniana jest pod nazwą stosowaną w standardzie ECMAScript 5 (indexOf), jak i pod
nazwą zaczerpniętą z języka PHP (inArray). Oto testy nowego obiektu myarray:
myarray.isArray([1,2]); // true
myarray.isArray({0: 1}); // false
myarray.indexOf(["a", "b", "z"], "z"); // 2
myarray.inArray(["a", "b", "z"], "z"); // 2

Gdy wydarzy się coś nieprzewidzianego z publiczną wersją indexOf(), jej prywatny odpo-
wiednik wciąż będzie bezpieczny i wersja inArray() nadal będzie działała prawidłowo.
myarray.indexOf = null;
myarray.inArray(["a", "b", "z"], "z"); // 2

Wzorzec modułu
Wzorzec modułu jest powszechnie stosowany, bo pomaga zapewnić strukturę, która jest nie-
zbędna przy większej ilości kodu. W odróżnieniu od innych języków JavaScript nie posiada
żadnej specjalnej składni do tworzenia pakietów, ale wzorzec modułu daje narzędzia po-
zwalające tworzyć odseparowane od siebie fragmenty kodu, które można traktować jako tak
zwane czarne skrzynki i dodawać, usuwać lub zastępować w zależności od potrzeb.
Wzorzec modułu to połączenie kilku wzorców opisanych do tej pory w książce:
• przestrzeni nazw,
• funkcji natychmiastowych,
• składowych prywatnych i uprzywilejowanych,
• deklarowania zależności.

Pierwszy krok polega na ustawieniu przestrzeni nazw. W tym celu wykorzystamy metodę
namespace() zdefiniowaną we wcześniejszej części rozdziału i utworzymy przykładowy
moduł zawierający przydatne metody pomocnicze dotyczące tablic.
MYAPP.namespace('MYAPP.utilities.array');

Następny krok to zdefiniowanie modułu. Wzorzec wykorzystuje funkcję natychmiastową, która


w razie potrzeby zapewni zakres lokalny zmiennych. Funkcja natychmiastowa zwraca obiekt
— rzeczywisty moduł z interfejsem publicznym, z którego mogą korzystać użytkownicy.
MYAPP.utilities.array = (function () {
return {
// do wykonania...
};
}());

100 | Rozdział 5. Wzorce tworzenia obiektów


Następnie dodajmy pewne metody do interfejsu publicznego.
MYAPP.utilities.array = (function () {
return {
inArray: function (needle, haystack) {
// ...
},
isArray: function (a) {
// ...
}
};
}());

Korzystając z zakresu lokalnego zapewnianego przez funkcję natychmiastową, możemy za-


deklarować w razie potrzeby właściwości i metody prywatne. Na samej górze funkcji na-
tychmiastowej można umieścić deklaracje dotyczące zależności modułu od innych modułów.
Nic nie stoi na przeszkodzie, by zaraz pod nimi umieścić opcjonalny kod jego jednorazowej
inicjalizacji. Efektem końcowym jest obiekt zwracany przez funkcję natychmiastową, który
określa publiczny interfejs modułu.
MYAPP.namespace('MYAPP.utilities.array');

MYAPP.utilities.array = (function () {

// zależności
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.lang,

// właściwości prywatne
array_string = "[object Array]",
ops = Object.prototype.toString;

// metody prywatne
// ...

// koniec var

// opcjonalna inicjalizacja jednorazowa


// ...

// interfejs publiczny
return {
inArray: function (needle, haystack) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return true;
}
}
},

isArray: function (a) {


return ops.call(a) === array_string;
}
// dalsze metody i właściwości
};
}());

Wzorzec modułu jest powszechnie stosowaną i bardzo zalecaną metodą organizacji kodu,
szczególnie gdy jest go naprawdę sporo.

Wzorzec modułu | 101


Odkrywczy wzorzec modułu
Kilka akapitów wcześniej jako część wzorców dotyczących prywatności pojawił się wzorzec
udostępniania. Wzorzec modułu można zorganizować w podobny sposób — wszystkie me-
tody uczynić prywatnymi i na zewnątrz udostępniać tylko te, które mają stanowić część
publicznego API.
Po przeróbkach poprzedni przykład wyglądałby następująco:
MYAPP.utilities.array = (function () {

// właściwości prywatne
var array_string = "[object Array]",
ops = Object.prototype.toString,

// metody prywatne
inArray = function (haystack, needle) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return 1;
},
isArray = function (a) {
return ops.call(a) === array_string;
};
// koniec var

// odkrywanie publicznego API


return {
isArray: isArray,
indexOf: inArray
};
}());

Moduły, które tworzą konstruktory


W poprzednim przykładzie jest tworzony obiekt MYAPP.utilities.array, ale czasem lepiej
jest tworzyć obiekty za pomocą funkcji konstruujących. Nic nie stoi na przeszkodzie, by
funkcje te definiować przy użyciu wzorca modułu. Jedyna różnica polega na tym, że funkcja
natychmiastowa otaczająca moduł zwróci na końcu funkcję zamiast obiektu.
Poniższy przykład ilustruje zastosowanie wzorca modułu, który tworzy funkcję konstruującą
MYAPP.utilities.Array.
MYAPP.namespace('MYAPP.utilities.Array');

MYAPP.utilities.Array = (function () {

// zależności
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.lang,

// właściwości i metody prywatne...


Constr;

// koniec var

// opcjonalna inicjalizacja jednorazowa

102 | Rozdział 5. Wzorce tworzenia obiektów


// ...

// API publiczne — konstruktor


Constr = function (o) {
this.elements = this.toArray(o);
};
// API publiczne — prototyp
Constr.prototype = {
constructor: MYAPP.utilities.Array,
version: "2.0",
toArray: function (obj) {
for (var i = 0, a = [], len = obj.length; i < len; i += 1) {
a[i] = obj[i];
}
return a;
}
};

// zawraca konstruktor, który zostanie


// przypisany do nowej przestrzeni nazw
return Constr;

}());

Sposób korzystania z nowego konstruktora jest następujący:


var arr = new MYAPP.utilities.Array(obj);

Import zmiennych globalnych do modułu


W często spotykanej odmianie omawianego wzorca do funkcji natychmiastowej otaczającej
moduł przekazuje się parametry. Można przekazać dowolne wartości, ale najczęściej są nimi
referencje do zmiennych globalnych, a nawet sam obiekt globalny. Import zmiennych global-
nych wewnątrz funkcji natychmiastowej przyspiesza konwersję nazw zmiennych na prze-
chowywane w nich referencje, gdyż stają się one dla tej funkcji zmiennymi lokalnymi.
MYAPP.utilities.module = (function (app, global) {

// referencje do obiektu globalnego


// i jego przestrzeni nazw są teraz
// równoważne zmiennym lokalnym

}(MYAPP, this));

Wzorzec piaskownicy
Wzorzec piaskownicy ma za zadanie wyeliminować dwie wady wzorca przestrzeni nazw:
• Wykorzystanie jednej zmiennej globalnej jako globalnego punktu dostępu do aplikacji;
we wzorcu przestrzeni nazw nie mamy możliwości zastosowania dwóch wersji tej samej
aplikacji lub biblioteki na tej samej stronie, ponieważ obie korzystałyby z tej samej glo-
balnej nazwy (na przykład MYAPP).
• Potrzebę używania długich i rozwiązywanych w trakcie działania programu nazw takich
jak MYAPP.utilities.array.

Wzorzec piaskownicy | 103


Jak sama nazwa wskazuje, wzorzec piaskownicy umożliwia zapewnienie modułom odpo-
wiednich środowisk, w których mogą się „bawić” bez wpływania na inne moduły i ich oso-
biste piaskownice.
Wzorzec ten jest powszechnie stosowany w bibliotece YUI w wersji 3., ale warto pamiętać,
że przedstawiony w książce opis nie stanowi odzwierciedlenia rzeczywistej implementacji
z biblioteki. Jest to jedynie przykładowa implementacja do celów poglądowych.

Globalny konstruktor
We wzorcu przestrzeni nazw mamy jeden globalny obiekt — we wzorcu piaskownicy tym
pojedynczym obiektem jest konstruktor, któremu możemy nadać nazwę Sandbox(). Obiekty
tworzy się za pomocą konstruktora, ale również przekazuje się im funkcję wywołania zwrot-
nego, która staje się izolowaną piaskownicą dla własnego kodu.
Zastosowanie piaskownicy wygląda następująco:
new Sandbox(function (box) {
// tu znajduje się kod aplikacji
});

Obiekt box stanowi odpowiednik obiektu MYAPP ze wzorca przestrzeni nazw — będzie za-
wierał całą funkcjonalność biblioteczną niezbędną do zapewnienia prawidłowego działania
aplikacji.
Dodajmy do wzorca jeszcze dwa następujące elementy:
• Przy odrobinie magii (wzorzec wymuszenia new z rozdziału 3.) możliwe będzie pominięcie
new w konstruktorze.
• Konstruktor Sandbox() będzie przyjmował dodatkowy parametr konfiguracyjny okre-
ślający nazwy obiektów wymaganych w instancji obiektu. Ponieważ kod powinien być
modułowy, większość funkcjonalności zapewnianej przez Sandbox() znajdzie się
w modułach.
Zastanówmy się, jak będzie wyglądał kod programu po wprowadzeniu dwóch wspomnia-
nych funkcjonalności.
Możemy pominąć new i utworzyć obiekt, który wykorzystuje dwa fikcyjne moduły: ajax
i event.
Sandbox(['ajax', 'event'], function (box) {
// console.log(box);
});

Poniższy przykład jest podobny do poprzedniego, ale nazwy modułów zostały przekazane
jako osobne argumenty.
Sandbox('ajax', 'dom', function (box) {
// console.log(box);
});

Można by nawet dodać specjalny argument o treści *, który oznaczałby dodanie wszystkich
dostępnych modułów. Dla uproszczenia kodu obiekt piaskownicy może zakładać, że brak
określenia modułów oznacza chęć skorzystania ze wszystkich dostępnych.

104 | Rozdział 5. Wzorce tworzenia obiektów


Sandbox('*', function (box) {
// console.log(box);
});

Sandbox(function (box) {
// console.log(box);
});

Ostatni przykład użycia wzorca ilustruje, jak wyglądałoby tworzenie kilku piaskownic — co
istotne, mogą one nawet znajdować się jedna wewnątrz drugiej bez wzajemnych interferencji.
Sandbox('dom', 'event', function (box) {

// wykorzystanie modułów dom i event

Sandbox('ajax', function (box) {


// kolejna piaskownica z nowym obiektem box
// ten box nie jest taki sam
// jak box poza tą funkcją

// ...

// koniec operacji dotyczących modułu ajax


});

// nie ma śladu po module ajax

});

Jak nietrudno zauważyć w przedstawionych przykładach, stosując wzorzec piaskownicy,


można bardzo łatwo zabezpieczyć globalną przestrzeń nazw za pomocą funkcji zwrotnych
otaczających właściwy kod.
Jeśli istnieje taka potrzeba, można wykorzystać fakt, iż funkcje są obiektami, i zapamiętać
pewne dane jako statyczne właściwości konstruktora Sandbox().
Co więcej, poszczególne fragmenty kodu mogą zawierać niezależne od siebie zestawy
modułów, które działają w pełnej separacji.
Teraz zastanówmy się, jak zaimplementować konstruktor Sandbox(), by zapewniał on całą
funkcjonalność przedstawioną w przykładach.

Dodawanie modułów
Przed zaimplementowaniem rzeczywistego konstruktora pomyślmy, w jaki sposób będą
określane moduły.
Funkcja konstruująca Sandbox() jest obiektem, więc można dodać do niej właściwość sta-
tyczną o nazwie modules. Właściwość ta będzie obiektem zawierającym pary klucz-wartość,
gdzie kluczem będzie nazwa modułu, a wartością funkcje implementujące dany moduł.
Sandbox.modules = {};

Sandbox.modules.dom = function (box) {


box.getElement = function () {};
box.getStyle = function () {};
box.foo = "bar";
};

Sandbox.modules.event = function (box) {

Wzorzec piaskownicy | 105


// uzyskiwanie dostępu do prototypu Sandbox, jeśli to niezbędne:
// box.constructor.prototype.m = "mmm";
box.attachEvent = function () {};
box.dettachEvent = function () {};
};

Sandbox.modules.ajax = function (box) {


box.makeRequest = function () {};
box.getResponse = function () {};
};

W tym przykładzie pojawiły się moduły dom, event i ajax, ponieważ są to najczęściej wyko-
rzystywane funkcjonalności, które pojawiają się w każdej bibliotece lub złożonej aplikacji.
Funkcje, które implementują każdy moduł, przyjmują jako parametr instancję aktualnego
obiektu box, by mogły dodać do niego nowe właściwości i metody.

Implementacja konstruktora
Przystąpmy do implementacji konstruktora Sandbox(). Oczywiście we własnym projekcie
warto rozważyć zmianę jego nazwy na bardziej dopasowaną do tworzonej biblioteki lub
aplikacji.
function Sandbox() {
// zamiana argumentów na tablicę
var args = Array.prototype.slice.call(arguments),
// ostatni argument to funkcja wywołania zwrotnego
callback = args.pop(),
// moduły mogą zostać przekazane jako tablica lub osobne parametry
modules = (args[0] && typeof args[0] === "string") ? args : args[0],
i;

// sprawdzenie, czy funkcja została


// wywołana jako konstruktor
if (!(this instanceof Sandbox)) {
return new Sandbox(modules, callback);
}

// dodanie w razie potrzeby właściwości do this


this.a = 1;
this.b = 2;

// dodaj moduły do głównego obiektu this


// brak modułów lub * oznacza zastosowanie wszystkich modułów
if (!modules || modules === '*') {
modules = [];
for (i in Sandbox.modules) {
if (Sandbox.modules.hasOwnProperty(i)) {
modules.push(i);
}
}
}

// inicjalizacja wymaganych modułów


for (i = 0; i < modules.length; i += 1) {
Sandbox.modules[modules[i]](this);
}

// wywołanie funkcji zwrotnej


callback(this);
}

106 | Rozdział 5. Wzorce tworzenia obiektów


// dodanie w razie potrzeby ogólnych właściwości do prototypu
Sandbox.prototype = {
name: "Moja aplikacja",
version: "1.0",
getName: function () {
return this.name;
}
};

Implementacja ta zawiera kilka elementów godnych uwagi.


• Sprawdza, czy this jest instancją Sandbox, i jeśli nie jest (czyli Sandbox() zostało wywo-
łane bez new), ponownie wywołuje konstruktor.
• Konstruktor może dodawać nowe właściwości do this. Podobnie, nic nie stoi na prze-
szkodzie, by dodać właściwości do prototypu konstruktora.
• Wymagane moduły mogą zostać przekazane jako tablica z ich nazwami lub jako poje-
dyncze argumenty. Dodatkowo znak * lub brak podanych modułów oznacza, że powinny
zostać wczytane wszystkie dostępne moduły. Zaprezentowana implementacja nie wczy-
tuje niezbędnych modułów z dodatkowych plików, ale dodanie odpowiedniego do tego
kodu nie stanowi żadnego problemu. Podobną funkcjonalność wykorzystuje biblioteka
YUI3 — wystarczy tylko wczytać najbardziej podstawowy moduł (tak zwane ziarno),
a pozostałe zostaną pobrane automatycznie w zależności od potrzeb przez zastosowanie
prostej konwencji nazewnictwa, w której nazwa modułu odpowiada nazwie pliku.
• Po uzyskaniu listy niezbędnych modułów są one inicjalizowane, czyli zostaje wywołana
funkcja implementująca każdy z nich.
• Ostatnim argumentem konstruktora jest funkcja wywołania zwrotnego, która zostanie
wywołana na samym końcu i wykorzysta utworzoną instancję. Jest ona tak naprawdę
piaskownicą z kodem użytkownika i box otrzymuje wszystkie pożądane moduły.

Składowe statyczne
Właściwości i metody statyczne to takie, które nie ulegają zmianie między poszczególnymi
instancjami obiektu. W językach obiektowych bazujących na klasach składowe statyczne są
tworzone za pomocą specjalnej składni, a następnie używa się ich, jakby były składowymi
klasy. Przykładowo, metoda statyczna max() pewnej klasy MathUtils będzie wywoływana
jako MathUtils.max(3, 5). To przykład publicznej składowej statycznej dostępnej bez
potrzeby tworzenia instancji. Mogą również istnieć prywatne składowe statyczne, które nie
są dostępne dla świata zewnętrznego, ale z których mogą korzystać wszystkie instancje.
Zobaczmy, jak w języku JavaScript zaimplementować oba te rodzaje.

Publiczne składowe statyczne


W języku JavaScript nie istnieje żadna szczególna składnia związana ze składowymi statycz-
nymi, ale nietrudno uzyskać składnię bardzo podobną do występującej w językach progra-
mowania stosujących klasy — wystarczy użyć funkcji konstruującej i przypisać do niej wła-
ściwości. Wszystko działa prawidłowo, bo konstruktory, podobnie jak wszystkie funkcje, są
obiektami i mogą mieć właściwości. Z podobnego podejścia korzysta przedstawiony wcze-
śniej wzorzec zapamiętywania, który dodaje właściwości do funkcji.

Składowe statyczne | 107


Poniższy przykład zawiera definicję konstruktora Gadget z metodą statyczną isShiny() oraz
standardową metodą setPrice(). Metoda isShiny() jest traktowana jako statyczna, bo do
działania nie wymaga żadnego obiektu gadżetu (nie trzeba konkretnego gadżetu, by spraw-
dzić, że wszystkie są błyszczące). Metoda setPrice() wymaga obiektu, bo każdy gadżet
może mieć inną cenę.
// konstruktor
var Gadget = function () {};

// metoda statyczna
Gadget.isShiny = function () {
return "Oczywiście";
};

// standardowa metoda dodawana do prototypu


Gadget.prototype.setPrice = function (price) {
this.price = price;
};

Wywołajmy utworzone metody. Metoda statyczna isShiny() jest dostępna bezpośrednio


z poziomu konstruktora, a standardowa wymaga utworzenia obiektu.
// wywołanie metody statycznej
Gadget.isShiny(); // "Oczywiście"

// utworzenie obiektu i wywołanie metody


var iphone = new Gadget();
iphone.setPrice(500);

Wywołanie zwykłej metody w sposób statyczny i odwrotnie nie powiedzie się.


typeof Gadget.setPrice; // "undefined"
typeof iphone.isShiny; // "undefined"

Czasem byłoby wygodniej, gdyby metoda statyczna była również dostępna jako metoda in-
stancji (obiektu). Osiągnięcie tego jest bardzo proste — wymaga jedynie przypisania metody
do prototypu, czyli utworzenia referencji wskazującej na oryginalną metodę.
Gadget.prototype.isShiny = Gadget.isShiny;
iphone.isShiny(); // "Oczywiście"

W takich sytuacjach, pisząc metodę statyczną, trzeba bardzo uważać na użycie this. Wywo-
łanie Gadget.isShiny() oznacza, że this wewnątrz isShiny() będzie wskazywało na kon-
struktor Gadget. W wywołaniu iphone.isShiny() będzie natomiast wskazywało na iphone.
Ostatni przykład ilustruje, w jaki sposób można uzyskać odmienne zachowanie metody w za-
leżności od tego, czy jest wywoływana jako metoda statyczna, czy nie. Operator instanceof
pomaga określić sposób jej wywołania.
// konstruktor
var Gadget = function (price) {
this.price = price;
};

// metoda statyczna
Gadget.isShiny = function () {

// to zadziała za każdym razem


var msg = "Oczywiście";

if (this instanceof Gadget) {


// to zadziała tylko wtedy, gdy metoda zostanie wywołana jako niestatyczna
msg += ", kosztuje " + this.price + ' zł!';

108 | Rozdział 5. Wzorce tworzenia obiektów


}
return msg;
};

// zwykła metoda dodana do prototypu


Gadget.prototype.isShiny = function () {
return Gadget.isShiny.call(this);
};

Wywołanie w charakterze metody statycznej:


Gadget.isShiny(); // "Oczywiście"

Wywołanie w charakterze metody obiektu:


var a = new Gadget('499,99');
a.isShiny(); // "Oczywiście, kosztuje 499,99 zł!"

Prywatne składowe statyczne


Do tej pory skupialiśmy się na publicznych metodach statycznych, więc najwyższy czas
przyjrzeć się składowym prywatnym i statycznym. Pod tym pojęciem rozumie się składowe,
które:
• są współdzielone przez wszystkie obiekty tworzone przez ten sam konstruktor;
• nie są dostępne poza konstruktorem.

Przyjrzyjmy się przykładowi, w którym counter będzie prywatną składową statyczną kon-
struktora Gadget. Ponieważ wcześniejsza część rozdziału zawierała informacje o składowych
prywatnych, nie pojawią się tutaj żadne tajne chwyty — nadal potrzebna jest funkcja działa-
jąca jako domknięcie wokół składowych prywatnych. Niech funkcja otaczająca wykona się od
razu i zwróci inną funkcję. Ta zwrócona funkcja zostanie przypisana do zmiennej Gadget,
stając się konstruktorem.
var Gadget = (function () {

// zmienna statyczna
var counter = 0;

// zwraca nową implementację konstruktora


return function () {
console.log(counter += 1);
};

}()); // natychmiastowe wykonanie

Nowy konstruktor Gadget zwiększa i zapamiętuje najnowsze wartości w prywatnej zmiennej


counter. Testowanie przy użyciu kilku instancji wskazuje, że licznik rzeczywiście jest współ-
dzielony przez wszystkie obiekty.
var g1 = new Gadget(); // wyświetla 1
var g2 = new Gadget(); // wyświetla 2
var g3 = new Gadget(); // wyświetla 3

Ponieważ przy każdym nowym obiekcie licznik jest zwiększany o 1, statyczna właściwość
jest niejako unikatowym identyfikatorem każdego obiektu tworzonego za pomocą konstruktora
Gadget. Unikatowy identyfikator może być przydatny, więc czy nie lepiej udostępnić go
dzięki metodzie uprzywilejowanej? Poniższy przykład bazuje na poprzednich i dodaje metodę
uprzywilejowaną getLastId() udostępniającą prywatną zmienną statyczną.

Składowe statyczne | 109


// konstruktor
var Gadget = (function () {

// zmienna (właściwość) statyczna


var counter = 0,
NewGadget;

// to stanie się nową implementacją konstruktora


NewGadget = function () {
counter += 1;
};

// metoda uprzywilejowana
NewGadget.prototype.getLastId = function () {
return counter;
};

// nadpisanie konstruktora
return NewGadget;

}()); // natychmiastowe wykonanie

Oto testy nowej implementacji:


var iphone = new Gadget();
iphone.getLastId(); // 1
var ipod = new Gadget();
ipod.getLastId(); // 2
var ipad = new Gadget();
ipad.getLastId(); // 3

Właściwości statyczne (prywatne lub publiczne) mogą być bardzo pomocne. Mogą zawierać
metody i dane niezwiązane z żadną konkretną instancją i nie będą tworzone osobno dla
każdego obiektu. Rozdział 7. zawiera opis wzorca singletonu, który w swej implementacji
korzysta z właściwości statycznych w celu uzyskania konstruktorów znanych z klas będą-
cych singletonami.

Stałe obiektów
W języku JavaScript nie istnieją stałe, choć wiele nowoczesnych środowisk wykonawczych
oferuje instrukcję const do ich definiowania.
Typowym obejściem problemu jest stosowanie konwencji nazewnictwa i oznaczanie wszystkich
zmiennych, których nie należy modyfikować, przez pisanie ich wielkimi literami. Dokładnie
ten sposób jest wykorzystywany w przypadku stałych z obiektów wbudowanych w język:
Math.PI; // 3.141592653589793
Math.SQRT2; // 1.4142135623730951
Number.MAX_VALUE; // 1.7976931348623157e+308

We własnych obiektach można zastosować dokładnie tę samą konwencję, dodając stałe jako
właściwości statyczne do funkcji konstruującej.
// konstruktor
var Widget = function () {
// implementacja...
};

// stałe
Widget.MAX_HEIGHT = 320;
Widget.MAX_WIDTH = 480;

110 | Rozdział 5. Wzorce tworzenia obiektów


Nic nie stoi na przeszkodzie, by konwencję zastosować również w literałach obiektów —
stałe mogą być zwykłymi właściwościami z nazwami pisanymi wielkimi literami.
Jeśli naprawdę chce się zastosować wartość bez możliwości jej zmiany, należy utworzyć
zmienną prywatną i zapewnić metodę pobierającą, ale nie ustawiającą. W większości sytuacji
będzie to przerost formy nad treścią, bo zwykła konwencja najczęściej wystarcza.
Poniższy przykład stanowi implementację ogólnego obiektu constant zapewniającego na-
stępujące metody:
• set(name, value) — definicja nowej stałej;
• isDefined(name) — sprawdzenie, czy stała istnieje;
• get(name) — pobranie wartości stałej.

W tej implementacji jako stałe mogą być używane tylko typy podstawowe. Dodatkowo zadbano,
by możliwe było zadeklarowanie stałych, których nazwy są nazwami wbudowanych właściwości
takich jak toString lub hasOwnProperty, wykorzystując sprawdzenie hasOwnProperty()
i dodając do wszystkich nazw stałych losowo wygenerowany przedrostek.
var constant = (function () {
var constants = {},
ownProp = Object.prototype.hasOwnProperty,
allowed = {
string: 1,
number: 1,
boolean: 1
},
prefix = (Math.random() + "_").slice(2);
return {
set: function (name, value) {
if (this.isDefined(name)) {
return false;
}
if (!ownProp.call(allowed, typeof value)) {
return false;
}
constants[prefix + name] = value;
return true;
},
isDefined: function (name) {
return ownProp.call(constants, prefix + name);
},
get: function (name) {
if (this.isDefined(name)) {
return constants[prefix + name];
}
return null;
}
};
}());

Testy implementacji:
// sprawdzenie, czy stała została zdefiniowana
constant.isDefined("maxwidth"); // false
// definicja
constant.set("maxwidth", 480); // true
// ponowne sprawdzenie
constant.isDefined("maxwidth"); // true
// próba zmiany definicji
constant.set("maxwidth", 320); // false
// czy wartość nadal jest nienaruszona?
constant.get("maxwidth"); // 480

Stałe obiektów | 111


Wzorzec łańcucha wywołań
Wzorzec łańcucha wywołań umożliwia wywoływanie metod obiektu jednej po drugiej bez
potrzeby przypisywania zwracanych wartości poprzednich operacji i bez konieczności dzie-
lenia wywołań na kilka wierszy:
myobj.method1("witaj").method2(",").method3("świecie").method4();

Gdy tworzy się metody, które nie zwracają żadnej sensownej wartości, można zwrócić aktualną
wartość this, czyli instancję obiektu, na którym metody aktualnie operują. Dzięki tej operacji
użytkownicy obiektu będą mogli łączyć wywołania metod w jeden łańcuch.
var obj = {
value: 1,
increment: function () {
this.value += 1;
return this;
},
add: function (v) {
this.value += v;
return this;
},
shout: function () {
alert(this.value);
}
};

// łańcuchowe wywołanie metod


obj.increment().add(3).shout(); // 5

// metod nie trzeba już wywoływać jednej po drugiej


obj.increment();
obj.add(3);
obj.shout(); // 5

Wady i zalety wzorca łańcucha wywołań


Zaletą wzorca jest niezaprzeczalny fakt, iż można zaoszczędzić nieco pisania i utworzyć
zwięzły kod, który czyta się niemalże jak zdanie.
Dodatkowym plusem jest to, że pomaga on dzielić funkcje na mniejsze, bardziej wyspecjali-
zowane. Unika się w ten sposób funkcji, które wykonują zbyt wiele zadań. Na dłuższą metę
zapewnia to znacznie łatwiejszą konserwację lub modyfikację kodu.
Wadą jest utrudnione testowanie i debugowanie napisanego w ten sposób kodu. Narzędzia
do analizy błędów najczęściej wyświetlają informację o wystąpieniu błędu w określonym
wierszu, ale tutaj w jednym wierszu wykonuje się zbyt wiele zadań. Jeśli jedna z metod tworzą-
cych łańcuch zgłasza błąd, trudno jest powiedzieć która. Robert Martin, autor książki Clean Code,
posuwa się nawet do nazwania opisywanego wzorca pociągiem do koszmaru.
Generalnie warto zauważać podobne sytuacje i jeśli metoda nie zwraca żadnej oczywistej
wartości, zawsze można zwrócić aktualne this. Zaprezentowany wzorzec jest powszechnie
stosowany w bibliotece jQuery. Jeśli dokładniej przyjrzeć się API DOM, nietrudno zauważyć,
że ono także może posłużyć do tworzenia łańcuchów wywołań:
document.getElementsByTagName('head')[0].appendChild(newnode);

112 | Rozdział 5. Wzorce tworzenia obiektów


Metoda method()
JavaScript jest językiem, który potrafi niejednokrotnie zmylić osoby przyzwyczajone do my-
ślenia w kategoriach klas. Właśnie z tego powodu niektórzy programiści proponują upodob-
nienie go do innych języków, które je stosują. Jedną z prób takiego przybliżenia jest metoda
method() zaproponowana przez Douglasa Crockforda. Po jakimś czasie przyznał on jednak,
że tworzenie z JavaScriptu języka klasowego nie jest zalecanym podejściem. Niezależnie od
tego jest to interesujący wzorzec, który można spotkać w niektórych aplikacjach.
Funkcje konstruujące przypominają klasy języka Java. Co więcej, umożliwiają one dodawanie
właściwości instancji do this w treści konstruktora. Z drugiej strony dodawanie metod w ten
sposób nie jest efektywne, ponieważ są one tworzone osobno dla każdej instancji, przez co
zajmują więcej pamięci. Właśnie dlatego metody stosowane przez wiele obiektów lepiej do-
dawać do właściwości prototype konstruktora. Właściwość ta może dla niektórych progra-
mistów wyglądać jak przybysz z innej planety, więc można ukryć ją za metodą.

Dodawane do języka przydatne funkcjonalności nazywa się najczęściej dodatkami


syntaktycznymi, bo tak naprawdę nie dodają one nowych funkcji, a jedynie
usprawniają już istniejące. W tym przypadku metodę method() można by nazwać
dodatkiem syntaktycznym.

Wykorzystanie metody method() do zdefiniowania „klasy” przybrałoby następującą postać:


var Person = function (name) {
this.name = name;
}.
method('getName', function () {
return this.name;
}).
method('setName', function (name) {
this.name = name;
return this;
});

Zauważmy, że po konstruktorze pojawia się wywołanie method() w wersji łańcuchowej, a po


nim następne wywołanie method() również przeprowadzone w ten sam sposób. Odpowiada
to opisanemu wcześniej wzorcowi łańcucha wywołań i umożliwia zdefiniowanie całej „klasy”
jednym poleceniem.
Metoda przyjmuje dwa parametry:
• nazwę nowej metody,
• implementację metody.

Nowa metoda trafia następnie do „klasy” Person. Implementacja jest zgodnie z oczekiwaniami
dodatkową funkcją, w której this wskazuje na obiekt utworzony przez Person.
Oto, w jaki sposób można utworzyć nowy obiekt Person() i z niego korzystać:
var a = new Person('Adam');
a.getName(); // "Adam"
a.setName('Ewa').getName(); // "Ewa"

Łańcuch wywołań jest możliwy do uzyskania, ponieważ metoda setName() zwraca this.

Metoda method() | 113


Oto, w jaki sposób zaimplementowana została metoda method():
if (typeof Function.prototype.method !== "function") {
Function.prototype.method = function (name, implementation) {
this.prototype[name] = implementation;
return this;
};
}

Wewnątrz metody method() najpierw następuje sprawdzenie, czy nie została ona już zaim-
plementowana. Jeśli nie, funkcja przekazana jako argument implementation trafia do pro-
totypu konstruktora. W tym przypadku this odnosi się do funkcji konstruującej, której wła-
ściwość prototype jest modyfikowana.

Podsumowanie
W tym rozdziale przedstawione zostały różne wzorce tworzenia obiektów, które wykraczają
poza podstawową tematykę związaną z tworzeniem literałów i funkcji konstruujących.
Wyjaśniony został wzorzec przestrzeni nazw, który ma za zadanie utrzymać globalną prze-
strzeń nazw w czystości i poprawić strukturę kodu. Wzorzec deklaracji zależności okazał się
także wyjątkowo prostą, a jednocześnie bardzo użyteczną techniką. Następnie pojawił się dosyć
szczegółowy opis wzorców prywatności zawierający omówienie składowych prywatnych,
metod uprzywilejowanych, pewnych przypadków krańcowych, użycia literałów obiektów
wraz ze składowymi prywatnymi i udostępniania metod prywatnych jako publicznych.
Wszystkie przedstawione rozwiązania posłużyły do zaprezentowania popularnego i uży-
tecznego wzorca modułu.
W dalszej kolejności omówiony został wzorzec piaskownicy stanowiący alternatywę dla
długich przestrzeni nazw, który dodatkowo ułatwia tworzenie niezależnych środowisk dla
kodu i modułów.
Na końcu rozdziału pojawiło się kilka tematów uzupełniających takich jak stałe obiektów,
metody statyczne (publiczne i prywatne), łańcuchy wywołań i metoda method().

114 | Rozdział 5. Wzorce tworzenia obiektów


ROZDZIAŁ 6.

Wzorce wielokrotnego użycia kodu

Wzorce wielokrotnego użycia tego samego kodu to ważny i interesujący temat, ponieważ
naturalnym jest, że każdy dąży do napisania jak najmniejszej ilości kodu i jak najczęstszego
stosowania tego, który już napisał (własnego lub innych osób). Dążenie to jest szczególnie
silne, gdy kod jest dobry, przetestowany, łatwy w konserwacji, rozszerzalny i dobrze udo-
kumentowany.
Gdy mówimy o wielokrotnym wykorzystaniu kodu, często pierwszą rzeczą przychodzącą
nam na myśl jest dziedziczenie, więc nie powinno dziwić, że spora część rozdziału została
poświęcona właśnie temu zagadnieniu. Pojawią się przykłady zarówno dziedziczenia w wersji
„klasycznej”, jak i innych. Nie należy jednak zapominać o celu nadrzędnym — wielokrotnym
użyciu tego samego kodu. Dziedziczenie to tylko jeden ze sposobów (środków) jego osią-
gnięcia. Jest ich więcej. Z kilku obiektów można na zasadzie kompozycji uzyskać inny obiekt,
można do obiektu dodać nową funkcjonalność na zasadzie dołączania (mix-in) lub pożyczyć
pewną funkcjonalność bez dziedziczenia w sensie technicznym.
Czytając niniejszy rozdział, nie należy zapominać, że autorzy kultowej książki na temat wzor-
ców projektowych zaoferowali swoim czytelnikom następującą radę: „preferuj kompozycję
obiektów zamiast dziedziczenia klas”.

Klasyczne i nowoczesne wzorce dziedziczenia


Czytając na temat dziedziczenia w języku JavaScript, bardzo często napotyka się termin
„dziedziczenie klasyczne”, więc najpierw zajmijmy się wyjaśnieniem, czemu w ogóle pojawia
się wyraz klasyczne. Termin ten nie oznacza tylko i wyłącznie wykonywania działań w spo-
sób tradycyjny lub ogólnie akceptowany. Duży nacisk położony jest na pierwszą część tego
słowa zbliżoną do słowa „klasa” — to gra w podobieństwo wyrazów.
Wiele języków programowania wykorzystuje klasy do określania szablonów obiektów. W tych
językach każdy obiekt stanowi instancję (egzemplarz) określonej klasy i w wielu z nich (na
przykład w języku Java) obiekt bez klasy nie może istnieć. Ponieważ w języku JavaScript nie
ma klas, instancje klas nie mają dużego sensu. Obiekty to po prostu pary klucz-wartość, które
mogą się tworzyć „w locie” lub w razie potrzeby dowolnie zmieniać.
Z drugiej strony JavaScript stosuje funkcje konstruujące, a składnia operatora new przypomina
składnię stosowaną w językach wykorzystujących klasy.

115
W języku Java napisalibyśmy:
Person adam = new Person();

W JavaScripcie napisalibyśmy:
var adam = new Person();

Poza jedną różnicą wynikającą z faktu, iż Java jest językiem o silnej kontroli typów i wymaga
zadeklarowania, że adam jest typu Person, składnia jest identyczna. Wywołanie w języku
JavaScript sugeruje, że Person jest klasą, choć w rzeczywistości to nadal funkcja. Podobieństwo
składniowe zmyliło wielu programistów, którzy zaczęli traktować JavaScript jak język bazujący
na klasach i tworzyć wzorce dziedziczenia zakładające istnienie klas. Takie implementacje
nazywa się klasycznymi, a wszystkie inne, które nie zakładają istnienia klas, nowoczesnymi.
Istnieje spory wybór wzorców dziedziczenia możliwych do zastosowania w projekcie. Jeśli zespół
nie czuje się niekomfortowo, gdy nie ma klas, zawsze stosuj jeden ze wzorców nowoczesnych.
Niniejszy rozdział najpierw omawia wzorce klasyczne, a następnie różne wersje nowocze-
snych wzorców dotyczących dziedziczenia.

Oczekiwane wyniki w przypadku stosowania


wzorca klasycznego
Celem dziedziczenia klasycznego jest uzyskanie obiektów tworzonych za pomocą funkcji kon-
struującej Child(), które dziedziczą również właściwości po innej funkcji konstruującej Parent().

Choć temat dotyczy wzorców klasycznych, starajmy się unikać słowa „klasa”. Zasto-
sowane terminy „konstruktor” i „funkcja konstruująca” są co prawda dłuższe, ale bar-
dziej prawidłowe i niedwuznaczne. Warto wystrzegać się stosowania słowa „klasa”
w trakcie rozmów w zespole, ponieważ w przypadku języka JavaScript może ono dla
różnych osób oznaczać coś innego.

Oto przykład zdefiniowania dwóch konstruktorów — Parent() i Child():


// konstruktor przodka
function Parent(name) {
this.name = name || 'Adam';
}

// funkcjonalność dodawana do prototypu


Parent.prototype.say = function () {
return this.name;
};

// pusty konstruktor potomka


function Child(name) {}

// przy odrobinie magii zachodzi dziedziczenie


inherit(Child, Parent);

Kod definiuje dwa konstruktory (przodka i potomka) oraz metodę say() dodawaną do pro-
totypu przodka. Następnie wywołuje funkcję inherit(), która zajmuje się dziedziczeniem.
Język nie zapewnia tej metody, więc trzeba ją zdefiniować samodzielnie. Przyjrzyjmy się kilku
jej ogólnym implementacjom.

116 | Rozdział 6. Wzorce wielokrotnego użycia kodu


Pierwszy wzorzec klasyczny — wzorzec domyślny
Najczęściej stosowanym rozwiązaniem jest utworzenie obiektu za pomocą konstruktora
Parent(), a następnie przypisanie go do prototypu Child(). Pierwsza implementacja funkcji
inherit() mogłaby mieć następującą postać:
function inherit(C, P) {
C.prototype = new P();
}

Warto pamiętać, że właściwość prototype musi wskazywać na obiekt, a nie na funkcję, więc
należy utworzyć obiekt za pomocą konstruktora przodka i to jego (a nie sam konstruktor)
przypisać jako prototyp. Innymi słowy, nie wolno zapomnieć o operatorze new, by ten wzo-
rzec zadziałał prawidłowo.
W dalszej części kodu programu, w której za pomocą new Child() tworzony jest obiekt,
dziedziczy on funkcjonalność po instancji Parent() dzięki prototypowi, co przedstawia
poniższy kod.
var kid = new Child();
kid.say(); // "Adam"

Podążanie wzdłuż łańcucha prototypów


Wzorzec ten umożliwia odziedziczenie zarówno właściwości własnych (właściwości instancji
dodanych do this takich jak name), jak i właściwości oraz metod dodanych do prototypu
takich jak say().
Prześledźmy, jak działa łańcuch prototypów w klasycznym wzorcu dziedziczenia. Dla celów
tej dyskusji potraktujmy obiekty jako pewne bloki pamięci, które zawierają dane i referencje
do innych bloków. Tworząc obiekt za pomocą new Parent(), tworzymy nowy blok (na ry-
sunku 6.1 jest to blok numer dwa). Zawiera on dane właściwości name. Jeżeli wywołamy
metodę say() (na przykład za pomocą (new Parent).say()), nie znajdziemy jej w drugim bloku.
Uzyskamy jednak dostęp do obiektu numer jeden za pomocą ukrytej referencji __proto__
wskazującej na prototyp funkcji konstruującej Parent(), czyli Parent.prototype, która to
zawiera metodę o wskazanej nazwie. Wszystko to wykona się automatycznie, ale warto wie-
dzieć, w jaki sposób wyszukiwane są dane i co może je zmienić. Referencja __proto__ jest
wykorzystywana tylko do wyjaśnienia łańcucha prototypów i nie jest dostępna w samym
języku, choć udostępniają ją niektóre środowiska uruchomieniowe (na przykład przeglą-
darka Firefox).

Rysunek 6.1. Łańcuch prototypów dla konstruktora Parent()

Pierwszy wzorzec klasyczny — wzorzec domyślny | 117


Zastanówmy się, co się stanie, jeśli nowy obiekt zostanie utworzony za pomocą kodu var kid
= new Child() po wcześniejszym użyciu funkcji inherit(). Sytuację przedstawia rysunek 6.2.

Rysunek 6.2. Łańcuch prototypów po dziedziczeniu


Konstruktor Child() jest pusty, a do prototypu Child.prototype nie dodano żadnych ele-
mentów. Oznacza to, że obiekt powstały dzięki new Child() jest w zasadzie pusty poza ukrytą
referencją __proto__. W tym przypadku __proto__ wskazuje na obiekt new Parent() utwo-
rzony w funkcji inherit().
Co się stanie, jeśli napiszemy kid.say()? Obiekt numer trzy nie ma takiej metody, więc in-
terpreter poszuka jej w następnym obiekcie łańcucha prototypów. Obiekt numer dwa również
jej nie zawiera, więc nastąpi jeszcze jedno wyszukiwanie. Obiekt numer jeden ma metodę
o takiej nazwie. Metoda say() wykorzystuje referencję this.name, więc cały proces rozpocz-
nie się od nowa. W tej sytuacji this wskazuje na obiekt numer trzy, który nie zawiera name.
Następny w łańcuchu jest obiekt numer dwa, który zawiera właściwość o tej nazwie — to jej
wartość (Adam) zostanie zwrócona.
Przyjrzyjmy się jeszcze jednemu przykładowi. Załóżmy istnienie następującego kodu:
var kid = new Child();
kid.name = "Patryk";
kid.say(); // "Patryk"

Rysunek 6.3 przedstawia łańcuch prototypów dla takiej sytuacji.

Rysunek 6.3. Łańcuch prototypów po dziedziczeniu i dodaniu właściwości do obiektu potomnego

118 | Rozdział 6. Wzorce wielokrotnego użycia kodu


Ustawienie kid.name nie zmienia zawartości właściwości name obiektu numer dwa, ale two-
rzy nową właściwość bezpośrednio w obiekcie kid. Wywołanie kid.say() spowoduje wy-
szukiwanie name najpierw w obiekcie numer trzy, a dopiero potem w dwa i jeden. Ponieważ
this.name jest w tym przypadku równoważne kid.name, wyszukiwanie zakończy się bardzo
szybko zwróceniem wartości z obiektu numer trzy.
Jeśli nowa właściwość zostanie usunięta za pomocą polecenia delete kid.name, właściwość
name z obiektu numer dwa ponownie będzie widoczna i to jej wartość zostanie zwrócona
w następnych użyciach metody say().

Wady wzorca numer jeden


Wadą tego wzorca jest fakt, iż dziedziczone są zarówno właściwości dodane do obiektu nad-
rzędnego, jak i właściwości jego prototypu. W większości sytuacji właściwości dodane do
obiektu nie są potrzebne, bo najczęściej są specyficzne dla konkretnej instancji.

Ogólna zasada dotycząca konstruktorów jest następująca: składowe używane wielo-


krotnie przez różne obiekty należy dodawać do prototypu.

Wadą ogólnej implementacji inherit() jest to, iż nie umożliwia ona przekazywania do kon-
struktorów potomnych parametrów, które potomek przekazuje później do swojego przodka.
Rozważmy następujący przykład:
var s = new Child('Set');
s.say(); // "Adam"

Raczej nie taki był oczekiwany wynik. Potomek może przekazywać parametry do konstruk-
tora przodka, ale wtedy dziedziczenie musi być wykonywane osobno dla każdego potomka,
co nie jest wydajne, bo obiekt przodka powstaje ciągle na nowo.

Drugi wzorzec klasyczny — pożyczanie konstruktora


Następny wzorzec rozwiązuje problem przekazywania argumentów z potomka do przodka.
Pożycza się w nim konstruktor przodka, przekazując obiekt potomka jako this i przekazując
dalej wszystkie argumenty:
function Child(a, c, b, d) {
Parent.apply(this, arguments);
}

W ten sposób można jednak dziedziczyć jedynie właściwości dodane do this wewnątrz kon-
struktora przodka. Składowe dodane do prototypu nie zostaną odziedziczone.
We wzorcu pożyczenia konstruktora obiekty potomne otrzymują kopie odziedziczonych
składowych, a nie jedynie ich referencje, jak to miało miejsce w przypadku pierwszego wzorca
klasycznego. Poniższy przykład ilustruje różnicę.
// konstruktor przodka
function Article() {
this.tags = ['js', 'css'];
}
var article = new Article();

Drugi wzorzec klasyczny — pożyczanie konstruktora | 119


// blog dziedziczy po obiekcie article;
// wykorzystuje przy tym pierwszy wzorzec klasyczny
function BlogPost() {}
BlogPost.prototype = article;
var blog = new BlogPost();
// powyżej nie było potrzebne new Article(),
// ponieważ instancja była już dostępna

// strona statyczna dziedziczy po obiekcie article,


// wykorzystując zapożyczony wzorzec konstruktora
function StaticPage() {
Article.call(this);
}
var page = new StaticPage();

alert(article.hasOwnProperty('tags')); // true
alert(blog.hasOwnProperty('tags')); // false
alert(page.hasOwnProperty('tags')); // true

We wzorcu tym konstruktor Article() jest dziedziczony na dwa sposoby. Wzorzec domyśl-
ny zapewnia obiektowi blog dostęp do właściwości tags za pośrednictwem prototypu, więc
nie stanowi ona jego własnej właściwości i hasOwnProperty() zwraca wartość false. Obiekt
page zawiera własną wersję właściwości tags, ponieważ stosując pożyczony konstruktor,
uzyskał jej własną kopię (a nie referencję).
Różnica w momencie modyfikacji odziedziczonej właściwości tags przedstawia się następująco:
blog.tags.push('html');
page.tags.push('php');
alert(article.tags.join(', ')); // "js, css, html"

W przykładzie obiekt potomny blog modyfikuje właściwość tags, ale jednocześnie modyfi-
kuje też przodka, ponieważ blog.tags i article.tags to w zasadzie ta sama tablica. Zmiany
w page.tags nie wpływają na przodka, ponieważ page posiada własną kopię tablicy uzy-
skaną w momencie dziedziczenia.

Łańcuch prototypów
Przyjrzyjmy się łańcuchowi prototypów w tym wzorcu dla znanych nam już konstruktorów
Parent() i Child(). Konstruktor Child() zmieniono, by dostosować go do nowego wzorca.
// konstruktor przodka
function Parent(name) {
this.name = name || 'Adam';
}

// dodanie funkcjonalności do prototypu


Parent.prototype.say = function () {
return this.name;
};

// konstruktor potomka
function Child(name) {
Parent.apply(this, arguments);
}

var kid = new Child("Patryk");


kid.name; // "Patryk"
typeof kid.say; // "undefined"

120 | Rozdział 6. Wzorce wielokrotnego użycia kodu


Rysunek 6.4 pokazuje, że nie istnieje już połączenie między obiektem new Child i Parent.
Wynika to z prostego faktu: Child.prototype nie został użyty i wskazuje na pusty obiekt.
W tym wzorcu obiekt kid otrzymał własną właściwość name, ale metoda say() nie została
odziedziczona, więc próba jej wywołania spowoduje zgłoszenie błędu. Dziedziczenie okazało
się działaniem jednorazowym, które jedynie skopiowało właściwości przodka do właściwości
rodzica — nie brały w tym udziału żadne referencje __proto__.

Rysunek 6.4. Niepełny łańcuch prototypów po dziedziczeniu z użyciem wzorca pożyczania konstruktora

Dziedziczenie wielobazowe
przy użyciu pożyczania konstruktorów
Nic nie stoi na przeszkodzie, by stosując wzorzec pożyczania konstruktorów, pożyczyć więcej
niż jeden konstruktor i uzyskać proste dziedziczenie wielobazowe.
function Cat() {
this.legs = 4;
this.say = function () {
return "miiaał";
}
}

function Bird() {
this.wings = 2;
this.fly = true;
}

function CatWings() {
Cat.apply(this);
Bird.apply(this);
}

var jane = new CatWings();


console.dir(jane);

Wynik tej operacji przedstawia rysunek 6.5. W przypadku duplikatów wygra ten, który będzie
przypisywany jako ostatni.

Drugi wzorzec klasyczny — pożyczanie konstruktora | 121


Rysunek 6.5. Obiekt CatWings wyświetlony w narzędziu Firebug

Zalety i wady wzorca pożyczania konstruktora


Oczywistą wadą tego wzorca jest to, że nic nie jest dziedziczone po prototypie, a jak wcze-
śniej wspomniano, to właśnie prototyp stanowi podstawowe miejsce do dodawania wielo-
krotnie używanych metod i właściwości, by nie były ponownie tworzone dla każdej instancji.
Niewątpliwą zaletą są rzeczywiste kopie własnych składowych przodka, dzięki czemu nie
istnieje ryzyko, że któryś potomek przypadkowo nadpisze jego właściwości.
W jaki sposób uzyskać dziedziczenie prototypu przez potomków, by kid miał dostęp do
metody say()? Na to pytanie odpowiada następny wzorzec.

Trzeci wzorzec klasyczny


— pożyczanie i ustawianie prototypu
Łącząc dwa poprzednie wzorce, najpierw pożyczamy konstruktor, a następnie dodatkowo
ustawiamy prototyp potomka na nową instancję konstruktora przodka:
function Child(a, c, b, d) {
Parent.apply(this, arguments);
}
Child.prototype = new Parent();

Rozwiązanie to zapewnia, że wynikowy obiekt otrzyma kopie własnych składowych przodka,


a jednocześnie będzie miał dostęp do jego ogólnej funkcjonalności (zaimplementowanej w proto-
typie). Co więcej, potomek może przekazać do konstruktora przodka dowolne argumenty.
Przedstawione podejście jest zapewne najbliższe funkcjonalności dziedziczenia dostępnej
w języku Java — obiekt potomny dziedziczy wszystko po przodku, a jednocześnie ma własne
kopie właściwości, więc nie ma ryzyka, że dokona modyfikacji przodka.
Wadą jest dwukrotne wywoływanie konstruktora przodka, co z pewnością nie jest wydajne.
W efekcie jego własne właściwości (takie jak name z przykładu) są dziedziczone dwukrotnie.
Przyjrzyjmy się kodowi i wykonajmy kilka testów.
// konstruktor przodka
function Parent(name) {
this.name = name || 'Adam';
}

// dodanie funkcjonalności do prototypu


Parent.prototype.say = function () {
return this.name;
};

122 | Rozdział 6. Wzorce wielokrotnego użycia kodu


// konstruktor potomka
function Child(name) {
Parent.apply(this, arguments);
}
Child.prototype = new Parent();

var kid = new Child("Patryk");


kid.name; // "Patryk"
kid.say(); // "Patryk"
delete kid.name;
kid.say(); // "Adam"

W odróżnieniu od poprzedniego wzorca metoda say() jest dziedziczona prawidłowo. Właści-


wość name jest dziedziczona dwukrotnie, więc po usunięciu z potomka własnej kopii widoczna
jest druga z nich (dzięki łańcuchowi prototypów).
Rysunek 6.6 przedstawia wzajemne zależności między obiektami. Są one bardzo podobne do
zależności z rysunku 6.3, ale inny był sposób ich uzyskania.

Rysunek 6.6. Łańcuch prototypów połączony z kopiowaniem własnych składowych

Czwarty wzorzec klasyczny — współdzielenie prototypu


W odróżnieniu od poprzednich wzorców klasycznych, które wymagały dwóch wywołań
konstruktora przodka, następny wzorzec w ogóle go nie wywołuje.
W tym przypadku bardzo duży nacisk kładzie się na fakt, iż wszystkie składowe wielokrot-
nego stosowania powinny trafić do prototypu, a nie do this. Z tego powodu dla celów dzie-
dziczenia wszystko, co warto byłoby dziedziczyć, powinno znajdować się w prototypie.
Można by więc ustawić prototyp potomka na prototyp przodka:
function inherit(C, P) {
C.prototype = P.prototype;
}

Takie podejście zapewnia krótki i szybki łańcuch prototypów, ponieważ wszystkie obiekty
współdzielą ten sam prototyp. Oczywiście powyższe rozwiązanie ma i wadę: jeśli dowolny
potomek z łańcucha prototypów zmieni prototyp, zauważą to wszystkie obiekty, włączając
w to przodka.

Czwarty wzorzec klasyczny — współdzielenie prototypu | 123


Rysunek 6.7 przedstawia sytuację, w której przodek i potomek stosują ten sam prototyp i uzy-
skują dostęp do tej samej metody say(). Obiekty potomne nie dziedziczą właściwości name.

Rysunek 6.7. Związki między obiektami współdzielącymi ten sam prototyp

Piąty wzorzec klasyczny — konstruktor tymczasowy


Następny wzorzec stara się rozwiązać ten sam problem, usuwając bezpośrednie powiązanie
między prototypami przodka i potomka przy jednoczesnym wykorzystaniu faktu istnienia
łańcucha prototypów.
Poniżej znajduje się implementacja tego wzorca, która zawiera pustą funkcję F(). Funkcja
służy jako pomost między potomkiem i przodkiem. Jej prototyp wskazuje na prototyp
przodka. Prototyp potomka jest instancją pustej funkcji:
function inherit(C, P) {
var F = function () {};
F.prototype = P.prototype;
C.prototype = new F();
}

Wzorzec zachowuje się nieco inaczej niż wzorzec domyślny (pierwszy wzorzec klasyczny),
ponieważ potomek dziedziczy jedynie właściwości prototypu (patrz rysunek 6.8).

Rysunek 6.8. Dziedziczenie klasyczne wykorzystujące konstruktor pośredniczący F()

124 | Rozdział 6. Wzorce wielokrotnego użycia kodu


Opisany efekt końcowy nie jest niczym złym, a nawet jest preferowany, ponieważ to właśnie
prototyp powinien stanowić miejsce umieszczania użytecznej funkcjonalności do wielokrot-
nego stosowania. W tym wzorcu nie są dziedziczone żadne właściwości dodane do this
przez konstruktor przodka.
Utwórzmy nowy obiekt potomny i sprawdźmy jego zachowanie:
new kid = new Child();

Próba uzyskania dostępu do kid.name spowoduje zwrócenie wartości undefined. W tym


przypadku name to własna właściwość przodka, a w trakcie dziedziczenia new Parent()
nigdy nie zostało wywołane, więc właściwość nie miała nawet szansy powstać. Próba wy-
wołania kid.say() spowoduje wyszukiwanie metody w obiekcie numer trzy. Ponieważ nie
zostanie ona tam odnaleziona, wyszukiwanie przejdzie w górę łańcucha prototypów. Obiekt
numer cztery również nie zawiera takiej metody. Po kolejnym przejściu metoda zostanie od-
naleziona w obiekcie numer jeden w miejscu w pamięci współdzielonym przez inne kon-
struktory dziedziczące po Parent() i same obiekty utworzone za pomocą konstruktora Parent.

Zapamiętywanie klasy nadrzędnej


Na podstawie poprzedniego wzorca można by dodać referencję do oryginalnego przodka.
To jak posiadanie dostępu do klasy nadrzędnej w innych językach programowania, które
czasem okazuje się przydatne.
Powszechnie stosowaną nazwą właściwości jest uber, ponieważ „super” jest słowem zare-
zerwowanym, a „superclass” mogłoby dawać programiście złudzenie, że język JavaScript
zawiera klasy. Oto poprawiona implementacja wzorca klasycznego:
function inherit(C, P) {
var F = function () {};
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype;
}

Czyszczenie referencji na konstruktor


Ostatnim elementem do dodania do tej prawie idealnej funkcji dziedziczenia klasycznego jest
wyczyszczenie referencji na funkcję konstruującą, na wypadek gdyby była ona potrzebna
ponownie.
Jeśli to wyczyszczenie nie nastąpi, wszystkie obiekty potomne będą informowały, że
Parent() jest ich konstruktorem, co nie jest zbyt użyteczne. Stosując wcześniejszą imple-
mentację inherit(), uzyskamy efekt opisany w poprzednim zdaniu.
// przodek, potomek, dziedziczenie
function Parent() {}
function Child() {}
inherit(Child, Parent);

// testy
var kid = new Child();
kid.constructor.name; // "Parent"
kid.constructor === Parent; // true

Piąty wzorzec klasyczny — konstruktor tymczasowy | 125


Właściwość constructor nie jest stosowana często, ale przydaje się do sprawdzania kon-
struktorów obiektów w trakcie działania aplikacji. Można ją „wyczyścić”, czyli przypisać jej
właściwy konstruktor, bez wywierania jakiegokolwiek wpływu na funkcjonalność (właści-
wość ta ma w zasadzie jedynie zastosowanie informacyjne).
Ostateczna, idealna wersja wzorca dziedziczenia klasycznego mogłaby mieć postać:
function inherit(C, P) {
var F = function () {};
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype;
C.prototype.constructor = C;
}

Funkcja podobna do przedstawionej istnieje w bibliotece YUI i prawdopodobnie w wielu


innych bibliotekach, które mają zapewnić dziedziczenie klasyczne w języku nieposiadającym
klas. Oczywiście zakładamy, że ktoś zdecyduje, iż jest to dla niego najlepsze rozwiązanie.

Wzorzec ten nazywa się często funkcją pośredniczącą lub konstruktorem pośredni-
czącym, a nie konstruktorem tymczasowym, ponieważ konstruktor tymczasowy
służy jako pośrednik w uzyskiwaniu prototypu przodka.

Typową optymalizacją przedstawionego wzorca idealnego jest uniknięcie tworzenia konstruk-


tora pośredniczącego przy każdym dziedziczeniu. Wystarczy utworzyć go raz i zmieniać jego
prototyp. Należy w tym celu użyć funkcji natychmiastowej i zapamiętać funkcję pośredniczącą
w domknięciu.
var inherit = (function () {
var F = function () {};
return function (C, P) {
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype;
C.prototype.constructor = C;
}
}());

Podejście klasowe
Wiele bibliotek JavaScript emuluje klasy, wprowadzając dodatkową składnię. Implementacje
różnią się między sobą, ale najczęściej mają kilka cech wspólnych:
• Istnieje pewna konwencja nazywania metod traktowanych jako konstruktory klas (na przy-
kład initialize lub _init), by możliwe było ich automatyczne wywołanie.
• Klasy dziedziczą po innych klasach.
• Istnieje możliwość dostępu do klasy nadrzędnej z poziomu klasy podrzędnej.

W tym jednym fragmencie rozdziału słowo „klasa” będzie wyjątkowo pojawiało się
bardzo często, bo naszym zadaniem jest emulacja klas.

126 | Rozdział 6. Wzorce wielokrotnego użycia kodu


Bez wdawania się w zbyt wiele szczegółów przyjrzyjmy się przykładowej implementacji
emulacji klas w języku JavaScript. Najpierw zobaczymy sposób tworzenia klas z perspektywy
użytkownika emulatora.
var Man = klass(null, {
__construct: function (what) {
console.log("Konstruktor klasy Man");
this.name = what;
},
getName: function () {
return this.name;
}
});

Dodatkowa składnia to tak naprawdę funkcja o nazwie klass(). W niektórych implementa-


cjach pojawia się konstruktor Klass() lub zmodyfikowany prototyp Object.prototype, ale
na potrzeby przykładu załóżmy użycie prostej funkcji.
Funkcja przyjmuje dwa parametry: klasę przodka wykorzystywaną przy dziedziczeniu i im-
plementację nowej klasy zapewnianą przez literał obiektu. Zastosujmy konwencję znaną z ję-
zyka PHP i załóżmy, że konstruktor klasy musi nosić nazwę __construct. W powyższym
przykładzie powstała nowa klasa Man, która nie dziedziczy po żadnej klasie (w rzeczywisto-
ści będzie dziedziczyła po Object). Ma ona własną właściwość name utworzoną wewnątrz
__construct oraz metodę getName(). Klasa jest funkcją konstruującą, więc poniższy kod za-
działa prawidłowo (wygląda jak utworzenie instancji klasy).
var first = new Man('Adam'); // wyświetla "Konstruktor klasy Man"
first.getName(); // "Adam"

Teraz rozszerzmy klasę, dodając klasę SuperMan.


var SuperMan = klass(Man, {
__construct: function (what) {
console.log("Konstruktor klasy SuperMan");
},
getName: function () {
var name = SuperMan.uber.getName.call(this);
return "Jestem " + name;
}
});

Pierwszym parametrem funkcji klass() jest Man, czyli klasa, po której chcemy dziedziczyć.
Co więcej, metoda getName() korzysta z metody getName() klasy nadrzędnej, używając w tym
celu właściwości statycznej uber klasy SuperMan. Krótki test:
var clark = new SuperMan('Clark Kent');
clark.getName(); // "Jestem Clark Kent"

Dodatkowo jako wynik wykonania pierwszego wiersza kodu w konsoli pojawią się dwa teksty:
„Konstruktor klasy Man” i „Konstruktor klasy SuperMan”. W niektórych językach konstruktor
przodka jest wywoływany automatycznie przy każdym wywołaniu konstruktora potomka, więc
dlaczego by tego nie zasymulować?
Operator instanceof zwraca wynik zgodny z oczekiwaniami:
clark instanceof Man; // true
clark instanceof SuperMan; // true

Podejście klasowe | 127


Nadszedł czas na implementację funkcji klass().
var klass = function (Parent, props) {

var Child, F, i;

// 1.
// nowy konstruktor
Child = function () {
if (Child.uber && Child.uber.hasOwnProperty("__construct")) {
Child.uber.__construct.apply(this, arguments);
}
if (Child.prototype.hasOwnProperty("__construct")) {
Child.prototype.__construct.apply(this, arguments);
}
};

// 2.
// dziedziczenie
Parent = Parent || Object;
F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.uber = Parent.prototype;
Child.prototype.constructor = Child;

// 3.
// dodanie metod implementacji
for (i in props) {
if (props.hasOwnProperty(i)) {
Child.prototype[i] = props[i];
}
}

// zwrócenie "klasy"
return Child;
};

Implementacja klass() zawiera trzy interesujące części:


1. W pierwszej powstaje funkcja konstruująca Child(). Zostanie ona zwrócona na końcu
funkcji i będzie wykorzystywana jako klasa. W funkcji tej, jeśli tylko istnieje, wykonywana
jest metoda __construct. Co więcej, wywoływana jest też metoda __construct przodka
(o ile istnieje) za pomocą właściwości statycznej uber. Istnieją sytuacje, w których uber może
nie być zdefiniowane, na przykład w momencie dziedziczenia po Object w definicji klasy Man.
2. Druga część zajmuje się dziedziczeniem, które w zasadzie oparte jest na idealnym wzor-
cu dziedziczenia klasycznego opisanym w poprzednim podrozdziale. Jedyny nowy ele-
ment to ustawienie Parent na Object, jeśli w Parent nie przekazano istniejącego obiektu.
3. Ostatnia część to przejście w pętli przez wszystkie metody implementacji (na przykład
__construct i getName) stanowiące rzeczywistą definicję klasy i dodanie ich do pro-
totypu Child.
Kiedy można wykorzystać podobny wzorzec? W zasadzie najlepiej byłoby go unikać, ponie-
waż niesie on ze sobą wiele nieporozumień związanych z klasami, które przecież tak na-
prawdę w tym języku nie istnieją. Co więcej, dodaje nową składnię i nowe zasady, których
trzeba się nauczyć. Jeżeli jednak zespół bardziej komfortowo czuje się, stosując klasy, i nie
przepada za prototypami, podejście to może okazać się warte rozważenia. Wzorzec pozwala
całkowicie zapomnieć o prototypach i daje szansę na dostosowanie składni i konwencji do
rozwiązań stosowanych w innych językach.

128 | Rozdział 6. Wzorce wielokrotnego użycia kodu


Dziedziczenie prototypowe
Opis tak zwanych wzorców nowoczesnych rozpoczniemy od wzorca o nazwie dziedziczenie
prototypowe. To wzorzec, w którym nie ma klas, a obiekty dziedziczą po innych obiektach.
Zasada jest następująca: istnieje obiekt, który chcielibyśmy wykorzystać ponownie, więc two-
rzymy nowy obiekt o funkcjonalności pozyskanej od jego pierwowzoru.
Oto przykładowy sposób zdefiniowania i użycia drugiego obiektu:
// obiekt, z którego dziedziczymy
var parent = {
name: "Ojciec"
};

// nowy obiekt
var child = object(parent);

// test
alert(child.name); // "Ojciec"

W powyższym kodzie pojawia się istniejący obiekt o nazwie parent utworzony za pomocą
literału obiektu i na jego podstawie tworzony jest inny obiekt o nazwie child, który ma mieć
takie same właściwości i metody jak przodek. Nowy obiekt powstaje na skutek użycia funkcji
object(). W języku JavaScript taka funkcja nie istnieje (nie należy mylić jej z funkcją kon-
struującą Object()), ale zastanówmy się, jak można by ją zdefiniować.
Podobnie jak w przypadku klasycznego ideału, użyjemy pustego konstruktora tymczasowego
o nazwie F(). Jako prototyp F() ustawimy obiekt przodka. Następnie zwrócimy nową instancję
konstruktora tymczasowego.
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

Rysunek 6.9 przedstawia łańcuch prototypów po zastosowaniu wzorca dziedziczenia proto-


typowego. W tym rozwiązaniu child zawsze jest pustym obiektem, który sam nie ma żadnych
właściwości i całą funkcjonalność dziedziczy po przodku za pomocą referencji __proto__.

Rysunek 6.9. Wzorzec dziedziczenia prototypowego

Dyskusja
We wzorcu dziedziczenia prototypowego przodek nie musi być tworzony za pomocą notacji
literałowej (choć to najczęstsza forma). Może on też powstać dzięki użyciu funkcji konstru-
ującej, ale w takiej sytuacji odziedziczone zostaną zarówno własne właściwości obiektu, jak
i właściwości zdefiniowane w prototypie konstruktora.

Dziedziczenie prototypowe | 129


// konstruktor przodka
function Person() {
// właściwość własna
this.name = "Adam";
}
// właściwość dodana do prototypu
Person.prototype.getName = function () {
return this.name;
};

// utworzenie nowego obiektu


var papa = new Person();
// dziedziczenie
var kid = object(papa);

// sprawdzenie zarówno właściwości własnej,


// jak i z prototypu
kid.getName(); // "Adam"

W odmianie wzorca dziedziczy się jedynie obiekt prototypu istniejącego konstruktora. Obiekty
dziedziczą po innych obiektach niezależnie od tego, jak te były tworzone. Oto poprzedni
przykład po pewnych modyfikacjach:
// konstruktor przodka
function Person() {
// właściwość własna
this.name = "Adam";
}
// właściwość dodana do prototypu
Person.prototype.getName = function () {
return this.name;
};

// dziedziczenie
var kid = object(Person.prototype);

typeof kid.getName; // "function", ponieważ znajduje się w prototypie


typeof kid.name; // "undefined", ponieważ dziedziczenie obejmuje tylko prototyp

Dodatki do standardu ECMAScript 5


W specyfikacji ECMAScript 5 wzorzec dziedziczenia prototypowego stał się oficjalną częścią
języka. Implementuje go metoda Object.create(). Oznacza to, że nie trzeba tworzyć własnej
funkcji podobnej do object(), bo została ona wbudowana w język:
var child = Object.create(parent);

Funkcja Object.create() przyjmuje dodatkowy parametr — obiekt. Właściwości tego do-


datkowego obiektu zostaną dodane jako własne właściwości nowo tworzonego obiektu po-
tomnego. To dodatkowe usprawnienie, które pozwala jednocześnie dziedziczyć i definiować
obiekt potomny. Oto krótki przykład użycia funkcji:
var child = Object.create(parent, {
age: { value: 2 } // deskryptor ECMA5
});
child.hasOwnProperty("age"); // true

Wzorzec dziedziczenia prototypowego został również zaimplementowany w bibliotekach


JavaScript. W bibliotece YUI3 jest to metoda Y.Object():
YUI().use('*', function (Y) {
var child = Y.Object(parent);
});

130 | Rozdział 6. Wzorce wielokrotnego użycia kodu


Dziedziczenie przez kopiowanie właściwości
Przyjrzyjmy się jeszcze jednemu wzorcowi dziedziczenia — dziedziczeniu przez kopiowanie
właściwości. W tym wzorcu obiekt uzyskuje funkcjonalność od innego obiektu, po prostu
kopiując jego zawartość. Oto prosta implementacja funkcji extend() wykonującej to zadanie:
function extend(parent, child) {
var i;
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
child[i] = parent[i];
}
}
return child;
}

Ta prosta implementacja przechodzi w pętli przez wszystkie składowe przodka i zwyczajnie


je kopiuje. child jest w niej opcjonalne — jeśli nie zostanie przekazany istniejący obiekt do
zmodyfikowania, funkcja utworzy i zwróci nowy obiekt:
var dad = {name: "Adam"};
var kid = extend(dad);
kid.name; // "Adam"

Zaprezentowana implementacja to tak zwana płytka kopia obiektu. Kopia głęboka oznacza-
łaby dodatkowe sprawdzenie, czy właściwość jest obiektem lub tablicą, i jeśli jest, rekuren-
cyjne przejście przez jej elementy w celu ich skopiowania. W przypadku kopii płytkiej zmiana
właściwości obiektu potomnego, która jest obiektem, spowoduje również identyczną zmianę
u przodka (ponieważ obiekty w języku JavaScript są przekazywane przez referencję). Roz-
wiązanie to jest odpowiednie dla metod (funkcje również są obiektami, więc są przekazywane
referencyjnie), ale może prowadzić do przykrych niespodzianek w przypadku obiektów
i tablic. Oto przykład takiej sytuacji:
var dad = {
counts: [1, 2, 3],
reads: {paper: true}
};
var kid = extend(dad);
kid.counts.push(4);
dad.counts.toString(); // "1,2,3,4"
dad.reads === kid.reads; // true

Zmodyfikujmy funkcję extend(), by obsługiwała również kopie głębokie. Wystarczy tylko


sprawdzić, czy właściwość jest obiektem, a jeśli tak, rekurencyjnie skopiować jej właściwości.
Dla obiektów trzeba jeszcze sprawdzić, czy to zwykły obiekt, czy może tablica. W tym celu
wykorzystamy test opisany w rozdziale 3. Wersja extend() obsługująca kopie głębokie wy-
gląda następująco:
function extendDeep(parent, child) {
var i,
toStr = Object.prototype.toString,
astr = "[object Array]";

child = child || {};

for (i in parent) {
if (parent.hasOwnProperty(i)) {
if (typeof parent[i] === "object") {

Dziedziczenie przez kopiowanie właściwości | 131


child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
extendDeep(parent[i], child[i]);
} else {
child[i] = parent[i];
}
}
}
return child;
}

Nowa implementacja zapewnia prawdziwe kopie obiektów, więc potomkowie nie mogą
zmodyfikować swoich przodków.
var dad = {
counts: [1, 2, 3],
reads: {paper: true}
};

var kid = extendDeep(dad);


kid.counts.push(4);
kid.counts.toString(); // "1,2,3,4"
dad.counts.toString(); // "1,2,3"

dad.reads === kid.reads; // false


kid.reads.paper = false;
kid.reads.web = true;
dad.reads.paper; // true

Wzorzec kopiowania właściwości jest bardzo prosty i często stosowany. Można go znaleźć
między innymi w dodatku Firebug (rozszerzenia dla przeglądarki Firefox pisze się w języku
JavaScript) zapewniającym metodę extend() wykonującą płytką kopię. Biblioteka jQuery
stosuje funkcję o tej samej nazwie, która wykonuje kopie płytkie i głębokie. YUI3 oferuje
metodę Y.clone() tworzącą kopię głęboką oraz oferuje kopiowanie funkcji przez ich dowią-
zywanie do obiektu potomnego (więcej informacji na ten temat w dalszej części rozdziału).
Warto jeszcze raz podkreślić, że w tym wzorcu nie występują prototypy — dotyczy on tylko
i wyłącznie obiektów i ich własnych właściwości.

Wzorzec wmieszania
Pomysł dziedziczenia właściwości przez ich kopiowanie można rozwinąć, stosując tak zwany
wzorzec wmieszania (mix-in). Zamiast kopiować z jednego obiektu, kopiuje się z dowolnej
ich liczby i miesza się je wszystkie w jednym nowym obiekcie.
Implementacja tego wzorca jest bardzo prosta — wystarczy przejść w pętli przez wszystkie
argumenty i skopiować wszystkie właściwości każdego z obiektów przekazanych do funkcji.
function mix() {
var arg, prop, child = {};
for (arg = 0; arg < arguments.length; arg += 1) {
for (prop in arguments[arg]) {
if (arguments[arg].hasOwnProperty(prop)) {
child[prop] = arguments[arg][prop];
}
}
}
return child;
}

132 | Rozdział 6. Wzorce wielokrotnego użycia kodu


Ta bardzo ogólna funkcja mix() umożliwia przekazanie do niej dowolnej liczby obiektów,
a wynikiem będzie nowy obiekt, który będzie miał właściwości wszystkich obiektów źró-
dłowych. Oto przykład jej użycia:
var cake = mix(
{eggs: 2, large: true},
{butter: 1, salted: true},
{flour: "3 szklanki"},
{sugar: "tak!"}
);

Rysunek 6.10 przedstawia wynik wykonania polecenia console.dir(cake) w konsoli Firebug


po utworzeniu obiektu cake.

Rysunek 6.10. Sprawdzenie zawartości obiektu cake w konsoli Firebug

Jeżeli znamy koncepcję wmieszania nowych elementów w inne obiekty z języków,


w których stanowi ona oficjalną część składni, możemy oczekiwać, że zmiana jednego
lub kilku przodków spowoduje zmianę potomka. W zaprezentowanej implementacji
nie będzie to miało miejsca. Pętla po prostu kopiuje właściwości i niszczy powiązanie
z przodkiem.

Pożyczanie metod
Czasem zdarza się, że z istniejącego obiektu potrzeba jedynie jednej lub dwóch metod. Choć
chcemy z nich skorzystać, nie chcemy tworzyć związku przodek – potomek między obiektami.
Zależy nam tylko na wybranych metodach, a nie na wszystkich znajdujących się w oryginal-
nym obiekcie. Zadanie to wykonamy za pomocą wzorca pożyczania metod, który korzysta
z metod call() i apply(). Rozwiązanie to pojawiło się już w kilku miejscach w książce,
a nawet w tym rozdziale w implementacji funkcji extendDeep().
Jak wiadomo, funkcje w języku JavaScript to obiekty, które zawierają kilka interesujących
metod, w tym call() i apply(). Jedyna różnica między tymi metodami polega na sposobie
przyjmowania argumentów: pierwsza przyjmuje zwykłe argumenty wymieniane jeden po
drugim, a druga przyjmuje wszystkie argumenty jako jedną tablicę wartości. Metody te mogą
w bardzo prosty sposób posłużyć do pożyczenia funkcjonalności od innych obiektów:
// przykład użycia call()
notmyobj.doStuff.call(myobj, param1, p2, p3);
// przykład użycia apply()
notmyobj.doStuff.apply(myobj, [param1, p2, p3]);

Istnieje tu utworzony przez nas obiekt myobj, a także inny obiekt notmyobj zawierający uży-
teczną metodę doStuff(). Zamiast dziedziczyć po tym obiekcie i być może uzyskać wiele
innych niepotrzebnych metod, po prostu tymczasowo dziedziczymy tylko doStuff().

Pożyczanie metod | 133


W wywołaniu przekazujemy własny obiekt i wszystkie parametry. Pożyczona metoda będzie
zawierała w swoim this referencję do naszego obiektu. Można powiedzieć, że na potrzeby
wykonania zadania oszukujemy metodę, że this to jej standardowy obiekt, choć w rzeczy-
wistości jest inaczej. Przypomina to dziedziczenie, choć bez płacenia związanej z nim ceny
(w postaci dodatkowych parametrów i metod, które nie są potrzebne).

Przykład — pożyczenie metody od obiektu Array


Bardzo często stosuje się pożyczanie metod od obiektów tablic.
Tablice zawierają użyteczne metody, których nie posiadają przypominające je obiekty takie
jak arguments. Nic jednak nie stoi na przeszkodzie, by obiekt arguments pożyczył od tablicy
na przykład metodę slice().
function f() {
var args = [].slice.call(arguments, 1, 3);
return args;
}

// przykład
f(1, 2, 3, 4, 5, 6); // zwraca [2,3]

W zaprezentowanym przykładzie pusta tablica powstaje tylko po to, by można było wywo-
łać jej metodę. Nieco dłuższym rozwiązaniem, które jednak nie wymaga niepotrzebnego
tworzenia tablicy, jest bezpośrednie pożyczenie metody od prototypu za pomocą konstrukcji
Array.prototype.slice.call(...). Mimo dłuższego zapisu jest to preferowane rozwiązanie.

Pożyczenie i przypisanie
Gdy pożycza się metodę za pomocą call() lub apply() albo przy użyciu prostego przypi-
sania, obiekt wskazywany przez this wewnątrz pożyczanej metody zależy od przekazanego
argumentu. Czasem jednak warto „zablokować” this na jednej, z góry określonej wartości.
Zobaczmy to na przykładzie. Istnieje obiekt o nazwie one zawierający metodę say():
var one = {
name: "obiekcie",
say: function (greet) {
return greet + ", " + this.name;
}
};

// test
one.say('Witaj'); // "Witaj, obiekcie"

Inny obiekt o nazwie two nie posiada metody say(), ale może ją pożyczyć od one.
var two = {
name: "inny obiekcie"
};

one.say.apply(two, ['Witaj']); // "Witaj, inny obiekcie"

W przykładzie tym this wewnątrz say() wskazuje na two, więc this.name zwróciło wartość
inny obiekt. Co dzieje się w sytuacjach, w których obiekt funkcji zostaje wpisany do zmien-
nej globalnej lub funkcja trafia do innej funkcji jako wywołanie zwrotne? W programowaniu
po stronie klienta istnieje wiele zdarzeń i wywołań zwrotnych, więc poniższa sytuacja za-
chodzi stosunkowo często.

134 | Rozdział 6. Wzorce wielokrotnego użycia kodu


// przypisanie do zmiennej
// this wskazuje na obiekt globalny
var say = one.say;

say('Hej'); // "Hej, undefined"

// przekazanie w postaci wywołania zwrotnego


var yetanother = {
name: "kolejny obiekcie",
method: function (callback) {
return callback('Cześć');
}
};
yetanother.method(one.say); // "Cześć, undefined"

W obu przypadkach this wewnątrz say() wskazuje na obiekt globalny i cały przykład nie
działa zgodnie z oczekiwaniami. Aby rozwiązać problem, czyli powiązać obiekt z metodą,
wystarczy bardzo prosta funkcja:
function bind(o, m) {
return function () {
return m.apply(o, [].slice.call(arguments));
};
}

Funkcja bind() przyjmuje obiekt o i metodę m, a następnie łączy je ze sobą i zwraca nową
metodę. Zwrócona funkcja ma dostęp do o i m dzięki domknięciu. Oznacza to, że nawet po
wykonaniu bind() będzie ona pamiętała o i m, więc będzie mogła wywołać oryginalny obiekt
i oryginalną metodę. Utwórzmy nową funkcję za pomocą bind():
var twosay = bind(two, one.say);
twosay('Witaj'); // "Witaj, inny obiekcie"

Choć funkcja twosay() jest funkcją globalną, this nie wskazuje na obiekt globalny, ale na
obiekt two, który został przekazany do bind(). Niezależnie od sposobu wywołania funkcji
twosay() this będzie zawsze wskazywało na two.

Ceną, którą trzeba zapłacić za posiadanie dowiązania, jest dodatkowe domknięcie.

Metoda Function.prototype.bind()
ECMAScript 5 dodaje metodę bind() do Function.prototype, co umożliwia stosowanie jej
w tak samo prosty sposób jak metody apply() i call(). Oto przykład jej użycia:
var newFunc = obj.someFunc.bind(myobj, 1, 2, 3);

Powyższy wiersz kodu wiąże ze sobą someFunc() i myobj i dodatkowo wstępnie wypełnia
trzy pierwsze argumenty funkcji someFunc(). To przykład aplikacji częściowej opisanej do-
kładniej w rozdziale 4.
Poniżej znajduje się przykładowa implementacja Function.prototype.bind() w środowisku,
które nie wspiera jeszcze rozwiązań wprowadzonych w standardzie ECMAScript 5.
if (typeof Function.prototype.bind === "undefined") {
Function.prototype.bind = function (thisArg) {
var fn = this,
args = slice.call(arguments, 1);
return function () {
return fn.apply(thisArg, args.concat(slice.call(arguments)));
};
};
}

Pożyczanie metod | 135


Implementacja ta zapewne wygląda znajomo, bo wykorzystuje aplikację częściową i łączenie
list argumentów — tych przekazanych do bind() (poza argumentem pierwszym) i tych
przekazanych w momencie wywoływania funkcji zwróconej przez bind(). Oto przykład jej
użycia:
var twosay2 = one.say.bind(two);
twosay2('Bonjour'); // "Bonjour, inny obiekcie"

Do metody bind() nie zostały tu przekazane żadne dodatkowe argumenty poza dowiązy-
wanym obiektem. Następny przykład wykorzystuje aplikację częściową.
var twosay3 = one.say.bind(two, 'Enchanté');
twosay3(); // "Enchanté, inny obiekcie"

Podsumowanie
Dziedziczenie w języku JavaScript można przeprowadzić na wiele sposobów. Warto prze-
analizować i zrozumieć różne wzorce, by lepiej poznać sam język. W niniejszym rozdziale
przedstawionych zostało kilka wzorców klasycznych i kilka nowoczesnych.
Z drugiej strony dziedziczenie nie jest problemem, z którym każdy styka się w trakcie prac
programistycznych. Częściowo wynika to z faktu, iż jest on już rozwiązany w taki czy inny
sposób w wielu bibliotekach, a częściowo z faktu, iż w języku JavaScript rzadko zachodzi
potrzeba tworzenia długich i złożonych łańcuchów dziedziczenia. W językach ze statyczną
kontrolą typów dziedziczenie często jest jedynym sposobem wielokrotnego wykorzystania
kodu. JavaScript często oferuje prostsze i bardziej eleganckie rozwiązania, włączając w to po-
życzanie metod, ich dowiązywanie, kopiowanie właściwości, a nawet mieszanie właściwości
z kilku obiektów.
Dziedziczenie nie powinno być celem samym w sobie, bo stanowi tylko jeden ze sposobów
osiągnięcia rzeczywistego celu — wielokrotnego użycia tego samego kodu.

136 | Rozdział 6. Wzorce wielokrotnego użycia kodu


ROZDZIAŁ 7.

Wzorce projektowe

Wzorce projektowe opisane w książce tak zwanego gangu czworga oferują rozwiązania ty-
powych problemów związanych z projektowaniem oprogramowania zorientowanego obiektowo.
Są dostępne już od jakiegoś czasu i sprawdziły się w wielu różnych sytuacjach, warto więc
się z nimi zapoznać i poświęcić im nieco czasu.
Choć same te wzorce projektowe nie są uzależnione od języka programowania i implementa-
cji, były analizowane przez wiele lat głównie z perspektywy języków o silnym sprawdzaniu
typów i statycznych (niezmiennych) klasach takich jak Java lub C++.
JavaScript jest językiem o luźnej kontroli typów i bazuje na prototypach (a nie klasach), więc nie-
które z tych wzorców okazują się wyjątkowo proste, a czasem wręcz banalne w implementacji.
Zacznijmy od przykładu sytuacji, w której w języku JavaScript rozwiązanie wygląda inaczej
niż w przypadku języków statycznych bazujących na klasach, czyli od wzorca singletonu.

Singleton
Wzorzec singletonu ma w założeniu zapewnić tylko jedną instancję danej klasy. Oznacza to,
że próba utworzenia obiektu danej klasy po raz drugi powinna zwrócić dokładnie ten sam
obiekt, który został zwrócony za pierwszym razem.
Jak zastosować ten wzorzec w języku JavaScript? Nie mamy przecież klas, a jedynie obiekty.
Gdy powstaje nowy obiekt, nie ma w zasadzie drugiego identycznego, więc jest on automa-
tycznie singletonem. Utworzenie prostego obiektu za pomocą literału to doskonały przykład
utworzenia singletonu.
var obj = {
myprop: 'wartość'
};

W JavaScripcie obiekty nie są sobie równe, jeśli nie są dokładnie tym samym obiektem,
więc nawet jeśli utworzy się dwa identyczne obiekty z takimi samymi wartościami, nie
będą równoważne.
var obj2 = {
myprop: 'wartość'
};
obj === obj2; // false
obj == obj2; // false

137
Można więc stwierdzić, że za każdym razem, gdy powstaje nowy obiekt tworzony za pomocą
literału, powstaje nowy singleton, i to bez użycia dodatkowej składni.

Czasem gdy ludzie mówią „singleton” w kontekście języka JavaScript, mają na myśli
wzorzec modułu opisany w rozdziale 5.

Użycie słowa kluczowego new


JavaScript jest językiem niestosującym klas, więc dosłowna definicja singletonu nie ma tu za-
stosowania. Z drugiej strony język posiada słowo kluczowe new, które tworzy obiekty na
podstawie funkcji konstruujących. Czasem tworzenie ich w ten sposób jako singletonów mo-
że być ciekawym podejściem. Ogólny pomysł jest następujący: kilkukrotne wywołanie funkcji
konstruującej z użyciem new powinno spowodować każdorazowo zwrócenie dokładnie tego
samego obiektu.

Przedstawiony poniżej opis nie jest użyteczny w praktyce. Stanowi raczej teoretyczne
wyjaśnienie powodów powstania wzorca w językach statycznych o ścisłej kontroli
typów, w których to funkcje nie są pełnoprawnymi obiektami.

Poniższy przykład ilustruje oczekiwane zachowanie (pod warunkiem że nie wierzy się
w światy równoległe i akceptuje się tylko jeden).
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true

W tym przykładzie uni tworzone jest tylko przy pierwszym wywołaniu konstruktora. Drugie
i kolejne wywołania zwracają ten sam obiekt. Dzięki temu uni === uni2 (to dokładnie ten
sam obiekt). Jak osiągnąć taki efekt w języku JavaScript?
Konstruktor Universe musi zapamiętać instancję obiektu (this), gdy zostanie utworzona po
raz pierwszy, a następnie zwracać ją przy kolejnych wywołaniach. Istnieje kilka sposobów,
by to uzyskać.
• Wykorzystanie zmiennej globalnej do zapamiętania instancji. Nie jest to zalecane podej-
ście, bo zmienne globalne należy tworzyć tylko wtedy, gdy jest to naprawdę niezbędne.
Co więcej, każdy może nadpisać taką zmienną, także przez przypadek. Na tym zakończmy
rozważania dotyczące tej wersji.
• Wykorzystanie właściwości statycznej konstruktora. Funkcje w języku JavaScript są
obiektami, więc mają właściwości. Można by utworzyć właściwość Universe.instance
i to w niej przechowywać obiekt. To eleganckie rozwiązanie, ale ma jedną wadę: właści-
wość instance byłaby dostępna publicznie i inny kod mógłby ją zmienić.
• Zamknięcie instancji w domknięciu. W ten sposób instancja staje się elementem prywatnym
i nie może zostać zmieniona z zewnątrz. Ceną tego rozwiązania jest dodatkowe domknięcie.
Przyjrzyjmy się przykładowym implementacjom drugiej i trzeciej opcji.

138 | Rozdział 7. Wzorce projektowe


Instancja we właściwości statycznej
Poniższy kod zapamiętuje pojedynczą instancję we właściwości statycznej konstruktora
Universe.
function Universe() {

// czy istnieje już instancja?


if (typeof Universe.instance === "object") {
return Universe.instance;
}

// standardowe działania
this.start_time = 0;
this.bang = "Wielki";

// zapamiętanie instancji
Universe.instance = this;

// niejawna instrukcja return:


// return this;
}

// test
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true

To bardzo proste rozwiązanie z jedną wadą, którą jest publiczne udostępnienie instance.
Choć prawdopodobieństwo zmiany takiej właściwości przez kod jest niewielkie (i na pewno
znacząco mniejsze niż w przypadku zmiennej globalnej), to jednak jest to możliwe.

Instancja w domknięciu
Innym sposobem uzyskania singletonu podobnego do rozwiązań klasowych jest użycie
domknięcia w celu ochrony instancji. W implementacji można wykorzystać wzorzec prywat-
nej składowej statycznej omówiony w rozdziale 5. Tajnym składnikiem jest nadpisanie kon-
struktora.
function Universe() {

// zapamiętanie instancji
var instance = this;

// standardowe działania
this.start_time = 0;
this.bang = "Wielki";

// nadpisanie konstruktora
Universe = function () {
return instance;
};
}

// testy
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true

Singleton | 139
Za pierwszym razem zostaje wywołany oryginalny konstruktor, który zwraca this w sposób
standardowy. Drugie i następne wywołania wykonują już zmieniony konstruktor, który ma
dostęp do zmiennej prywatnej instance dzięki domknięciu i po prostu ją zwraca.
Przedstawiona implementacja jest w zasadzie przykładem wzorca samomodyfikującej się
funkcji z rozdziału 4. Wadą tego rozwiązania opisaną we wspomnianym rozdziale jest to, że
nadpisana funkcja (w tym przypadku konstruktor Universe()) utraci wszystkie właściwości
dodane między jej zdefiniowaniem i nadpisaniem. W tej konkretnej sytuacji nic z tego, co zo-
stanie dodane do prototypu Universe() po pierwszym obiekcie, nie będzie mogło posiadać
referencji do instancji utworzonej przez oryginalną implementację.
Dla uwidocznienia problemu wykonajmy krótki test. Najpierw kilka wierszy przygotowujących:
// dodanie właściwości do prototypu
Universe.prototype.nothing = true;

var uni = new Universe();

// ponowne dodanie właściwości do prototypu


// po utworzeniu pierwszego obiektu
Universe.prototype.everything = true;

var uni2 = new Universe();

Oto właściwy test:


// tylko oryginalny prototyp jest powiązany z obiektami
uni.nothing; // true
uni2.nothing; // true
uni.everything; // undefined
uni2.everything; // undefined

// wygląda prawidłowo:
uni.constructor.name; // "Universe"

// ale to jest dziwne:


uni.constructor === Universe; // false

Powodem, dla którego właściwość uni.constructor nie jest już taka sama jak konstruktor
Universe(), jest fakt, iż uni.constructor nadal wskazuje na oryginalny konstruktor zamiast
przedefiniowanego.
Jeśli prototyp i referencja wskazująca na konstruktor muszą działać prawidłowo, do wcze-
śniejszej implementacji trzeba wprowadzić kilka poprawek.
function Universe() {

// zapamiętanie instancji
var instance;

// nadpisanie konstruktora
Universe = function Universe() {
return instance;
};

// przeniesienie właściwości prototypu


Universe.prototype = this;

// instancja
instance = new Universe();

// zmiana referencji wskazującej na konstruktor


instance.constructor = Universe;

140 | Rozdział 7. Wzorce projektowe


// właściwa funkcjonalność
instance.start_time = 0;
instance.bang = "Wielki";

return instance;
}

Teraz wszystkie testy powinny działać zgodnie z oczekiwaniami.


// aktualizacja prototypu i utworzenie instancji
Universe.prototype.nothing = true; // true
var uni = new Universe();
Universe.prototype.everything = true; // true
var uni2 = new Universe();

// to ta sama pojedyncza instancja


uni === uni2; // true

// wszystkie właściwości prototypu działają prawidłowo


// niezależnie od momentu ich zdefiniowania
uni.nothing && uni.everything && uni2.nothing && uni2.everything; // true
// standardowe właściwości również działają prawidłowo
uni.bang; // "Wielki"
// referencja wskazująca na konstruktor również jest prawidłowa
uni.constructor === Universe; // true

Alternatywne rozwiązanie mogłoby polegać na otoczeniu konstruktora oraz instancji funkcją


natychmiastową. Pierwsze wywołanie konstruktora tworzy obiekt i zapamiętuje go w pry-
watnej zmiennej instance. Drugie i kolejne wywołania jedynie zwracają zawartość zmiennej.
Wszystkie poprzednie testy będą działały również dla implementacji przedstawionej poniżej.
var Universe;

(function () {

var instance;

Universe = function Universe() {

if (instance) {
return instance;
}

instance = this;

// właściwa funkcjonalność
this.start_time = 0;
this.bang = "Wielki";

};

}());

Fabryka
Celem wzorca fabryki jest tworzenie obiektów. Najczęściej fabryką jest klasa lub metoda sta-
tyczna klasy, której celem jest:
• wykonanie powtarzających się operacji przy tworzeniu podobnych obiektów;
• zapewnienie użytkownikom możliwości tworzenia obiektów bez potrzeby znania kon-
kretnego typu (klasy) na etapie kompilacji.

Fabryka | 141
Drugi punkt ma większe znaczenie w przypadku języków ze statyczną analizą typów, w któ-
rych to utworzenie instancji klas nieznanych na etapie kompilacji nie jest zadaniem łatwym.
Na szczęście w języku JavaScript nie trzeba głowić się nad tym zagadnieniem.
Obiekty tworzone przez metodę fabryczną z reguły dziedziczą po tym samym przodku, ale
z drugiej strony są wyspecjalizowanymi wersjami z pewnymi dodatkowymi rozwiązaniami.
Czasem wspólny przodek to klasa zawierająca metodę fabryczną.
Przyjrzyjmy się przykładowej implementacji, która ma:
• wspólny konstruktor przodka CarMaker;
• metodę statyczną CarMaker o nazwie factory(), która tworzy obiekty samochodów;
• wyspecjalizowane konstruktory CarMaker.Compact, CarMaker.SUV i CarMaker.Convertible,
które dziedziczą po CarMaker i wszystkie są statycznymi właściwościami przodka, dzięki
czemu globalna przestrzeń nazw pozostaje czysta i łatwo je w razie potrzeby odnaleźć.
Implementacja będzie mogła być wykorzystywana w następujący sposób:
var corolla = CarMaker.factory('Compact');
var solstice = CarMaker.factory('Convertible');
var cherokee = CarMaker.factory('SUV');
corolla.drive(); // "Brum, mam 4 drzwi"
solstice.drive(); // "Brum, mam 2 drzwi"
cherokee.drive(); // "Brum, mam 17 drzwi"

Fragment
var corolla = CarMaker.factory('Compact');

to prawdopodobnie najbardziej rozpoznawalna część wzorca fabryki. Metoda przyjmuje typ


jako tekst i na jego podstawie tworzy i zwraca obiekty danego typu. Nie pojawiają się kon-
struktory wykorzystujące new lub literały obiektów — użytkownik stosuje funkcję, która two-
rzy obiekty na podstawie typu wskazanego jako tekst.
Oto przykładowa implementacja wzorca fabryki, która odpowiada wcześniejszemu przykła-
dowi jego użycia:
// konstruktor przodka
function CarMaker() {}

// metoda przodka
CarMaker.prototype.drive = function () {
return "Brum, mam " + this.doors + " drzwi";
};

// statyczna metoda fabryczna


CarMaker.factory = function (type) {
var constr = type,
newcar;

// błąd, jeśli konstruktor nie istnieje


if (typeof CarMaker[constr] !== "function") {
throw {
name: "Error",
message: constr + " nie istnieje"
};
}

// na tym etapie wiemy, że konstruktor istnieje


// niech odziedziczy przodka, ale tylko raz

142 | Rozdział 7. Wzorce projektowe


if (typeof CarMaker[constr].prototype.drive !== "function") {
CarMaker[constr].prototype = new CarMaker();
}
// utworzenie nowej instancji
newcar = new CarMaker[constr]();
// opcjonalne wywołanie dodatkowych metod i zwrócenie obiektu...
return newcar;
};

// definicje konkretnych konstruktorów


CarMaker.Compact = function () {
this.doors = 4;
};
CarMaker.Convertible = function () {
this.doors = 2;
};
CarMaker.SUV = function () {
this.doors = 17;
};

W implementacji wzorca fabryki nie ma nic szczególnego. Wystarczy wyszukać odpowiednią


funkcję konstruującą, która utworzy obiekt wymaganego typu. W tym przypadku zastoso-
wano bardzo proste odwzorowanie nazw przekazywanych do fabryki na odpowiadające im
obiekty. Przykładem powtarzających się zadań, które warto byłoby umieścić w fabryce, za-
miast powtarzać osobno dla każdego konstruktora, jest dziedziczenie.

Wbudowane fabryki obiektów


W zasadzie język JavaScript posiada wbudowaną fabrykę, którą jest globalny konstruktor
Object(). Zachowuje się on jak fabryka, ponieważ zwraca różne typy obiektów w zależności
od parametru wejściowego. Przekazanie liczby spowoduje utworzenie obiektu konstruktorem
Number(). Podobnie dzieje się dla tekstów i wartości logicznych. Wszystkie inne wartości lub
brak argumentu spowodują utworzenie zwykłego obiektu.
Oto kilka przykładów i testów tego sposobu działania. Co więcej, Object można również
wywołać z takim samym efektem bez użycia new.
var o = new Object(),
n = new Object(1),
s = Object('1'),
b = Object(true);

// testy
o.constructor === Object; // true
n.constructor === Number; // true
s.constructor === String; // true
b.constructor === Boolean; // true

To, że Object() jest również fabryką, ma małe znaczenie praktyczne, ale warto o tym
wspomnieć, by mieć świadomość, iż wzorzec fabryki pojawia się niemal wszędzie.

Iterator
We wzorcu iteratora mamy do czynienia z pewnym obiektem zawierającym zagregowane
dane. Dane te mogą być przechowywane wewnętrznie w bardzo złożonej strukturze, ale se-
kwencyjny dostęp do nich zapewnia bardzo prosta funkcja. Kod korzystający z obiektu nie
musi znać całej złożoności struktury danych — wystarczy, że wie, jak korzystać z poje-
dynczego elementu i pobrać następny.

Iterator | 143
We wzorcu iteratora kluczową rolę odgrywa metoda next(). Każde jej wywołanie powinno
zwracać następny element w kolejce. To, jak ułożona jest kolejka i jak posortowane są ele-
menty, zależy od zastosowanej struktury danych.
Przy założeniu, że obiekt znajduje się w zmiennej agg, dostęp do wszystkich elementów da-
nych uzyska się dzięki wywoływaniu next() w pętli:
var element;
while (element = agg.next()) {
// wykonanie działań na elemencie...
console.log(element);
}

We wzorcu iteratora bardzo często obiekt agregujący zapewnia dodatkową metodę pomocni-
czą hasNext(), która informuje użytkownika, czy został już osiągnięty koniec danych. Inny
sposób uzyskania sekwencyjnego dostępu do wszystkich elementów, tym razem z użyciem
hasNext(), mógłby wyglądać następująco:
while (agg.hasNext()) {
// wykonanie działań na następnym elemencie...
console.log(agg.next());
}

Po przedstawieniu sposobów użycia wzorca czas na implementację obiektu agregującego.


Implementując wzorzec iteratora, warto w zmiennej prywatnej przechowywać dane oraz
wskaźnik (indeks) do następnego elementu. W naszym przykładzie załóżmy, że dane to ty-
powa tablica, a „specjalna” logika pobierania tak naprawdę zwraca jej następny element.
var agg = (function () {

var index = 0,
data = [1, 2, 3, 4, 5],
length = data.length;

return {

next: function () {
var element;
if (!this.hasNext()) {
return null;
}
element = data[index];
index = index + 2;
return element;
},

hasNext: function () {
return index < length;
}

};
}());

Aby zapewnić łatwiejszy dostęp do danych i możliwość kilkukrotnej iteracji, obiekt może
oferować dodatkowe metody:
• rewind() — ustawia wskaźnik na początek kolejki;
• current() — zwraca aktualny element, bo nie można tego uczynić za pomocą next()
bez jednoczesnej zmiany wskaźnika.

144 | Rozdział 7. Wzorce projektowe


Implementacja tych dodatkowych metod nie sprawi żadnych trudności.
var agg = (function () {

// [jak wyżej...]

return {

// [jak wyżej...]

rewind: function () {
index = 0;
},
current: function () {
return data[index];
}
};
}());

Oto dodatkowy test iteratora:


// pętla wyświetla wartości 1, 3 i 5
while (agg.hasNext()) {
console.log(agg.next());
}

// powrót na początek
agg.rewind();
console.log(agg.current()); // 1

W konsoli pojawią się następujące wartości: 1, 3 i 5 (z pętli), a na końcu ponownie 1 (po


przejściu na początek kolejki).

Dekorator
We wzorcu dekoratora dodatkową funkcjonalność można dodawać do obiektu dynamicznie
w trakcie działania programu. W przypadku korzystania ze statycznych i niezmiennych klas
jest to faktycznie duże wyzwanie. W języku JavaScript obiekty można modyfikować, więc
dodanie do nich nowej funkcjonalności nie stanowi wielkiego problemu.
Dodatkową cechą wzorca dekoratora jest łatwość dostosowania i konfiguracji jego oczekiwa-
nego zachowania. Zaczyna się od prostego obiektu z podstawową funkcjonalnością. Następ-
nie wybiera się kilka z zestawu dostępnych dekoratorów, po czym rozszerza się nimi pod-
stawowy obiekt. Czasem istotna jest kolejność tego rozszerzania.

Sposób użycia
Przyjrzyjmy się sposobom użycia tego wzorca. Przypuśćmy, że opracowujemy aplikację, która
coś sprzedaje. Każda nowa sprzedaż to nowy obiekt sale. Obiekt zna cenę produktu i potrafi
ją zwrócić po wywołaniu metody sale.getPrice(). W zależności od aktualnych warunków
można zacząć „dekorować” obiekt dodatkową funkcjonalnością. Wyobraźmy sobie, że jako
amerykański sklep sprzedajemy produkt klientowi z kanadyjskiej prowincji Québec. W takiej
sytuacji klient musi zapłacić podatek federalny i dodatkowo podatek lokalny. We wzorcu
dekoratora będziemy więc „dekorowali” obiekt dekoratorem podatku federalnego i dekora-
torem podatku lokalnego. Po wyliczeniu ceny końcowej można również dodać dekorator do
jej formatowania. Scenariusz byłby następujący:

Dekorator | 145
var sale = new Sale(100); // cena wynosi 100 dolarów
sale = sale.decorate('fedtax'); // dodaj podatek federalny
sale = sale.decorate('quebec'); // dodaj podatek lokalny
sale = sale.decorate('money'); // formatowanie ceny
sale.getPrice(); // "USD 112.88"

W innym scenariuszu kupujący może mieszkać w prowincji, która nie stosuje podatku lokal-
nego, i dodatkowo możemy chcieć podać cenę w dolarach kanadyjskich.
var sale = new Sale(100); // cena wynosi 100 dolarów
sale = sale.decorate('fedtax'); // dodaj podatek federalny
sale = sale.decorate('cdn'); // sformatuj jako dolary kanadyjskie
sale.getPrice(); // "CAD 105.00"

Nietrudno zauważyć, że jest to wygodny i elastyczny sposób dodawania lub modyfikowania


funkcjonalności utworzonych już obiektów. Czas na implementację wzorca.

Implementacja
Jednym ze sposobów implementacji wzorca dekoratora jest utworzenie dekoratorów jako
obiektów zawierających metody do nadpisania. Każdy dekorator dziedziczy wówczas tak
naprawdę po obiekcie rozszerzonym przez poprzedni dekorator. Każda dekorowana metoda
wywołuje swoją poprzedniczkę za pomocą uber (odziedziczony obiekt), pobiera wartość
i przetwarza ją, dodając coś nowego.
Efekt jest taki, że wywołanie metody sale.getPrice() z pierwszego z przedstawionych
przykładów powoduje tak naprawdę wywołanie metody dekoratora money (patrz rysunek 7.1).
Ponieważ jednak każdy dekorator wywołuje najpierw odpowiadającą mu metodę ze swego
poprzednika, getPrice() z money wywołuje getPrice() z quebec, a ta metodę getPrice()
z fedtax i tak dalej. Łańcuch może być dłuższy, ale kończy się oryginalną metodą getPrice()
zaimplementowaną przez konstruktor Sale().

Rysunek 7.1. Implementacja wzorca dekoratora

146 | Rozdział 7. Wzorce projektowe


Implementacja rozpoczyna się od konstruktora i metody prototypu.
function Sale(price) {
this.price = price || 100;
}
Sale.prototype.getPrice = function () {
return this.price;
};

Wszystkie obiekty dekoratorów znajdą się we właściwości konstruktora:


Sale.decorators = {};

Przyjrzyjmy się przykładowemu dekoratorowi. To obiekt implementujący zmodyfikowaną


wersję metody getPrice(). Metoda najpierw pobiera zwróconą przez metodę przodka wartość,
a następnie ją modyfikuje.
Sale.decorators.fedtax = {
getPrice: function () {
var price = this.uber.getPrice();
price += price * 5 / 100;
return price;
}
};

W podobny sposób można zaimplementować dowolną liczbę innych dekoratorów. Mogą one
stanowić rozszerzenie podstawowej funkcjonalności Sale(), czyli działać jak dodatki. Co więcej,
nic nie stoi na przeszkodzie, by znajdowały się w dodatkowych plikach i były implementowane
przez innych, niezależnych programistów.
Sale.decorators.quebec = {
getPrice: function () {
var price = this.uber.getPrice();
price += price * 7.5 / 100;
return price;
}
};

Sale.decorators.money = {
getPrice: function () {
return "USD " + this.uber.getPrice().toFixed(2);
}
};

Sale.decorators.cdn = {
getPrice: function () {
return "CAD " + this.uber.getPrice().toFixed(2);
}
};

Na koniec przyjrzyjmy się „magicznej” metodzie o nazwie decorate(), która łączy ze sobą
wszystkie elementy. Sposób jej użycia jest następujący:
sale = sale.decorate('fedtax');

Tekst 'fedtax' odpowiada obiektowi zaimplementowanemu w Sale.decorators.fedtax.


Nowy obiekt newobj dziedziczy obiekt aktualny (oryginał lub już udekorowaną wersję), któ-
ry jest zawarty w this. Do zapewnienia dziedziczenia wykorzystajmy wzorzec konstruktora
tymczasowego z poprzedniego rozdziału. Dodatkowo ustawmy właściwość uber obiektu
newobj, by potomek miał dostęp do przodka. Następnie niech kod kopiuje wszystkie wła-
ściwości z dekoratora do nowego obiektu i zwraca newobj jako wynik całej operacji, co spo-
woduje, że stanie się on nowym obiektem sale.

Dekorator | 147
Sale.prototype.decorate = function (decorator) {
var F = function () {},
overrides = this.constructor.decorators[decorator],
i, newobj;
F.prototype = this;
newobj = new F();
newobj.uber = F.prototype;
for (i in overrides) {
if (overrides.hasOwnProperty(i)) {
newobj[i] = overrides[i];
}
}
return newobj;
};

Implementacja wykorzystująca listę


Przeanalizujmy inną implementację, która korzysta z dynamicznej natury języka JavaScript
i w ogóle nie stosuje dziedziczenia. Dodatkowo, zamiast wymuszać na każdej metodzie de-
korującej, by wywoływała swoją poprzedniczkę, przekazujemy tu wynik poprzedniej metody
jako parametr następnej.
Taka implementacja znacząco ułatwia wycofanie udekorowania, czyli usunięcie jednego
z elementów z listy dekoratorów.
Sposób użycia nowej implementacji będzie prostszy, bo nie wymaga ona przypisywania
wartości zwróconej przez decorate(). W tym przypadku decorate() jedynie dodaje nowy
element do listy:
var sale = new Sale(100); // cena wynosi 100 dolarów
sale.decorate('fedtax'); // dodaj podatek federalny
sale.decorate('quebec'); // dodaj podatek lokalny
sale.decorate('money'); // formatowanie ceny
sale.getPrice(); // "USD 112.88"

Tym razem konstruktor Sale() zawiera listę dekoratorów jako własną właściwość.
function Sale(price) {
this.price = (price > 0) || 100;
this.decorators_list = [];
}

Dostępne dekoratory są ponownie implementowane jako właściwości Sale.decorators.


Są prostsze, bo nie muszą już wywoływać poprzedniej wersji metody getPrice(), by uzy-
skać wartość pośrednią. Teraz trafia ona do systemu jako parametr.
Sale.decorators = {};

Sale.decorators.fedtax = {
getPrice: function (price) {
return price + price * 5 / 100;
}
};

Sale.decorators.quebec = {
getPrice: function (price) {
return price + price * 7.5 / 100;
}
};

148 | Rozdział 7. Wzorce projektowe


Sale.decorators.money = {
getPrice: function (price) {
return "USD " + price.toFixed(2);
}
};

Interesujące konstrukcje pojawiają się w metodach decorate() i getPrice() oryginalnego


obiektu. W poprzedniej implementacji metoda decorate() była w miarę złożona, a getPrice()
niezwykle prosta. W nowej jest dokładnie odwrotnie — decorate() po prostu dodaje nowy
element do listy, a getPrice() wykonuje całą istotną pracę. Pracą tą jest przejście przez listę
wszystkich dodanych dekoratorów i wywołanie dla każdego z nich metody getPrice() z po-
przednią wartością podaną jako argument metody.
Sale.prototype.decorate = function (decorator) {
this.decorators_list.push(decorator);
};

Sale.prototype.getPrice = function () {
var price = this.price,
i,
max = this.decorators_list.length,
name;
for (i = 0; i < max; i += 1) {
name = this.decorators_list[i];
price = Sale.decorators[name].getPrice(price);
}
return price;
};

Druga implementacja jest prostsza i nie korzysta z dziedziczenia. Prostsze są również metody
dekorujące. Całą rzeczywistą pracę wykonuje metoda, która „zgadza” się na dekorację. W tej
prostej implementacji dekorację dopuszcza jedynie metoda getPrice(). Jeśli dekoracja mia-
łaby dotyczyć większej liczby metod, każda z nich musiałaby przejść przez listę dekoratorów
i wywołać odpowiednie metody. Oczywiście taki kod stosunkowo łatwo jest umieścić
w osobnej metodzie pomocniczej i uogólnić. Umożliwiałby on dodanie dekorowalności do
dowolnej metody. Co więcej, w takiej implementacji właściwość decorators_list byłaby
obiektem z właściwościami o nazwach metod i z tablicami dekorowanych obiektów jako
wartościami.

Strategia
Wzorzec strategii umożliwia wybór odpowiedniego algorytmu na etapie działania aplikacji.
Użytkownicy kodu mogą stosować ten sam interfejs zewnętrzny, ale wybierać spośród kilku
dostępnych algorytmów, by lepiej dopasować implementację do aktualnego kontekstu.
Przykładem wzorca strategii może być rozwiązywanie problemu walidacji formularzy. Można
utworzyć jeden obiekt sprawdzania z metodą validate(). Metoda zostanie wywołana nie-
zależnie od rodzaju formularza i zawsze zwróci ten sam wynik — listę danych, które nie są
poprawne, wraz z komunikatami o błędach.
W zależności od sprawdzanych danych i typu formularza użytkownik kodu może wybrać
różne rodzaje sprawdzeń. Walidator wybiera najlepszą strategię wykonania zadania i dele-
guje konkretne czynności sprawdzeń do odpowiednich algorytmów.

Strategia | 149
Przykład walidacji danych
Przypuśćmy, że mamy do czynienia z następującym zestawem danych pochodzącym naj-
prawdopodobniej z formularza i że chcemy go sprawdzić pod kątem poprawności:
var data = {
first_name: "Super",
last_name: "Man",
age: "unknown",
username: "o_O"
};

Aby walidator znał najlepszą strategię do zastosowania w tym konkretnym przykładzie, trzeba
najpierw go skonfigurować, określając zestaw reguł i wartości uznawanych za prawidłowe.
Przypuśćmy, że nie wymagamy podania nazwiska i zaakceptujemy dowolną wartość imienia,
ale wymagamy podania wieku jako liczby i nazwy użytkownika, która składa się tylko z liczb
i liter bez znaków specjalnych. Konfiguracja mogłaby wyglądać następująco:
validator.config = {
first_name: 'isNonEmpty',
age: 'isNumber',
username: 'isAlphaNum'
};

Po skonfigurowaniu obiektu validator jest on gotowy do przyjęcia danych. Wywołujemy


jego metodę validate() i wyświetlamy błędy walidacji w konsoli.
validator.validate(data);
if (validator.hasErrors()) {
console.log(validator.messages.join("\n"));
}

Efektem wykonania kodu mógłby być następujący komunikat:


Niepoprawna wartość *age*; wartość musi być liczbą, na przykład 1, 3.14 lub 2010
Niepoprawna wartość *username*; wartość musi zawierać jedynie litery i cyfry bez
żadnych znaków specjalnych

Przyjrzyjmy się implementacji walidatora. Poszczególne algorytmy są obiektami o z góry


ustalonym interfejsie — zawierają metodę validate() i jednowierszową informację wyko-
rzystywaną jako komunikat o błędzie.
// sprawdzenie, czy podano jakąś wartość
validator.types.isNonEmpty = {
validate: function (value) {
return value !== "";
},
instructions: "wartość nie może być pusta"
};

// sprawdzenie, czy wartość jest liczbą


validator.types.isNumber = {
validate: function (value) {
return !isNaN(value);
},
instructions: "wartość musi być liczbą, na przykład 1, 3.14 lub 2010"
};

// sprawdzenie, czy wartość zawiera jedynie litery i cyfry


validator.types.isAlphaNum = {
validate: function (value) {

150 | Rozdział 7. Wzorce projektowe


return !/[^a-z0-9]/i.test(value);
},
instructions: "wartość musi zawierać jedynie litery i cyfry bez żadnych znaków
´specjalnych"
};

Najwyższy czas na obiekt validator:


var validator = {

// wszystkie dostępne sprawdzenia


types: {},

// komunikaty o błędach
// z aktualnej sesji walidacyjnej
messages: [],

// aktualna konfiguracja walidacji


// nazwa => rodzaj testu
config: {},

// metoda interfejsu
// data to pary klucz-wartość
validate: function (data) {

var i, msg, type, checker, result_ok;

// usunięcie wszystkich komunikatów


this.messages = [];

for (i in data) {

if (data.hasOwnProperty(i)) {

type = this.config[i];
checker = this.types[type];

if (!type) {
continue; // nie trzeba sprawdzać
}
if (!checker) { // ojej
throw {
name: "ValidationError",
message: "Brak obsługi dla klucza " + type
};
}

result_ok = checker.validate(data[i]);
if (!result_ok) {
msg = "Niepoprawna wartość *" + i + "*; " + checker.instructions;
this.messages.push(msg);
}
}
}
return this.hasErrors();
},

// metoda pomocnicza
hasErrors: function () {
return this.messages.length !== 0;
}
};

Strategia | 151
Obiekt validator jest uniwersalny i będzie działał prawidłowo dla różnych rodzajów spraw-
dzeń. Jednym z usprawnień mogłoby być dodanie kilku nowych testów. Po wykonaniu kilku
różnych formularzy z walidacją Twoja lista dostępnych sprawdzeń z pewnością się wydłuży.
Każdy kolejny formularz będzie wymagał jedynie skonfigurowania walidatora i uruchomie-
nia metody validate().

Fasada
Wzorzec fasady jest bardzo prosty i ma za zadanie zapewnić alternatywny interfejs obiektu.
Dobrą praktyką jest stosowanie krótkich metod, które nie wykonują zbyt wielu zadań. Stosując
to podejście, uzyskuje się znacznie więcej metod niż w przypadku tworzenia supermetod z wie-
loma parametrami. W większości sytuacji dwie lub więcej metod wykonuje się jednocześnie.
Warto wtedy utworzyć jeszcze jedną metodę, która stanowi otoczkę dla takich połączeń.
W trakcie obsługi zdarzeń przeglądarki bardzo często korzysta się z następujących metod:
• stopPropagation() — zapobiega wykonywaniu obsługi zdarzenia w węzłach nadrzędnych;
• preventDefault() — zapobiega wykonaniu przez przeglądarkę domyślnej akcji dla zda-
rzenia (na przykład kliknięcia łącza lub wysłania formularza).
To dwie osobne metody wykonujące odmienne zadania, więc nie stanowią jednej całości, ale z dru-
giej strony w zdecydowanej większości sytuacji są one wykonywane jednocześnie. Zamiast więc
powielać wywołania obu metod w całej aplikacji, można utworzyć fasadę, która je obie wykona.
var myevent = {
// ...
stop: function (e) {
e.preventDefault();
e.stopPropagation();
}
// ...
};

Wzorzec fasady przydaje się również w sytuacjach, w których za fasadą warto ukryć różnice
pomiędzy przeglądarkami internetowymi. Nic nie stoi na przeszkodzie, by rozbudować po-
przedni przykład o inny sposób obsługi anulowania zdarzeń przez przeglądarkę IE.
var myevent = {
// ...
stop: function (e) {
// inne
if (typeof e.preventDefault === "function") {
e.preventDefault();
}
if (typeof e.stopPropagation === "function") {
e.stopPropagation();
}
// IE
if (typeof e.returnValue === "boolean") {
e.returnValue = false;
}
if (typeof e.cancelBubble === "boolean") {
e.cancelBubble = true;
}
}
// ...
};

152 | Rozdział 7. Wzorce projektowe


Wzorzec fasady bywa pomocny w przypadku zmiany interfejsów związanej na przykład
z refaktoryzacją. Gdy chce się zamienić obiekt na inną implementację, najczęściej może to
zająć sporo czasu (jeśli jest on naprawdę rozbudowany). Załóżmy też, że już powstaje kod
dla nowego interfejsu. W takiej sytuacji można utworzyć przed starym obiektem fasadę, która
imituje nowy interfejs. W ten sposób po dokonaniu rzeczywistej zamiany i pozbyciu się starego
obiektu ilość zmian w najnowszym kodzie zostanie ograniczona do minimum.

Pośrednik
We wzorcu projektowym pośrednika jeden obiekt stanowi interfejs dla innego obiektu. Różni
się to od wzorca fasady, w którym po prostu istnieją pewne metody dodatkowe łączące
w sobie wywołania kilku innych metod. Pośrednik znajduje się między użytkownikiem
a obiektem i broni dostępu do niego.
Choć wzorzec wygląda jak dodatkowy narzut, w rzeczywistości często służy do poprawy
wydajności. Pośrednik staje się strażnikiem rzeczywistego obiektu i stara się, by ten wykonał
jak najmniej pracy.
Jednym z przykładów zastosowania pośrednika jest tak zwana leniwa inicjalizacja. Stosuje
się ją w sytuacjach, w których inicjalizacja rzeczywistego obiektu jest kosztowna, a istnieje
spora szansa, że klient po jego zainicjalizowaniu tak naprawdę nigdy go nie użyje. Pośrednik
może wtedy stanowić interfejs dla rzeczywistego obiektu. Otrzymuje polecenie inicjalizacji,
ale nie przekazuje go dalej aż do momentu, gdy rzeczywisty obiekt naprawdę zostanie użyty.
Rysunek 7.2 ilustruje sytuację, w której klient wysyła polecenie inicjalizujące, a pośrednik
odpowiada, że wszystko jest w porządku, choć tak naprawdę nie przekazuje polecenia dalej.
Czeka z inicjalizacją właściwego obiektu do czasu, gdy klient rzeczywiście będzie wykony-
wał na nim pracę — wówczas przekazuje obydwa komunikaty.

Rysunek 7.2. Komunikacja między klientem i rzeczywistym obiektem z wykorzystaniem pośrednika

Przykład
Wzorzec pośrednika bywa przydatny, gdy rzeczywisty obiekt docelowy wykonuje kosztow-
ne zadanie. W aplikacjach internetowych jedną z kosztownych sytuacji jest żądanie sieciowe,
więc w miarę możliwości warto zebrać kilka operacji i wykonać je jednym żądaniem. Prze-
śledźmy praktyczne zastosowanie wzorca właśnie w takiej sytuacji.

Pośrednik | 153
Aplikacja wideo
Załóżmy istnienie prostej aplikacji odtwarzającej materiał wideo wybranego artysty (patrz
rysunek 7.3). W zasadzie możesz nawet przetestować kod, wpisując w przeglądarce interne-
towej adres http://www.jspatterns.com/book/7/proxy.html.

Rysunek 7.3. Aplikacja wideo w akcji


Strona zawiera listę tytułów materiałów wideo. Gdy użytkownik kliknie tytuł, obszar poniżej
rozszerzy się, by przedstawić dodatkowe informacje i umożliwić odtworzenie filmu. Szcze-
góły dotyczące materiałów oraz adres URL treści wideo nie stanowią części strony — są po-
bierane poprzez osobne wywołania serwera. Serwer przyjmuje jako parametr kilka identyfi-
katorów materiałów wideo, więc aplikację można przyspieszyć, wykonując mniej żądań
HTTP i pobierając za każdym razem dane kilku filmów.
Aplikacja umożliwia jednoczesne rozwinięcie szczegółów kilku (a nawet wszystkich) mate-
riałów, co stanowi doskonałą okazję do połączenia kilku żądań w jedno.

Bez użycia pośrednika


Głównymi elementami aplikacji są dwa obiekty:
• videos — jest odpowiedzialny za rozwijanie i zwijanie obszarów informacyjnych (metoda
videos.getInfo()) oraz za odtwarzanie materiałów wideo (metoda videos.getPlayer()).
• http — jest odpowiedzialny za komunikację z serwerem za pomocą metody http.make
´Request().

154 | Rozdział 7. Wzorce projektowe


Bez stosowania pośrednika videos.getInfo() wywoła http.makeRequest() dla każdego
materiału wideo. Po dodaniu pośrednika pojawi się nowy obiekt o nazwie proxy znajdujący
się między videos oraz http i delegujący wszystkie wywołania makeRequest(), a także łą-
czący je ze sobą.
Najpierw pojawi się kod, w którym nie zastosowano wzorca pośrednika. Druga wersja, sto-
sująca obiekt pośrednika, poprawi ogólną płynność działania aplikacji.

Kod HTML
Kod HTML to po prostu zbiór łączy.
<p><span id="toggle-all">Przełącz zaznaczone</span></p>
<ol id="vids">
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--2158073">Gravedigger</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--4472739">Save Me</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--45286339">Crush</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--2144530">Don't Drink The Water
´</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--217241800">Funny the Way It Is
´</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--2144532">What Would You Say</a></li>
</ol>

Obsługa zdarzeń
Zanim pojawi się właściwa obsługa zdarzeń, warto dodać funkcję pomocniczą $ do pobiera-
nia elementów DOM na podstawie ich identyfikatorów.
var $ = function (id) {
return document.getElementById(id);
};

Stosując delegację zdarzeń (więcej na ten temat w rozdziale 8.), można obsłużyć wszystkie
kliknięcia dotyczące listy uporządkowanej id="vids" za pomocą jednej funkcji.
$('vids').onclick = function (e) {
var src, id;

e = e || window.event;
src = e.target || e.srcElement;

if (src.nodeName !== "A") {


return;
}

if (typeof e.preventDefault === "function") {


e.preventDefault();
}
e.returnValue = false;

id = src.href.split('--')[1];

if (src.className === "play") {

Pośrednik | 155
src.parentNode.innerHTML = videos.getPlayer(id);
return;
}

src.parentNode.id = "v" + id;


videos.getInfo(id);
};

Obsługa kliknięcia zainteresowana jest tak naprawdę dwoma sytuacjami: pierwszą dotyczącą
rozwinięcia lub zamknięcia części informacyjnej (wywołanie getInfo()) i drugą związaną
z odtworzeniem materiału wideo (gdy kliknięcie dotyczyło obiektu z klasą play), co oznacza,
że rozwinięcie już nastąpiło i można bezpiecznie wywołać metodę getPlayer(). Identyfika-
tory materiałów wideo wydobywa się z atrybutów href łączy.
Druga z funkcji obsługujących kliknięcia dotyczy sytuacji, w której użytkownik chce przełą-
czyć wszystkie części informacyjne. W zasadzie sprowadza się ona do wywoływania w pętli
metody getInfo().
$('toggle-all').onclick = function (e) {

var hrefs,
i,
max,
id;

hrefs = $('vids').getElementsByTagName('a');
for (i = 0, max = hrefs.length; i < max; i += 1) {
// pomiń łącza odtwarzania
if (hrefs[i].className === "play") {
continue;
}
// pomiń niezaznaczone
if (!hrefs[i].parentNode.firstChild.checked) {
continue;
}

id = hrefs[i].href.split('--')[1];
hrefs[i].parentNode.id = "v" + id;
videos.getInfo(id);
}
};

Obiekt videos
Obiekt videos zawiera trzy metody:
• getPlayer() — zwraca kod HTML niezbędny do odtworzenia materiału wideo (nie-
istotny w rozważaniach na temat obiektu pośrednika).
• updateList() — wywołanie zwrotne otrzymujące wszystkie dane z serwera i generujące
kod HTML do wykorzystania przy rozwijaniu szczegółów filmów (w tej metodzie rów-
nież nie dzieje się nic interesującego).
• getInfo() — metoda przełączająca widoczność części informacyjnych i wykonu-
jąca metody obiektu http przez przekazanie updateList() jako funkcji wywołania
zwrotnego.

156 | Rozdział 7. Wzorce projektowe


Oto istotny fragment obiektu videos:
var videos = {

getPlayer: function (id) {...},


updateList: function (data) {...},

getInfo: function (id) {

var info = $('info' + id);

if (!info) {
http.makeRequest([id], "videos.updateList");
return;
}

if (info.style.display === "none") {


info.style.display = '';
} else {
info.style.display = 'none';
}
}
};

Obiekt http
Obiekt http ma tylko jedną metodę, która wykonuje żądanie JSONP do usługi YQL firmy Yahoo.
var http = {
makeRequest: function (ids, callback) {
var url = 'http://query.yahooapis.com/v1/public/yql?q=',
sql = 'select * from music.video.id where ids IN ("%ID%")',
format = "format=json",
handler = "callback=" + callback,
script = document.createElement('script');

sql = sql.replace('%ID%', ids.join('","'));


sql = encodeURIComponent(sql);

url += sql + '&' + format + '&' + handler;


script.src = url;

document.body.appendChild(script);
}
};

YQL (Yahoo! Query Language) to uogólniona usługa internetowa, która oferuje moż-
liwość korzystania ze składni przypominającej SQL do pobierania danych z innych
usług. W ten sposób nie trzeba poznawać szczegółów ich API.

Gdy jednocześnie przełączone zostaną wszystkie materiały wideo, do serwera trafi sześć
osobnych żądań; każde będzie podobne do następującego żądania YQL:
select * from music.video.id where ids IN ("2158073")

Obiekt proxy
Zaprezentowany wcześniej kod działa prawidłowo, ale można go zoptymalizować. Na scenę
wkracza obiekt proxy, który przejmuje komunikację między http i videos. Obiekt stara się
połączyć ze sobą kilka żądań, czekając na ich zebranie 50 ms. Obiekt videos nie wywołuje

Pośrednik | 157
usługi HTTP bezpośrednio, ale przez pośrednika. Ten czeka krótką chwilę z wysłaniem żą-
dania. Jeśli wywołania z videos będą przychodziły w odstępach krótszych niż 50 ms, zostaną
połączone w jedno żądanie. Takie opóźnienie nie jest szczególnie widoczne, ale pomaga zna-
cząco przyspieszyć działanie aplikacji w przypadku jednoczesnego odsłaniania więcej niż
jednego materiału wideo. Co więcej, jest również przyjazne dla serwera, który nie musi ob-
sługiwać sporej liczby żądań.
Zapytanie YQL dla dwóch materiałów wideo może mieć postać:
select * from music.video.id where ids IN ("2158073", "123456")

W istniejącym kodzie zachodzi tylko jedna zmiana: metoda videos.getInfo() wywołuje


metodę proxy.makeRequest() zamiast metody http.makeRequest().
proxy.makeRequest(id, videos.updateList, videos);

Obiekt pośrednika korzysta z kolejki, w której gromadzi identyfikatory materiałów wideo przeka-
zane w ostatnich 50 ms. Następnie przekazuje wszystkie identyfikatory, wywołując metodę obiektu
http i przekazując własną funkcję wywołania zwrotnego, ponieważ videos.updateList() potrafi
przetworzyć tylko pojedynczy rekord danych.
Oto kod obiektu pośredniczącego proxy:
var proxy = {
ids: [],
delay: 50,
timeout: null,
callback: null,
context: null,
makeRequest: function (id, callback, context) {

// dodanie do kolejki
this.ids.push(id);

this.callback = callback;
this.context = context;

// ustawienie funkcji czasowej


if (!this.timeout) {
this.timeout = setTimeout(function () {
proxy.flush();
}, this.delay);
}
},
flush: function () {

http.makeRequest(this.ids, "proxy.handler");

// wyczyszczenie kolejki i funkcji czasowej


this.timeout = null;
this.ids = [];
},
handler: function (data) {
var i, max;

// pojedynczy materiał wideo


if (parseInt(data.query.count, 10) === 1) {
proxy.callback.call(proxy.context, data.query.results.Video);
return;
}

158 | Rozdział 7. Wzorce projektowe


// kilka materiałów wideo
for (i = 0, max = data.query.results.Video.length; i < max; i += 1) {
proxy.callback.call(proxy.context, data.query.results.Video[i]);
}
}
};

Wprowadzenie pośrednika umożliwiło połączenie kilku żądań pobrania danych w jedno po-
przez zmianę tylko jednego wiersza oryginalnego kodu.
Rysunki 7.4 i 7.5 przedstawiają scenariusze z trzema osobnymi żądaniami (bez pośrednika)
i z jednym połączonym żądaniem (po użyciu pośrednika).

Rysunek 7.4. Trzy osobne żądania do serwera

Rysunek 7.5. Wykorzystanie pośrednika do zmniejszenia liczby żądań wysyłanych do serwera

Pośrednik jako pamięć podręczna


W prezentowanym przykładzie obiekt videos żądający danych jest na tyle inteligentny, że
nie żąda tych samych informacji dwukrotnie. Nie zawsze jednak musi tak być. Pośrednik
może pójść o krok dalej i chronić rzeczywisty obiekt http przed powielaniem tych samych
żądań, zapamiętując je w nowej właściwości cache (patrz rysunek 7.6). Gdyby obiekt videos
ponownie poprosił o informacje o tym samym materiale (ten sam identyfikator), pośrednik
wydobyłby dane z pamięci podręcznej i uniknął komunikacji z serwerem.

Rysunek 7.6. Pamięć podręczna w obiekcie pośrednika

Pośrednik | 159
Mediator
Aplikacje — duże czy małe — składają się z wielu obiektów. Obiekty muszą się ze sobą ko-
munikować w sposób, który nie uczyni przyszłej konserwacji kodu prawdziwą drogą przez
mękę i umożliwi bezpieczną zmianę jednego fragmentu bez potrzeby przepisywania wszyst-
kich innych. Gdy aplikacja się rozrasta, pojawiają się coraz to nowe obiekty. W trakcie refak-
toryzacji obiekty usuwa się lub przerabia. Gdy wiedzą o sobie za dużo i komunikują się
bezpośrednio (wywołują się wzajemnie i modyfikują właściwości), powstaje między nimi
niepożądany ścisły związek. Jeśli obiekty są ze sobą powiązane zbyt mocno, niełatwo zmie-
nić jeden z nich bez modyfikacji pozostałych. Wtedy nawet najprostsza zmiana w aplikacji
nie jest dłużej trywialna i bardzo trudno oszacować, ile tak naprawdę czasu trzeba będzie
na nią poświęcić.
Wzorzec mediatora ma za zadanie promować luźne powiązania obiektów i wspomóc przy-
szłą konserwację kodu (patrz rysunek 7.7). W tym wzorcu niezależne obiekty (koledzy) nie
komunikują się ze sobą bezpośrednio, ale korzystają z obiektu mediatora. Gdy jeden z kole-
gów zmieni stan, informuje o tym mediator, a ten przekazuje tę informację wszystkim innym
zainteresowanym kolegom.

Rysunek 7.7. Uczestnicy wzorca mediatora

Przykład mediatora
Prześledźmy przykład użycia wzorca mediatora. Aplikacja będzie grą, w której dwóch gra-
czy przez pół minuty stara się jak najczęściej klikać w przycisk. Pierwszy gracz naciska kla-
wisz nr 1, a drugi klawisz 0 (spory odstęp między klawiszami zapewnia, że nie pobiją się
o klawiaturę). Tablica wyników pokazuje aktualny stan rywalizacji.
Obiektami uczestniczącymi w wymianie informacji są:
• pierwszy gracz,
• drugi gracz,
• tablica,
• mediator.

160 | Rozdział 7. Wzorce projektowe


Mediator wie o wszystkich obiektach. Komunikuje się z urządzeniem wejściowym (klawiaturą),
obsługuje naciśnięcia klawiszy, określa, który gracz jest aktywny, i informuje o zmianach
wyników (patrz rysunek 7.8). Gracz jedynie gra (czyli aktualizuje swój własny wynik) i in-
formuje mediator o tym zdarzeniu. Mediator informuje tablicę o zmianie wyniku, a ta aktu-
alizuje wyświetlaną wartość.

Rysunek 7.8. Uczestnicy w grze na szybkość naciskania klawiszy


Poza mediatorem żaden inny obiekt nie wie nic o pozostałych. Dzięki temu bardzo łatwo
zaktualizować grę, na przykład dodać nowego gracza lub zmienić tablicę wyników na wersję
wyświetlającą pozostały czas.
Pełna wersja gry wraz z kodem źródłowym jest dostępna pod adresem http://www.jspatterns.com/
book/7/mediator.html.
Obiekty graczy są tworzone przy użyciu konstruktora Player() i zawierają własne właści-
wości points i name. Metoda play() z prototypu zwiększa liczbę punktów o jeden i infor-
muje o tym fakcie mediator.
function Player(name) {
this.points = 0;
this.name = name;
}
Player.prototype.play = function () {
this.points += 1;
mediator.played();
};

Obiekt scoreboard zawiera metodę update() wywoływaną przez mediator po zdobyciu


punktu przez jednego z graczy. Tablica nie wie nic o graczach i nie przechowuje wyniku —
po prostu wyświetla informacje przekazane przez mediator.
var scoreboard = {

// aktualizowany element HTML


element: document.getElementById('results'),

// aktualizacja wyświetlacza
update: function (score) {

var i, msg = '';


for (i in score) {
if (score.hasOwnProperty(i)) {

Mediator | 161
msg += '<p><strong>' + i + '<\/strong>: ';
msg += score[i];
msg += '<\/p>';
}
}
this.element.innerHTML = msg;
}
};

Czas na obiekt mediatora. Odpowiada on za inicjalizację gry oraz utworzenie obiektów graczy
w metodzie setup() i śledzenie ich poczynań dzięki umieszczeniu ich we właściwości players.
Metoda played() zostaje wywołana przez każdego z graczy po wykonaniu akcji. Aktualizuje ona
wynik (score) i przesyła go do tablicy (scoreboard). Ostatnia metoda, keypress(), obsługuje
zdarzenia klawiatury, określa, który gracz jest aktywny, i powiadamia go o wykonanej akcji.
var mediator = {

// wszyscy gracze
players: {},

// inicjalizacja
setup: function () {
var players = this.players;
players.home = new Player('Gospodarze');
players.guest = new Player('Goście');
},

// ktoś zagrał, uaktualnij wynik


played: function () {
var players = this.players,
score = {
"Gospodarze": players.home.points,
"Goście": players.guest.points
};

scoreboard.update(score);
},

// obsługa interakcji z użytkownikiem


keypress: function (e) {
e = e || window.event; // IE
if (e.which === 49) { // klawisz "1"
mediator.players.home.play();
return;
}
if (e.which === 48) { // klawisz "0"
mediator.players.guest.play();
return;
}
}
};

Ostatni element to uruchomienie i zakończenie gry.


// start!
mediator.setup();
window.onkeypress = mediator.keypress;

// gra kończy się po 30 sekundach


setTimeout(function () {
window.onkeypress = null;
alert('Koniec gry!');
}, 30000);

162 | Rozdział 7. Wzorce projektowe


Obserwator
Wzorzec obserwatora jest niezwykle często wykorzystywany w programowaniu po stronie
klienta w języku JavaScript. Wszystkie zdarzenia przeglądarki (poruszenie myszą, naciśnięcie
klawisza itp.) to przykłady jego użycia. Inną często pojawiającą się nazwą tego wzorca są
zdarzenia własne, czyli zdarzenia tworzone przez programistę, a nie przeglądarkę. Jeszcze
inna nazwa to wzorzec subskrybenta-dostawcy.
Głównym celem używania wzorca jest promowanie luźnego powiązania elementów. Zamiast
sytuacji, w której jeden obiekt wywołuje metodę drugiego, mamy sytuację, w której drugi
z obiektów zgłasza chęć otrzymywania powiadomień o zmianie w pierwszym obiekcie. Sub-
skrybenta nazywa się często obserwatorem, a obiekt obserwowany obiektem publikującym
lub źródłem. Obiekt publikujący wywołuje subskrybentów po zajściu istotnego zdarzenia
i bardzo często przekazuje informację o nim w postaci obiektu zdarzenia.

Pierwszy przykład — subskrypcja magazynu


Aby dowiedzieć się, jak zaimplementować wzorzec, posłużmy się konkretnym przykładem.
Przypuśćmy, że mamy wydawcę paper, który publikuje gazetę codzienną i miesięcznik. Sub-
skrybent joe zostanie powiadomiony o wydaniu nowego periodyku.
Obiekt paper musi zawierać właściwość subscribers, która jest tablicą przechowującą
wszystkich subskrybentów. Zgłoszenie się do subskrypcji polega jedynie na dodaniu nowego
wpisu do tablicy. Gdy zajdzie istotne zdarzenie, obiekt paper przejdzie w pętli przez wszyst-
kich subskrybentów, by ich o nim powiadomić. Notyfikacja polega na wywołaniu metody
obiektu subskrybenta. Oznacza to, że w momencie zgłoszenia chęci otrzymywania powia-
domień subskrybent musi przekazać obiektowi paper jedną ze swoich metod w wywołaniu
metody subscribe().
Obiekt paper może dodatkowo umożliwić anulowanie subskrypcji, czyli usunięcie wpisu
z tablicy subskrybentów. Ostatnią istotną metodą obiektu paper jest publish(), która wy-
wołuje metody subskrybentów. Podsumowując, obiekt publikujący musi zawierać następujące
składowe:
• subscribers — tablica;
• subscribe() — dodaje wpis do tablicy;
• unsubscribe() — usuwa wpis z tablicy;
• publish() — przechodzi w pętli przez subskrybentów i wywołuje przekazane przez
nich metody.
Wszystkie trzy metody potrzebują parametru type, ponieważ wydawca może zgłosić kilka
różnych zdarzeń (publikację gazety lub magazynu), a subskrybenci mogą zdecydować się na
otrzymywanie powiadomień tylko o jednym z nich.
Ponieważ powyższe składowe są bardzo ogólne i mogą być stosowane przez dowolnego
wydawcę, warto zaimplementować je jako część osobnego obiektu. W ten sposób będzie je
można w przyszłości skopiować do dowolnego obiektu, zamieniając go w wydawcę (obiekt
publikujący).

Obserwator | 163
Oto przykładowa implementacja ogólnej funkcjonalności obiektu publikującego, która defi-
niuje wszystkie wymagane składowe oraz metodę pomocniczą visitSubscribers():
var publisher = {
subscribers: {
any: [] // typ zdarzenia
},
subscribe: function (fn, type) {
type = type || 'any';
if (typeof this.subscribers[type] === "undefined") {
this.subscribers[type] = [];
}
this.subscribers[type].push(fn);
},
unsubscribe: function (fn, type) {
this.visitSubscribers('unsubscribe', fn, type);
},
publish: function (publication, type) {
this.visitSubscribers('publish', publication, type);
},
visitSubscribers: function (action, arg, type) {
var pubtype = type || 'any',
subscribers = this.subscribers[pubtype],
i,
max = subscribers.length;

for (i = 0; i < max; i += 1) {


if (action === 'publish') {
subscribers[i](arg);
} else {
if (subscribers[i] === arg) {
subscribers.splice(i, 1);
}
}
}
}
};

Poniżej znajduje się kod funkcji, która przyjmuje obiekt i zamienia go w obiekt publikujący
przez proste skopiowanie wszystkich ogólnych metod dotyczących publikacji.
function makePublisher(o) {
var i;
for (i in publisher) {
if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
o[i] = publisher[i];
}
}
o.subscribers = {any: []};
}

Czas na implementację obiektu paper, który będzie publikował gazetę i magazyn.


var paper = {
daily: function () {
this.publish("ciekawy news");
},
monthly: function () {
this.publish("interesującą analizę", "magazyn");
}
};

Trzeba jeszcze uczynić z obiektu wydawcę.


makePublisher(paper);

164 | Rozdział 7. Wzorce projektowe


Po utworzeniu wydawcy możemy utworzyć obiekt subskrybenta o nazwie joe, który ma
dwie metody.
var joe = {
drinkCoffee: function (paper) {
console.log('Właśnie przeczytałem ' + paper);
},
sundayPreNap: function (monthly) {
console.log('Chyba zasnę, czytając ' + monthly);
}
};

Następny krok to obiekt paper subskrybujący joe (tak naprawdę to joe zgłasza się jako sub-
skrybent do paper).
paper.subscribe(joe.drinkCoffee);
paper.subscribe(joe.sundayPreNap, 'magazyn');

Obiekt joe udostępnił dwie metody. Pierwsza z nich powinna być wywoływana dla domyślnego
zdarzenia „wszystko”, a druga jedynie dla zdarzeń „magazyn”. Oto kilka zgłoszeń zdarzeń:
paper.daily();
paper.daily();
paper.daily();
paper.monthly();

Wszystkie metody publikujące wywołały odpowiednie metody z obiektu joe, co spowodo-


wało wyświetlenie w konsoli następującego wyniku:
Właśnie przeczytałem ciekawy news
Właśnie przeczytałem ciekawy news
Właśnie przeczytałem ciekawy news
Chyba zasnę, czytając interesującą analizę

Bardzo ważnym elementem całego systemu jest to, że paper nie zawiera w sobie informacji
o joe i odwrotnie. Co więcej, nie istnieje obiekt mediatora, który wiedziałby o wszystkich
obiektach. Obiekty uczestniczące w interakcjach są ze sobą powiązane bardzo luźno i bez ja-
kichkolwiek modyfikacji można dodać jeszcze kilku subskrybentów. Co ważne, joe może
w dowolnym momencie anulować subskrypcję.
Nic też nie stoi na przeszkodzie, by joe również został wydawcą (przecież to nic trudnego
dzięki systemom blogowym i mikroblogowym). Jako wydawca joe wysyła aktualizację swojego
statusu do serwisu Twitter:
makePublisher(joe);
joe.tweet = function (msg) {
this.publish(msg);
};

Wyobraźmy sobie, że dział relacji z klientami wydawcy gazety decyduje się czytać, co o ga-
zecie sądzi jej subskrybent joe, i dodaje w tym celu metodę readTweets().
paper.readTweets = function (tweet) {
alert('Zwołajmy duże zebranie! Ktoś napisał: ' + tweet);
};
joe.subscribe(paper.readTweets);

Gdy joe zamieści swój wpis, wydawca (paper) go otrzyma.


joe.tweet("nie lubię tej gazety");

Wykonanie kodu spowoduje wyświetlenie w konsoli tekstu „Zwołajmy duże zebranie! Ktoś
napisał: nie lubię tej gazety”.

Obserwator | 165
Pełny kod źródłowy przykładu oraz możliwość sprawdzenia wyników jego działania w konsoli
zapewnia strona HTML dostępna pod adresem http://www.jspatterns.com/book/7/observer.html.

Drugi przykład — gra w naciskanie klawiszy


Przyjrzymy się jeszcze jednemu przykładowi. Zaimplementujemy tę samą grę w naciskanie
klawiszy co przy wzorcu mediatora, ale tym razem użyjemy wzorca obserwatora. Aby nieco
utrudnić zadanie, zapewnijmy obsługę dowolnej liczby graczy, a nie tylko dwóch. Ponownie
skorzystamy z konstruktora Player(), który tworzy obiekty graczy, i z obiektu scoreboard.
Jedynie obiekt mediator zamieni się w obiekt game.
We wzorcu mediatora obiekt mediator wiedział o wszystkich uczestnikach gry i wywoływał
ich metody. Obiekt game ze wzorca obserwatora nie będzie tego robił — to same obiekty będą
zgłaszały chęć otrzymywania informacji o zajściu wybranych zdarzeń. Przykładowo, obiekt
scoreboard zgłosi chęć bycia informowanym o zajściu zdarzenia scorechange w obiekcie game.

Oryginalny obiekt publisher należy nieco zmienić, by upodobnić go do rozwiązań znanych


z przeglądarek internetowych.
• Zamiast metod publish(), subscribe() i unsubscribe() pojawią się metody fire(),
on() i remove().
• Typ zdarzenia (type) będzie używany cały czas, więc stanie się pierwszym parametrem
wszystkich trzech funkcji.
• Dodatkowy parametr context przekazywany wraz z funkcją powiadomienia umożliwi
wywołanie funkcji zwrotnej z odpowiednio ustawioną wartością this.
Nowy obiekt publisher ma następującą postać:
var publisher = {
subscribers: {
any: []
},
on: function (type, fn, context) {
type = type || 'any';
fn = typeof fn === "function" ? fn : context[fn];

if (typeof this.subscribers[type] === "undefined") {


this.subscribers[type] = [];
}
this.subscribers[type].push({fn: fn, context: context || this});
},
remove: function (type, fn, context) {
this.visitSubscribers('unsubscribe', type, fn, context);
},
fire: function (type, publication) {
this.visitSubscribers('publish', type, publication);
},
visitSubscribers: function (action, type, arg, context) {
var pubtype = type || 'any',
subscribers = this.subscribers[pubtype],
i,
max = subscribers ? subscribers.length : 0;

for (i = 0; i < max; i += 1) {


if (action === 'publish') {
subscribers[i].fn.call(subscribers[i].context, arg);
} else {

166 | Rozdział 7. Wzorce projektowe


if (subscribers[i].fn === arg && subscribers[i].context === context) {
subscribers.splice(i, 1);
}
}
}
}
};

Nowy konstruktor Player() wygląda następująco:


function Player(name, key) {
this.points = 0;
this.name = name;
this.key = key;
this.fire('newplayer', this);
}

Player.prototype.play = function () {
this.points += 1;
this.fire('play', this);
};

Nowym parametrem przyjmowanym przez konstruktor jest key — określa on klawisz na


klawiaturze, który gracz będzie naciskał, by uzyskiwać punkty (we wcześniejszej wersji kodu
klawisze były zapisane na sztywno). Dodatkowo utworzenie nowego obiektu gracza powo-
duje zgłoszenie zdarzenia newplayer, a każde naciśnięcie klawisza przez gracza skutkuje
zgłoszeniem zdarzenia play.
Obiekt scoreboard pozostaje bez zmian — nadal aktualizuje tablicę wyników, korzystając
z bieżących wartości.
Nowy obiekt game potrafi śledzić poczynania wszystkich graczy, by mógł zliczać wyniki
i zgłaszać zdarzenie scorechange. Dodatkowo zgłasza się on jako subskrybent wszystkich
zdarzeń keypress przeglądarki, by wiedzieć o wszystkich klawiszach przypisanych poszcze-
gólnym graczom.
var game = {

keys: {},

addPlayer: function (player) {


var key = player.key.toString().charCodeAt(0);
this.keys[key] = player;
},

handleKeypress: function (e) {


e = e || window.event; // IE
if (game.keys[e.which]) {
game.keys[e.which].play();
}
},

handlePlay: function (player) {


var i,
players = this.keys,
score = {};

for (i in players) {
if (players.hasOwnProperty(i)) {
score[players[i].name] = players[i].points;
}

Obserwator | 167
}
this.fire('scorechange', score);
}
};

Funkcja makePublisher(), która zamieniała dowolny obiekt w obiekt publikujący zdarzenia,


jest identyczna jak w przykładzie z wydawcą i gazetą. Obiekt game będzie zgłaszał zdarzenia
takie jak scorechange. Obiektem publikującym stanie się również Player.prototype, by możli-
we było zgłaszanie zdarzeń play i newplayer wszystkim zainteresowanym.
makePublisher(Player.prototype);
makePublisher(game);

Obiekt game zgłasza się jako subskrybent zdarzeń play i newplayer (a także zdarzenia keypress
przeglądarki), natomiast obiekt scoreboard chce być powiadamiany o zdarzeniach scorechange.
Player.prototype.on("newplayer", "addPlayer", game);
Player.prototype.on("play", "handlePlay", game);
game.on("scorechange", scoreboard.update, scoreboard);
window.onkeypress = game.handleKeypress;

Metoda on() umożliwia subskrybentom określenie funkcji zwrotnej jako referencji (score
´board.update) lub jako tekstu ("addPlayer"). Wersja tekstowa działa prawidłowo tylko
w przypadku przekazania jako trzeciego parametru kontekstu (na przykład game).
Ostatni element to dynamiczne tworzenie tylu obiektów graczy (po naciśnięciu klawiszy), ile
zostanie zażądanych przez grających.
var playername, key;
while (1) {
playername = prompt("Dodaj gracza (imię)");
if (!playername) {
break;
}
while (1) {
key = prompt("Klawisz dla gracza " + playername + "?");
if (key) {
break;
}
}
new Player(playername, key);
}

To już wszystko w temacie gry. Pełny kod źródłowy wraz z możliwością zagrania znajduje
się pod adresem http://www.jspatterns.com/book/7/observer-game.html.
W implementacji wzorca mediatora obiekt mediator musiał wiedzieć o wszystkich obiektach,
by móc w odpowiednim czasie wywoływać właściwe metody i koordynować całą grę. W nowej
implementacji obiekt game jest nieco głupszy i wykorzystuje fakt, iż obiekty zgłaszają zdarzenia
i obserwują się nawzajem (na przykład obiekt scoreboard nasłuchuje zdarzenia scorechange).
Zapewnia to jeszcze luźniejsze powiązanie obiektów (im mniej z nich wie o innych, tym lepiej),
choć za cenę utrudnionej analizy, kto tak naprawdę nasłuchuje kogo. W przykładowej grze
wszystkie subskrypcje są na razie w jednym miejscu, ale gdyby stała się ona bardziej rozbu-
dowana, wywołania on() mogłyby się znaleźć w wielu różnych miejscach (niekoniecznie
w kodzie inicjalizującym). Taki kod trudniej jest testować, gdyż trudno od razu zrozumieć,
co tak naprawdę się w nim dzieje. Wzorzec obserwatora zrywa ze standardowym, proce-
duralnym wykonywaniem kodu od początku do końca.

168 | Rozdział 7. Wzorce projektowe


Podsumowanie
W rozdziale pojawiły się opisy kilku popularnych wzorców projektowych i przykłady ich
implementacji w języku JavaScript. Omawiane były następujące wzorce:
• Singleton — tworzenie tylko jednego obiektu danej „klasy”. Pojawiło się kilka rozwią-
zań, w tym takie, które starały się zachować składnię znaną z języka Java przez zastoso-
wanie funkcji konstruujących. Trzeba jednak pamiętać, że z technicznego punktu widze-
nia w języku JavaScript wszystkie obiekty są singletonami. Nie należy też zapominać, że
czasem programiści stosują słowo „singleton”, a mają na myśli obiekty utworzone przy
użyciu wzorca modułu.
• Fabryka — metoda tworząca obiekty o typie przekazanym jako wartość tekstowa.
• Iterator — interfejs umożliwiający łatwe przetwarzanie elementów umieszczonych w zło-
żonej strukturze danych.
• Dekorator — modyfikacja obiektów w trakcie działania programu przez dodawanie do
nich funkcjonalności zdefiniowanych w dekoratorach.
• Strategia — zachowanie tego samego interfejsu przy jednoczesnym wyborze najlepszej
strategii jego implementacji (uzależnionej od kontekstu).
• Fasada — zapewnienie bardziej przyjaznego API przez otoczenie typowych (lub źle za-
projektowanych) metod ich nowymi wersjami.
• Pośrednik — otoczenie obiektu zapewniające kontrolę dostępu do niego, gdy celem jest
uniknięcie wykonywania kosztownych operacji przez ich grupowanie lub opóźnianie do
momentu, gdy będą naprawdę konieczne.
• Mediator — promowanie luźnego powiązania obiektów przez unikanie bezpośredniej
komunikacji między nimi i zastąpienie jej komunikacją poprzez obiekt mediatora.
• Obserwator — luźne powiązanie między obiektami osiągane przez tworzenie obiektów,
których zmiany można obserwować, jawnie zgłaszając się jako subskrybent (często mówi
się również o własnych zdarzeniach lub wzorcu subskrybenta-dostawcy).

Podsumowanie | 169
170 | Rozdział 7. Wzorce projektowe
ROZDZIAŁ 8.

DOM i wzorce dotyczące przeglądarek

W poprzednich rozdziałach główny nacisk położony został na rdzeń języka JavaScript


(ECMAScript), a nie na wzorce wykorzystania języka w przeglądarkach internetowych. Niniejszy
rozdział skupi się przede wszystkim na wzorcach specyficznych dla przeglądarek, bo stanowią
one najbardziej typowe środowisko uruchomieniowe dla większości programów. Co więcej,
programowanie aplikacji działających w przeglądarkach to element, który wiele osób ma na
myśli, gdy mówi, że nie lubi języka JavaScript. To zrozumiałe głównie ze względu na różnice
w implementacji obiektów gospodarza i DOM. Każdemu przydadzą się techniki, dzięki którym
programowanie w takim środowisku stanie się łatwiejsze.
Wzorce opisywane w tym rozdziale podzielone zostały na kilka obszarów, włączając w to
skrypty wykorzystujące DOM, obsługę zdarzeń, komunikację zewnętrzną, strategie wczyty-
wania dodatkowego kodu i kroki niezbędne do efektywnego korzystania z kodu JavaScript
w środowisku produkcyjnym.
Najpierw jednak pojawi się krótkie i nieco filozoficzne rozważanie na temat ogólnego podej-
ścia do pisania programów uruchamianych po stronie klienta.

Podział zadań
Trzema głównymi elementami tworzonych aplikacji internetowych są:
• zawartość — dokument HTML;
• prezentacja — style CSS określające wygląd dokumentu;
• zachowanie — kod JavaScript obsługujący interakcję z użytkownikiem i wszystkie dy-
namiczne zmiany dokumentu.
Utrzymanie jak największego rozdziału między tymi trzema elementami ułatwia dostarcza-
nie aplikacji różnym systemom klienckim: przeglądarkom graficznym, przeglądarkom tek-
stowym, technologiom wspomagającym osoby niedowidzące, urządzeniom przenośnym i tak
dalej. Podział zadań idzie ręka w rękę z pomysłem progresywnego rozszerzania — rozpo-
czyna się od bardzo prostej wersji (jedynie kod HTML) dla najprostszych agentów użytkow-
nika i dodaje się nowe funkcjonalności wraz ze wzrostem możliwości agentów. Jeśli przeglą-
darka obsługuje CSS, dokument wygląda ładniej. Jeśli obsługuje JavaScript, dokument staje
się tak naprawdę aplikacją posiadającą zachowania dynamiczne.

171
W praktyce podział zadań oznacza:
• Testowanie stron z wyłączoną obsługą CSS w celu sprawdzenia, czy nadal są użyteczne
i czytelne.
• Testowanie stron z wyłączoną obsługą JavaScriptu w celu upewnienia się, że nadal wy-
konują poprawnie swoje podstawowe zadanie, że działają wszystkie łącza (brak przy-
padków typu href="#"), a formularze można wypełnić i wysłać bez przeszkód.
• Powstrzymanie się od korzystania z definicji obsługi zdarzeń (na przykład onclick) lub
stylów (atrybut style) w kodzie HTML, bo nie należą one do warstwy prezentacji.
• Stosowanie elementów HTML o znaczeniu semantycznym takich jak nagłówki lub listy.

Warstwa JavaScriptu (zachowanie) powinna być nieinwazyjna, czyli nie powinna przeszkadzać
użytkownikowi, czynić strony bezużytecznej w nieobsługiwanych przeglądarkach i stanowić
niezbędnego elementu do prawidłowego funkcjonowania strony WWW. Powinna jedynie
ułatwiać korzystanie ze strony.
Typową techniką eleganckiej obsługi różnic między przeglądarkami jest wykrywanie moż-
liwości. Zakłada ona, że nie należy korzystać ze sprawdzeń rodzaju i wersji przeglądarki do
określania kodu do wykonania, ale zamiast tego testować istnienie metody lub właściwości
w aktualnym środowisku. Sprawdzanie rodzaju przeglądarki uznaje się obecnie za antywzorzec.
Czasem faktycznie nie można go uniknąć, ale powinno być stosowane tylko w sytuacjach,
w których inne rozwiązania nie zapewnią jednoznacznego wyniku (lub są bardzo kosztowne
wydajnościowo).
// antywzorzec
if (navigator.userAgent.indexOf('MSIE') !== 1) {
document.attachEvent('onclick', console.log);
}

// znacznie lepiej
if (document.attachEvent) {
document.attachEvent('onclick', console.log);
}

// bardziej skonkretyzowane sprawdzenie


if (typeof document.attachEvent !== "undefined") {
document.attachEvent('onclick', console.log);
}

Podział zadań pomaga w tworzeniu aplikacji, jej pielęgnowaniu i aktualizowaniu, bo gdy coś
nie zadziała, najczęściej wiadomo, co należy sprawdzić w pierwszej kolejności. Gdy pojawi
się błąd JavaScriptu, nie trzeba patrzeć na kod HTML lub CSS, by go naprawić.

Skrypty wykorzystujące DOM


Korzystanie z drzewa DOM strony WWW to jedno z najczęstszych zadań wykonywanych
w kodzie JavaScript po stronie klienta. To również podstawowy powód niezliczonych bólów
głowy i złej sławy języka JavaScript. Wszystko to jednak przez niejednolitą i niepozbawioną
błędów obsługę metod DOM przez różne przeglądarki. Wykorzystanie dobrej biblioteki
JavaScript, która niweluje różnice między przeglądarkami, potrafi znacząco zwiększyć pro-
duktywność programisty.
Przyjrzyjmy się kilku zalecanym wzorcom dostępu do drzewa DOM i jego modyfikacji, sku-
piając się przede wszystkim na wydajności.

172 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


Dostęp do DOM
Uzyskiwanie dostępu do DOM jest kosztowne i bardzo często stanowi główne źródło pro-
blemów z wydajnością w języku JavaScript. Wynika to z faktu, iż DOM jest w wielu przypad-
kach zaimplementowany poza rdzeniem interpretera JavaScriptu. Z perspektywy przeglą-
darki takie podejście ma sens, ponieważ aplikacja JavaScript może w ogóle nie wymagać
DOM, a on sam może być wykorzystywany przez wiele różnych języków (na przykład przez
VBScript w IE).
Ogólna reguła jest więc następująca: ograniczyć wykorzystanie DOM do minimum. To oznacza:
• unikanie uzyskiwania dostępu do DOM w pętli;
• przypisywanie referencji DOM do lokalnych zmiennych i korzystanie z wersji lokalnych;
• wykorzystanie API selektorów CSS, jeśli tylko jest dostępne;
• zapamiętywanie length w trakcie iteracji przez kolekcje HTML (patrz rozdział 2.).

W poniższym przykładzie druga z pętli, choć nieco dłuższa, w niektórych przeglądarkach


będzie wręcz dziesiątki tysięcy razy szybsza.
// antywzorzec
for (var i = 0; i < 100; i += 1) {
document.getElementById("result").innerHTML += i + ", ";
}

// lepiej — aktualizacja zmiennej lokalnej


var i, content = "";
for (i = 0; i < 100; i += 1) {
content += i + ",";
}
document.getElementById("result").innerHTML += content;

W kolejnym przykładzie drugi z fragmentów kodu (korzystający z lokalnej zmiennej style)


jest lepszy, choć wymaga dodatkowego wiersza i dodatkowej zmiennej.
// antywzorzec
var padding = document.getElementById("result").style.padding,
margin = document.getElementById("result").style.margin;

// lepiej
var style = document.getElementById("result").style,
padding = style.padding,
margin = style.margin;

Wykorzystanie API selektorów CSS oznacza użycie następujących metod:


document.querySelector("ul .selected");
document.querySelectorAll("#widget .class");

Metody te przyjmują selektor CSS i zwracają listę węzłów, które do niego pasują. Są dostępne
we wszystkich nowoczesnych przeglądarkach (w IE od wersji 8.) i zawsze będą szybsze
w porównaniu z ręcznym wyborem elementów za pomocą metod DOM. Aktualne wersje
wielu popularnych bibliotek JavaScript wykorzystują wspomniane metody (o ile są dostępne),
więc warto dokonać aktualizacji, jeśli stosowana wersja nie jest najnowszą.
Warto w tym miejscu wspomnieć, że dodanie do bardzo często modyfikowanych elementów
atrybutów id="" zapewnia najłatwiejszy i najszybszy dostęp do nich (document.getElement
´ById(myid)).

Skrypty wykorzystujące DOM | 173


Modyfikacja DOM
Poza dostępem do elementów DOM bardzo często zachodzi potrzeba ich zmiany oraz dodania
nowych lub usunięcia starych węzłów. Aktualizacja DOM powoduje przerysowanie zmody-
fikowanej części ekranu, a czasem również przeliczenie geometrii elementów, co nierzadko
jest kosztowne.
Ogólna zasada jest następująca: należy wykonywać jak najmniej aktualizacji DOM, czyli
przeprowadzać je w dużych grupach, najlepiej poza aktualnie renderowanym drzewem
węzłów.
Gdy trzeba utworzyć stosunkowo duże poddrzewo, warto generować je w pamięci poza
właściwym drzewem DOM dokumentu i dodać je do niego na samym końcu całego procesu.
W tym celu można wykorzystać tak zwany fragment dokumentu.
Oto, w jaki sposób nie należy dodawać nowych węzłów:
// antywzorzec
// dodawanie węzłów tuż po ich utworzeniu

var p, t;

p = document.createElement('p');
t = document.createTextNode('first paragraph');
p.appendChild(t);
document.body.appendChild(p);

p = document.createElement('p');
t = document.createTextNode('second paragraph');
p.appendChild(t);
document.body.appendChild(p);

Lepszym rozwiązaniem będzie utworzenie fragmentu dokumentu, aktualizowanie go poza


głównym drzewem i dodanie do DOM dokumentu, gdy będzie gotowy. Podczas dodawania
fragmentu dokumentu do drzewa DOM dodawany jest nie fragment drzewa, a jego zawar-
tość. To naprawdę wygodne. Fragment dokumentu to bardzo dobry sposób na zebranie kilku
węzłów, nawet jeśli nie mają żadnego logicznego przodka (na przykład akapity nie znajdują się
w elemencie div).
Oto przykład użycia fragmentu dokumentu:
var p, t, frag;

frag = document.createDocumentFragment();

p = document.createElement('p');
t = document.createTextNode('first paragraph');
p.appendChild(t);
frag.appendChild(p);

p = document.createElement('p');
t = document.createTextNode('second paragraph');
p.appendChild(t);
frag.appendChild(p);

document.body.appendChild(frag);

W tym przykładzie drzewo DOM dokumentu jest aktualizowane tylko raz, więc pojawia się tylko
jedno przeliczenie i przerysowanie, a nie jedno na każdy akapit jak w poprzednim kodzie.

174 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


Fragment dokumentu przydaje się w przypadku dodawania nowych węzłów do drzewa.
Gdy jednak aktualizuje się jego istniejącą część, nadal można wykonać wszystkie zmiany
jako jedną aktualizację. Wystarczy sklonować korzeń poddrzewa, które ma zostać zmienione,
dokonać odpowiednich modyfikacji, a następnie zamienić oryginał na klon.
var oldnode = document.getElementById('result'),
clone = oldnode.cloneNode(true);

// modyfikacja klonu...

// po zakończeniu:
oldnode.parentNode.replaceChild(clone, oldnode);

Zdarzenia
Kolejnym obszarem przeglądarek, który pełen jest nieścisłości i przez to stanowi źródło frustra-
cji programistów, są zdarzenia takie jak kliknięcie, naciśnięcie klawisza i tym podobne.
Biblioteki JavaScript starają się ukryć podwójną pracę niezbędną do prawidłowej obsługi
zdarzeń w przeglądarce IE (przed wersją 9.) i w implementacjach zgodnych ze standardem W3C.
Warto znać podstawowe różnice, bo niejednokrotnie zdarza się, że proste strony i niewielkie
projekty nie korzystają z istniejących bibliotek. Wiedza ta przydatna jest również przy two-
rzeniu bibliotek własnych.

Obsługa zdarzeń
Wszystko zaczyna się od przypisania funkcji obsługi zdarzeń do elementów. Przypuśćmy, że
mamy do czynienia z przyciskiem, który zwiększa wartość licznika po każdym kliknięciu.
Można dodać obsługę zdarzenia za pomocą atrybutu onclick, który będzie działał prawi-
dłowo we wszystkich przeglądarkach, ale złamie to zasadę podziału obowiązków i progresyw-
nego rozszerzania. Obsługę zdarzenia warto dodać w kodzie JavaScript, poza kodem HTML.
Przypuśćmy, że kod HTML ma następującą postać:
<button id="clickme">Kliknij mnie: 0</button>

Można przypisać funkcję do właściwości onclick węzła, ale można to zrobić tylko raz.
// nieoptymalne rozwiązanie
var b = document.getElementById('clickme'),
count = 0;
b.onclick = function () {
count += 1;
b.innerHTML = "Kliknij mnie: " + count;
};

Jeśli kliknięcie powinno wykonać kilka funkcji, nie można tego uczynić bez narażania się na
utratę luźnego powiązania funkcjonalności. Oczywiście można przed przypisaniem nowej
funkcji sprawdzić, czy onclick zawiera już jakąś funkcję, i jeśli tak, dodać tę istniejącą jako
część nowej, a następnie przypisać do onclick nową funkcję. Istnieje jednak znacznie wygod-
niejsze rozwiązanie — metoda addEventListener(). Metoda ta nie istnieje w przeglądarce IE
aż do wersji 8., więc dla wersji poprzednich trzeba stosować metodę attachEvent().

Zdarzenia | 175
W rozdziale 4. przy okazji opisu wzorca usuwania warunkowych wersji kodu pojawiło się
bardzo dobre rozwiązanie dotyczące definiowania zdarzeń w sposób zgodny z różnymi
przeglądarkami. Bez wdawania się w szczegóły spójrzmy na kod przypisujący funkcję do
zdarzenia kliknięcia przycisku.
var b = document.getElementById('clickme');
if (document.addEventListener) { // W3C
b.addEventListener('click', myHandler, false);
} else if (document.attachEvent) { // IE
b.attachEvent('onclick', myHandler);
} else { // najbardziej ogólne rozwiązanie
b.onclick = myHandler;
}

Kliknięcie przycisku spowoduje wykonanie funkcji myHandler(). Niech funkcja ta zwiększa


liczbę podawaną na etykiecie przycisku. By nieco utrudnić zadanie, przyjmijmy, że istnieje
kilka przycisków stosujących tę samą funkcję myHandler(). Przechowywanie referencji do
każdego obiektu oraz stanu licznika nie byłoby efektywne, szczególnie jeśli uzyskiwalibyśmy
informację o klikniętym obiekcie.
Najpierw kod rozwiązania, a następnie jego krótki opis:
function myHandler(e) {

var src, parts;

// pobranie zdarzenia i elementu źródłowego


e = e || window.event;
src = e.target || e.srcElement;

// właściwe zadanie: aktualizacja etykiety


parts = src.innerHTML.split(": ");
parts[1] = parseInt(parts[1], 10) + 1;
src.innerHTML = parts[0] + ": " + parts[1];

// wyłączenie bąbelkowania
if (typeof e.stopPropagation === "function") {
e.stopPropagation();
}
if (typeof e.cancelBubble !== "undefined") {
e.cancelBubble = true;
}

// wyłączenie domyślnej akcji


if (typeof e.preventDefault === "function") {
e.preventDefault();
}
if (typeof e.returnValue !== "undefined") {
e.returnValue = false;
}
}

W pełni działający przykład jest dostępny pod adresem http://www.jspatterns.com/book/8/click.html.


Funkcja obsługi zdarzenia składa się z czterech części.
• Najpierw trzeba uzyskać dostęp do obiektu zdarzenia zawierającego informacje o zda-
rzeniu oraz element strony, który je wywołał. Obiekt zdarzenia jest przekazywany do
funkcji jako parametr, ale w przypadku stosowania właściwości onclick należy go uzy-
skać za pomocą globalnej zmiennej window.event.
• Druga część wykonuje właściwe zadanie, czyli aktualizację etykiety.

176 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


• Następna część dotyczy anulowania tak zwanego bąbelkowania. W tym konkretnym
przypadku nie jest to wymagane, ale gdy się tego nie uczyni, zdarzenie będzie wykonywało
wszystkie funkcje obsługi aż do rdzenia dokumentu lub obiektu okna. Niestety, zadanie
trzeba wykonać na dwa sposoby: zgodny ze standardem W3C (stopPropagation()) i zgod-
ny z IE (cancelBuble).
• Na końcu warto zapobiec wykonaniu domyślnej akcji (jeśli to niezbędne). Pewne zdarzenia
(kliknięcie łącza, wysłanie formularza) mają przypisane w przeglądarce działania do-
myślne, ale można ich uniknąć, wykonując metodę preventDefault() (lub, w IE, przy-
pisując wartość false właściwości returnValue).
Nietrudno zauważyć, że wiele działań wykonuje się podwójnie, więc warto utworzyć metodę
pomocniczą — fasadę omawianą w rozdziale 7.

Delegacja zdarzeń
Wzorzec delegacji zdarzeń korzysta z istnienia tak zwanego bąbelkowania zdarzeń, co po-
zwala zmniejszyć liczbę niezbędnych funkcji obsługujących zdarzenia do jednej dla całego
zestawu węzłów. Jeśli element div zawiera 10 przycisków, wystarczy zastosować jedną funk-
cję obsługi zdarzeń przypisaną do niego, zamiast przypisywać funkcję 10 razy dla każdego
z przycisków z osobna.
Następny przykład przedstawiony na rysunku 8.1 zawiera trzy przyciski umieszczone
w elemencie div. W pełni działający kod przykładu delegacji zdarzeń znajdziesz pod adresem
http://www.jspatterns.com/book/8/click-delegate.html.

Rysunek 8.1. Przykład delegacji zdarzeń — trzy przyciski zwiększające swoje liczniki w etykietach
Kod HTML jest następujący:
<div id="click-wrap">
<button>Kliknij mnie: 0</button>
<button>Mnie też kliknij: 0</button>
<button>Kliknij mnie, numer trzy: 0</button>
</div>

Zamiast przypisywać procedury obsługi do każdego przycisku, przypisuje się jedną do ele-
mentu otaczającego div o identyfikatorze click-wrap. W zasadzie można wykorzystać funk-
cję myHandler() z poprzedniego przykładu, ale z jedną zmianą — należy wyfiltrować klik-
nięcia, którymi nie jest się zainteresowanym. W tym konkretnym przypadku trzeba obsłużyć
kliknięcia przycisków, ale pominąć kliknięcie samego elementu div.
Zamiana w funkcji myHandler() sprowadza się do sprawdzenia, czy właściwość nodeName
źródła zdarzenia jest równa "button".
// ...
// pobranie zdarzenia i elementu źródłowego
e = e || window.event;
src = e.target || e.srcElement;

if (src.nodeName.toLowerCase() !== "button") {


return;
}
// ...

Zdarzenia | 177
Wadą delegacji zdarzeń jest konieczność filtrowania tych z nich, którymi nie jesteśmy zainte-
resowani, co owocuje kilkoma dodatkowymi wierszami kodu. Zalety — wydajność i znacz-
nie elastyczniejszy kod — znacząco przewyższają wady, więc to wysoce zalecany wzorzec.
Nowoczesne biblioteki JavaScript ułatwiają delegację zdarzeń przez zapewnienie wygodnego
API. Przykładowo, biblioteka YUI3 zawiera metodę Y.delegate(), która umożliwia określe-
nie selektorów CSS dotyczących zarówno elementu otaczającego, jak i elementów i zdarzeń,
którymi jesteśmy zainteresowani. To wygodne rozwiązanie, bo funkcja wywołania zwrotne-
go nie zostaje wywołana dla zdarzeń, które chcemy pominąć. Dla poprzedniego przykładu
kod przypisujący delegację zdarzeń wyglądałby następująco:
Y.delegate('click', myHandler, "#click-wrap", "button");

Dzięki zniwelowaniu różnic między przeglądarkami wykonanemu przez bibliotekę YUI


i automatycznemu wskazaniu źródła zdarzenia wynikowy kod może być znacznie prostszy.
function myHandler(e) {

var src = e.currentTarget,


parts;

parts = src.get('innerHTML').split(": ");


parts[1] = parseInt(parts[1], 10) + 1;
src.set('innerHTML', parts[0] + ": " + parts[1]);

e.halt();
}

W pełni działający przykład jest dostępny pod adresem http://www.jspatterns.com/book/8/


click-y-delegate.html.

Długo działające skrypty


W trakcie surfowania po internecie napotyka się strony, na których przeglądarka informuje,
iż skrypt wykonuje się zbyt długo, i proponuje użytkownikowi jego zatrzymanie. W tworzo-
nych przez nas aplikacjach taki komunikat nigdy nie powinien się pojawić niezależnie od tego,
jak złożone jest wykonywane zadanie.
Jeśli skrypt działa zbyt długo, interfejs przeglądarki gorzej reaguje na działania użytkownika,
na przykład reakcja na kliknięcie wydaje się trwać wieki. Nie robi to na odwiedzających do-
brego wrażenia, więc warto tego unikać.
W języku JavaScript nie ma wątków, ale można je zasymulować, stosując funkcję setTimeout(),
a w niektórych nowoczesnych przeglądarkach również osobne skrypty obliczeniowe.

Funkcja setTimeout()
Pomysł polega na podzieleniu ogromu pracy na mniejsze kawałki i wyliczaniu każdego
z nich z przerwą wynoszącą 1 milisekundę. Opóźnienie spowoduje wykonanie zadania
w dłuższym czasie bezwzględnym, ale interfejs użytkownika będzie cały czas szybko reagował
na zdarzenia, zapewniając odwiedzającemu pełny komfort obsługi.

178 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


Opóźnienie wynoszące 1 milisekundę (lub nawet 0) w rzeczywistości będzie znacz-
nie dłuższe. Wszystko zależy od przeglądarki i systemu operacyjnego. Wartość 0 nie
oznacza „od razu”, ale raczej „najszybciej, jak to możliwe”. Na przykład w przeglą-
darce Internet Explorer najkrótszym rzeczywistym czasem reakcji jest 15 milisekund.

Skrypty obliczeniowe
Nowoczesne przeglądarki oferują dodatkową możliwość radzenia sobie z długo działającymi
skryptami — skrypty obliczeniowe. Są one odpowiednikiem wątków działających w przeglą-
darce w tle. Istotne obliczenia można umieścić w osobnym pliku, na przykład my_web_worker.js,
a następnie wywołać z poziomu głównego programu (strony).
var ww = new Worker('my_web_worker.js');
ww.onmessage = function (event) {
document.body.innerHTML +=
"<p>komunikat z wątku obliczeniowego: " + event.data + "</p>";
};

Kod źródłowy skryptu przedstawiony poniżej wykonuje pewną prostą operację arytmetyczną
około 100 milionów razy.
var end = 1e8, tmp = 1;

postMessage('Witaj');

while (end) {
end -= 1;
tmp += end;
if (end === 5e7) { // 5e7 to połowa 1e8
postMessage('Gotowe w połowie, tmp wynosi ' + tmp);
}
}

postMessage('Wszystko gotowe');

Skrypt obliczeniowy wykorzystuje funkcję postMessage() do komunikacji ze stroną, a strona


uzyskuje dane, nasłuchując zdarzenia onmessage. Funkcja wywołania zwrotnego przypisana
do onmessage otrzymuje obiekt zdarzenia zawierający właściwość data z dowolnymi danymi
przesyłanymi przez skrypt. W identyczny sposób strona może przekazać informacje skryp-
towi, wywołując metodę ww.postMessage(), a ten może ją odebrać, nasłuchując zdarzenia
onmessage.

Przedstawiony przykład wyświetli w przeglądarce internetowej następujące teksty:


komunikat z wątku obliczeniowego: Witaj
komunikat z wątku obliczeniowego: Gotowe w połowie, tmp wynosi 3749999975000001
komunikat z wątku obliczeniowego: Wszystko gotowe

Komunikacja z serwerem
Dzisiejsze aplikacje internetowe bardzo często stosują komunikację z serwerem bez ponow-
nego wczytywania całej strony WWW. Dzięki temu możliwe stało się tworzenie stron przy-
pominających tradycyjne aplikacje i uzyskiwanie bardzo dużej szybkości reakcji. Przyjrzyjmy
się kilku sposobom komunikacji z serwerem z poziomu języka JavaScript.

Komunikacja z serwerem | 179


Obiekt XMLHttpRequest
Obiekt XMLHttpRequest to szczególna funkcja konstruująca dostępna w większości stosowa-
nych obecnie przeglądarek internetowych, która umożliwia wysyłanie żądań HTTP wprost
z poziomu kodu JavaScript. Do wykonania żądania potrzeba trzech kroków.
1. Utworzenie obiektu XMLHttpRequest (nazywanego w skrócie XHR).
2. Zapewnienie funkcji wywołania zwrotnego, która zostanie wywołana w momencie każ-
dej zmiany stanu obiektu.
3. Wysłanie żądania HTTP.
Pierwszy krok jest prosty:
var xhr = new XMLHttpRequest();

Przed IE w wersji 7. funkcjonalność XHR została zaimplementowana jako obiekt ActiveX,


więc w tym przypadku trzeba zastosować inne rozwiązanie.
Drugi krok to zapewnienie funkcji wywołania zwrotnego dla zdarzenia readystatechange:
xhr.onreadystatechange = handleResponse;

Ostatni krok to wysłanie żądania, które wymaga wywołania dwóch metod: open() i send().
Metoda open() określa rodzaj żądania HTTP (na przykład POST lub GET) i adres URL. Metoda
send() przyjmuje dane do wysłania w typie POST lub nie przyjmuje żadnych parametrów
w typie GET. Ostatni parametr metody open() określa, czy żądanie powinno zostać wykona-
ne asynchronicznie, czyli bez blokowania przeglądarki aż do czasu otrzymania odpowiedzi.
Oczywiście jest to znacznie lepsze rozwiązanie z punktu widzenia użytkownika i jeśli nie ist-
nieje bardzo poważny powód, by było inaczej, parametr dotyczący asynchroniczności zawsze
powinien mieć wartość true:
xhr.open("GET", "page.html", true);
xhr.send();

Poniżej znajduje się pełny kod przykładu, który pobiera zawartość nowej podstrony i wyświetla
ją na aktualnej stronie (przykład jest dostępny pod adresem http://www.jspatterns.com/book/8/xhr.html).
var i, xhr, activeXids = [
'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP',
'Microsoft.XMLHTTP'
];

if (typeof XMLHttpRequest === "function") { // wbudowany obiekt XHR


xhr = new XMLHttpRequest();
} else { // IE przed wersją 7.
for (i = 0; i < activeXids.length; i += 1) {
try {
xhr = new ActiveXObject(activeXids[i]);
break;
} catch (e) {}
}
}

xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) {
return false;
}
if (xhr.status !== 200) {

180 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


alert("Błąd, kod statusu: " + xhr.status);
return false;
}
document.body.innerHTML += "<pre>" + xhr.responseText + "<\/pre>";
};

xhr.open("GET", "page.html", true);


xhr.send("");

Oto kilka komentarzy dotyczących przedstawionego kodu:


• Z powodu istnienia IE w wersji 6. lub starszej proces tworzenia obiektu XHR jest nieco
bardziej złożony. Kod przechodzi w pętli przez listę identyfikatorów ActiveX, zaczynając
od wersji najnowszej i starając się utworzyć obiekt w bloku try-catch.
• Funkcja wywołania zwrotnego sprawdza właściwość readyState obiektu xhr. Istnieje
pięć możliwych wartości od 0 do 4, gdzie 4 oznacza „ukończono”. W przypadku innego
stanu kod kontynuuje oczekiwanie na zakończenie żądania HTTP.
• Wywołanie zwrotne sprawdza także właściwość status obiektu xhr. Właściwość ta odpo-
wiada kodowi statusu HTTP, na przykład 200 (OK) lub 404 (nie znaleziono). Kod zaintere-
sowany jest tylko odpowiedzią 200 i wszystkie inne traktuje jako błąd (to pewne uprosz-
czenie, bo istnieje znacznie więcej statusów oznaczających prawidłowe zadziałanie żądania).
• Kod za każdym razem sprawdza, jaki jest prawidłowy sposób tworzenia obiektu XHR.
Wykorzystanie wzorców z poprzednich rozdziałów (na przykład usuwania zbędnej wersji
kodu) pozwoliłoby wykonać taki test tylko raz.

JSONP
JSONP (JSON with Padding) to inny sposób na tworzenie żądań HTTP. W odróżnieniu od XHR
nie jest domyślnie ograniczony do domeny, w której znajduje się oryginalna strona, więc należy
uważać na to podejście przy danych pobieranych z zewnętrznych stron (dosyć łatwo można
wykonać niebezpieczny kod).
Odpowiedzią na żądanie XHR może być dowolny dokument:
• dokument XML (historyczne);
• fragment HTML (stosunkowo popularne);
• dane JSON (lekkie i wygodne);
• proste pliki tekstowe lub inne.

W przypadku JSONP dane są zazwyczaj przesyłane w formacie JSON wraz z wywołaniem


funkcji, która została określona w żądaniu.
Adres URL żądania JSONP może mieć postać:
http://przyklad.pl/getdata.php?callback=myHandler
Ścieżka getdata.php to jedynie przykład — może ona mieć postać dowolnej innej strony lub
skryptu. Parametr callback określa nazwę funkcji JavaScript, która obsłuży odpowiedź.
Adres URL zostaje wczytany za pomocą dynamicznie tworzonego elementu <script>.
var script = document.createElement("script");
script.src = url;
document.body.appendChild(script);

Komunikacja z serwerem | 181


Serwer odpowiada danymi JSON przekazanymi jako parametr do funkcji wywołania zwrot-
nego. Cała operacja to w rzeczywistości wstawienie na stronie nowego skryptu, który tak na-
prawdę jest wywołaniem funkcji:
myHandler({"witaj": "świecie"});

Przykład JSONP — kółko i krzyżyk


Prześledźmy działanie JSONP na prostym przykładzie — grze w kółko i krzyżyk, w której
graczami są klient (przeglądarka) i serwer. Obaj gracze generują losowe liczby między 1 i 9.
Do pobrania wartości wygenerowanej przez serwer posłuży JSONP. Rysunek 8.2 przedsta-
wia grę w akcji.
Można w nią zagrać pod adresem http://www.jspatterns.com/book/8/ttt.html.

Rysunek 8.2. Gra w kółko i krzyżyk wykorzystująca JSONP


Strona zawiera dwa przyciski: rozpoczęcia nowej gry i prośby o wykonanie ruchu przez serwer
(ruch klienta zostanie wykonany automatycznie z niewielkim opóźnieniem).
<button id="new">Nowa gra</button>
<button id="server">Ruch serwera</button>

Tablica zawiera dziewięć komórek z odpowiednimi identyfikatorami. Oto jej fragment:


<td id="cell-1">&nbsp;</td>
<td id="cell-2">&nbsp;</td>
<td id="cell-3">&nbsp;</td>
...

Cały kod gry znajduje się w obiekcie globalnym ttt.


var ttt = {
// komórki wypełnione do tej pory
played: [],

// funkcja pomocnicza
get: function (id) {
return document.getElementById(id);
},

// obsługa kliknięć
setup: function () {
this.get('new').onclick = this.newGame;
this.get('server').onclick = this.remoteRequest;
},

182 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


// wyczyszczenie tablicy
newGame: function () {
var tds = document.getElementsByTagName("td"),
max = tds.length,
i;
for (i = 0; i < max; i += 1) {
tds[i].innerHTML = "&nbsp;";
}
ttt.played = [];
},

// wykonanie żądania
remoteRequest: function () {
var script = document.createElement("script");
script.src = "server.php?callback=ttt.serverPlay&played=" +
´ttt.played.join(',');
document.body.appendChild(script);
},

// wywołanie zwrotne, czyli odpowiedź z serwera


serverPlay: function (data) {
if (data.error) {
alert(data.error);
return;
}
data = parseInt(data, 10);
this.played.push(data);

this.get('cell-' + data).innerHTML = '<span class="server">X<\/span>';

setTimeout(function () {
ttt.clientPlay();
}, 300); // udawanie ciężkich obliczeń
},

// ruch klienta
clientPlay: function () {
var data = 5;

if (this.played.length === 9) {
alert("Koniec gry");
return;
}

// generuj wartości od 1 do 9
// aż do znalezienia pustej komórki
while (this.get('cell-' + data).innerHTML !== "&nbsp;") {
data = Math.ceil(Math.random() * 9);
}
this.get('cell-' + data).innerHTML = 'O';
this.played.push(data);
}
};

Obiekt ttt przechowuje listę wypełnionych komórek we właściwości ttt.played i wysyła ją


do serwera, by ten mógł zwrócić liczbę, która nie została jeszcze wybrana. W przypadku błędu
serwer prześle komunikat podobny do następującego:
ttt.serverPlay({"error": "Opis błędu"});

Wywołaniem zwrotnym w JSONP musi być publicznie i globalnie dostępna funkcja, ale nie
musi to być zwykła funkcja globalna, a na przykład metoda obiektu globalnego. Jeśli nie było
błędów, serwer odpowie metodą taką jak poniższa.

Komunikacja z serwerem | 183


ttt.serverPlay(3);

W tym przypadku 3 oznacza, że serwer wylosował komórkę o tym numerze. Ponieważ dane
są niezwykle proste, nie trzeba nawet używać formatu JSON — wystarczy pojedyncza liczba.

Ramki i wywołania jako obrazy


Alternatywnym sposobem komunikowania się z serwerem jest zastosowanie ramek. W języku
JavaScript można utworzyć element iframe i zmieniać jego atrybut src (adres URL). Nowy
adres może zawierać dane i aktualizować wywołującego (główną stronę).
Najprostsza postać komunikacji zachodzi wtedy, gdy jedyną operacją jest wysyłka danych do
serwera bez potrzeby sprawdzania odpowiedzi. W takiej sytuacji wystarczy utworzyć nowy
obiekt obrazu i ustawić jego atrybut src na skrypt po stronie serwera:
new Image().src = "http://przyklad.pl/strona/www.php";

To tak zwane wywołanie jako obraz, które przydaje się, gdy chcemy jedynie przesłać dane
do zapamiętania przez serwer (na przykład w celu zebrania danych statystycznych dotyczą-
cych odwiedzin). Ponieważ odpowiedź nie jest odczytywana, najczęściej wysyła się obraz
GIF o wymiarach 1×1. Jest to antywzorzec — zamiast tego lepiej wysłać kod odpowiedzi
HTTP 204, który oznacza brak zawartości. Dzięki temu klient pobierze jedynie nagłówek bez
danych obrazka.

Serwowanie kodu JavaScript klientom


Istnieje kilka istotnych aspektów wydajnościowych, o których warto pamiętać, przesyłając
kod JavaScript klientom. Prześledźmy najważniejsze z nich na nieco wyższym poziomie.
Szczegółowy opis przedstawionych technik znajduje się w książkach Wydajne witryny internetowe.
Przyspieszanie działania serwisów WWW i Jeszcze wydajniejsze witryny internetowe. Przyspieszanie
działania serwisów WWW wydanych przez wydawnictwo Helion.

Łączenie skryptów
Pierwszą zasadą budowania szybko wczytujących się stron WWW jest stosowanie jak naj-
mniejszej liczby komponentów zewnętrznych, bo żądania HTTP są kosztowne. W przypadku
JavaScriptu oznacza to, że można znacząco przyspieszyć wczytywanie stron, łącząc ze sobą
kilka skryptów w jeden plik.
Przypuśćmy, że witryna korzysta z biblioteki jQuery. To jeden plik .js. Dodatkowo używa
ona kilku modułów jQuery, a każdy z nich znajduje się w osobnym pliku. W ten sposób bar-
dzo łatwo uzyskać kilka plików, nie napisawszy jeszcze ani jednego wiersza własnego kodu.
Połączenie wszystkich wymienionych plików w jeden ma spory sens, szczególnie że niektóre
z nich są niewielkie (2 lub 3 kB), więc koszt samej komunikacji HTTP byłby większy od kosztu
ich pobrania. Łączenie skryptów oznacza po prostu utworzenie nowego pliku zawierającego
kod ze wszystkich pozostałych.
Oczywiście to połączenie powinno nastąpić dopiero przed przesłaniem plików do systemu
produkcyjnego, a nie w trakcie prac programistycznych, gdyż mogłoby wtedy znacząco
utrudnić testowanie.

184 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


Łączenie skryptów ma kilka wad.
• To dodatkowy krok przed umieszczeniem wszystkiego na serwerze, ale stosunkowo łatwo
go zautomatyzować z poziomu wiersza poleceń, na przykład stosując polecenie cat:
$ cat jquery.js jquery.quickselect.js jquery.limit.js > all.js
• Następuje częściowa utrata zalet pamięci podręcznej przeglądarki — dokonanie drob-
nych zmian w jednym ze złączonych plików wymaga podmiany połączonej wersji nowszą.
Z tego powodu w większych projektach lepiej jest określić sztywne daty przenosin kodu
na serwer (na przykład każdy wtorek) lub rozważyć zastosowanie dwóch plików z kodem:
pierwszego z kodem podlegającym częstym modyfikacjom i drugiego, który praktycznie
się nie zmienia.
• Wynikowy plik musi stosować konwencję nazewnictwa ułatwiającą jego łatwą podmianę,
czyli musi zawierać numer wersji, na przykład w postaci daty (all_20111013.js) lub w postaci
MD5 z zawartości pliku.
Przedstawione wady po ich połączeniu są tak naprawdę drobną niewygodą, która niknie
przy porównaniu jej z zaletami.

Minifikacja i kompresja
W rozdziale 2. wyjaśniono znaczenie i działanie minifikacji kodu. Powinna ona stanowić
część całego procesu umieszczania kodu na serwerze produkcyjnym.
Patrząc na to z perspektywy użytkowników, nie ma powodu, dla którego powinni oni pobie-
rać wszystkie komentarze umieszczone w kodzie, które w żaden sposób nie przyczyniają się
do działania aplikacji.
Korzyści z minifikacji mogą być większe lub mniejsze w zależności od tego, jak intensywnie
korzysta się z komentarzy i białych spacji, a także jak bardzo zaawansowanych narzędzi do
minifikacji się używa. Najczęściej poziom redukcji rozmiaru pliku oscyluje w okolicach 50%.
Udostępnianie plików z kodem w wersji skompresowanej to kolejne rozwiązanie, z którego
powinno się zawsze korzystać. To prosta pojedyncza konfiguracja serwera, która włącza
kompresję gzip i zapewnia natychmiastowe przyspieszenie. Nawet jeśli korzysta się z usług
zewnętrznej firmy hostingowej, która nie daje dużej swobody konfiguracyjnej, najczęściej
przynajmniej ma się możliwość użycia własnych plików konfiguracyjnych Apache o nazwie
.htaccess. W głównym folderze z serwowaną zawartością umieść plik .htaccess o następującej
treści:
AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml
application/javascript application/json

Kompresja spowoduje przesyłanie plików zmniejszonych średnio o 70%. Gdy połączyć to


z minifikacją, okaże się, że klienci pobierają jedynie 15% oryginalnego rozmiaru plików z kodem
źródłowym.

Nagłówek Expires
Wbrew popularnemu przesądowi pobrane pliki nie pozostają zbyt długo w pamięci pod-
ręcznej przeglądarki. Aby zwiększyć szansę na to, że użytkownik będzie miał tam niezbędne
pliki przy ponownej wizycie w portalu, warto zastosować nagłówek Expires.

Serwowanie kodu JavaScript klientom | 185


Podobnie jak w przypadku kompresji wystarczy użyć prostej opcji w pliku .htaccess:
ExpiresActive On
ExpiresByType application/x-javascript "access plus 10 years"

Wadą tego rozwiązania jest fakt, iż po każdej zmianie zawartości pliku trzeba również zmie-
nić jego nazwę. Nie stanowi to jednak dużego problemu, jeśli wcześniej ustaliło się jednolitą
konwencję dla nazw plików zawierających połączony kod źródłowy.

Wykorzystanie CDN
CDN to skrót od Content Delivery Network (sieć dostarczania treści). To płatna (czasem nawet
sporo) usługa hostingowa, która dystrybuuje kopie plików z wielu różnych lokalizacji na
świecie, co pozwala szybciej dostarczyć je użytkownikom końcowym przy jednoczesnym
zachowaniu dla nich tego samego adresu URL.
Nawet jeśli nie ma się budżetu na CDN, nadal do pewnego stopnia można skorzystać z niego
w przypadku niektórych ogólnodostępnych plików:
• Google hostuje za pomocą własnego CDN kilka popularnych bibliotek JavaScript, z któ-
rych można skorzystać bez opłat.
• Microsoft hostuje jQuery i własne biblioteki Ajax.
• Yahoo! hostuje bibliotekę YUI na własnym CDN.

Strategie wczytywania skryptów


Sposób umieszczania skryptu na stronie WWW na pierwszy rzut oka wydaje się być wyjąt-
kowo prostym zagadnieniem — wystarczy użyć elementu <script> i umieścić kod wewnątrz
niego lub wskazać osobny plik do wczytania za pomocą atrybutu src.
// pierwsze rozwiązanie
<script>
console.log("Witaj, świecie!");
</script>
// drugie rozwiązanie
<script src="external.js"></script>

Istnieje jednak kilka wzorców oraz sztuczek, o których warto pamiętać, jeśli celem jest two-
rzenie wysoce wydajnych aplikacji.
Oto kilka typowych atrybutów stosowanych przez większość programistów wraz z elementem
<script>:
• language="JavaScript" — istnieje w wielu wersjach z różną pisownią słowa JavaScript,
a czasem nawet z numerem wersji. Atrybut ten nie powinien być stosowany, bo domyślnym
językiem jest zawsze JavaScript, a numer wersji najczęściej nie działa prawidłowo i w za-
sadzie jest błędem projektowym.
• type="text/javascript" — atrybut ten jest wymagany przez HTML4 i XHTML1, choć
tak naprawdę nie powinien, gdyż wszystkie przeglądarki i tak zakładają język JavaScript.
HTML5 zniósł obowiązek jego podawania, a stosowanie go w starszych wersjach języka
ma na celu jedynie usatysfakcjonowanie walidatorów.

186 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


• defer — (lub w przypadku HTML5 async) to sposób (choć niezbyt powszechnie obsłu-
giwany) na wskazanie, że pobieranie zewnętrznego skryptu nie powinno blokować po-
zostałej części strony (więcej na ten temat w dalszej części podrozdziału).

Lokalizacja elementu <script>


Elementy <script> blokują progresywne pobieranie zawartości strony. Przeglądarki pobie-
rają jednocześnie kilka elementów, ale gdy napotkają zewnętrzny skrypt, zatrzymują dalsze
pobieranie, by go pobrać, przeanalizować i wykonać. Nie wpływa to najlepiej na ogólny czas
wczytywania strony, szczególnie jeśli sytuacja powtarza się w kodzie wielokrotnie.
Aby zminimalizować efekt blokowania, można umieścić element skryptu pod koniec strony,
najlepiej tuż przed zamykającym znacznikiem </body>. Dzięki temu skrypt nie zablokuje
żadnych pozostałych zasobów — wczytają się one wcześniej, a użytkownik nie będzie musiał
czekać na pierwsze elementy.
Najgorszym antywzorcem jest zastosowanie kilku osobnych plików na początku dokumentu.
<!doctype html>
<html>
<head>
<title>Moja aplikacja</title>
<!-- ANTYWZORZEC -->
<script src="jquery.js"></script>
<script src="jquery.quickselect.js"></script>
<script src="jquery.lightbox.js"></script>
<script src="myapp.js"></script>
</head>
<body>
...
</body>
</html>

Lepszym rozwiązaniem jest połączenie wszystkich plików w jeden.


<!doctype html>
<html>
<head>
<title>Moja aplikacja</title>
<script src="all_20111013.js"></script>
</head>
<body>
...
</body>
</html>

Jeszcze lepsze jest umieszczenie połączonych skryptów na samym końcu strony.


<!doctype html>
<html>
<head>
<title>Moja aplikacja</title>
</head>
<body>
...
<script src="all_20111013.js"></script>
</body>
</html>

Strategie wczytywania skryptów | 187


Wysyłanie pliku HTML fragmentami
Protokół HTTP obsługuje przesyłanie plików fragmentami bez podawania ich pełnej długości.
Jeśli więc istnieje pewna złożona strona WWW, nie trzeba czekać na wykonanie wszystkich
działań serwerowych przed rozpoczęciem wysyłania wyniku do klienta, szczególnie jeśli po-
czątkowa część strony (nagłówek) jest niezmienna.
Najprostsza strategia polega na wysłaniu części <head> dokumentu w pierwszym pakiecie,
a pozostałej części dopiero po zebraniu przez serwer wszystkich niezbędnych informacji.
Innymi słowy, wysłany zostanie kod:
<!doctype html>
<html>
<head>
<title>Moja aplikacja</title>
</head>
<!-- koniec części pierwszej -->
<body>
...
<script src="all_20111013.js"></script>
</body>
</html>
<!-- koniec części drugiej -->

Prostym usprawnieniem byłoby przeniesienie kodu JavaScript z powrotem do części na-


główkowej, bo w ten sposób przeglądarka rozpoczęłaby pobieranie skryptów, zanim jeszcze
otrzymałaby właściwą część strony.
<!doctype html>
<html>
<head>
<title>Moja aplikacja</title>
<script src="all_20111013.js"></script>
</head>
<!-- koniec części pierwszej -->
<body>
...
</body>
</html>
<!-- koniec części drugiej -->

Jeszcze lepsze byłoby zastosowanie trzech fragmentów. Ostatni z nich zawierałby tylko i wy-
łącznie informację o skrypcie. Dodatkowo w pierwszym fragmencie warto byłoby wysłać
statyczny nagłówek (na przykład logo) znajdujący się na każdej stronie WWW witryny.
<!doctype html>
<html>
<head>
<title>Moja aplikacja</title>
</head>
<body>
<div id="header">
<img src="logo.png" />
...
</div>
<!-- koniec części pierwszej -->
... The full body of the page ...

<!-- koniec części drugiej -->


<script src="all_20100426.js"></script>
</body>
</html>
<!-- koniec części trzeciej -->

188 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


To rozwiązanie doskonale wpasowuje się w ducha progresywnego rozszerzania i rozszerza-
nia podstawowej funkcjonalności przy użyciu języka JavaScript. Tuż po otrzymaniu przez
klienta drugiego fragmentu witryna powinna być w pełni funkcjonalna i działać dokładnie
tak samo, jakby w przeglądarce wyłączono obsługę języka JavaScript. Gdy do klienta dotrze
trzeci fragment i zostanie on wczytany, wzbogaci stronę, dodając wszystkie rozszerzenia
i ułatwienia.

Dynamiczne elementy <script>


zapewniające nieblokujące pobieranie
Jak wcześniej wspomniano, JavaScript blokuje pobieranie plików, które znajdują się za nim.
Istnieje jednak kilka rozwiązań pozwalających ominąć to ograniczenie:
• Wczytanie skryptu przy użyciu żądania XHR i wykonanie go poleceniem eval(). To po-
dejście generalnie ma zastosowanie ograniczone do tej samej domeny i dodatkowo ko-
rzysta z eval(), co samo w sobie jest antywzorcem.
• Użycie atrybutów defer i async, ale te nie działają we wszystkich przeglądarkach.
• Dynamiczne wstawianie elementu <script>.

Ostatnie rozwiązanie to ciekawy wzorzec. Podobnie jak we wcześniejszym przykładzie z JSONP,


tworzy się nowy element skryptu, ustawia jego atrybut src i dołącza się go do strony.
Poniższy fragment kodu wczyta plik JavaScript asynchronicznie bez blokowania pobierania
pozostałej części strony.
var script = document.createElement("script");
script.src = "all_20100426.js";
document.documentElement.firstChild.appendChild(script);

Wadą tego rozwiązania jest fakt, że nie można stosować żadnych innych skryptów wczyty-
wanych tradycyjnie, które wykorzystują elementy z dynamicznie wczytywanego pliku .js.
Ponieważ główny plik .js jest wczytywany asynchronicznie, nie ma żadnej gwarancji, że zostanie
wczytany w określonym czasie, więc skrypt wykonywany tuż po nim nie może zakładać istnienia
definiowanych przez niego obiektów.
Aby wyeliminować tę wadę, można zebrać wszystkie skrypty wstawione na stronie i wstawić
je jako funkcje tablicy. Gdy główny skrypt zostanie pobrany przez przeglądarkę, może wy-
konać wszystkie zebrane w tablicy funkcje. Całe zadanie będzie składało się z trzech kroków.
Najpierw, najlepiej jak najwyżej w kodzie strony, trzeba utworzyć tablicę przechowującą
bezpośrednio wstawiony kod.
var mynamespace = {
inline_scripts: []
};

Wszystkie pojedyncze skrypty należy otoczyć funkcjami i umieścić jako elementy w tablicy
inline_scripts. Innymi słowy:
// było:
// <script>console.log("Jestem kodem w pliku HTML");</script>

// jest:
<script>
mynamespace.inline_scripts.push(function () {

Strategie wczytywania skryptów | 189


console.log("Jestem kodem w pliku HTML");
});
</script>

W ostatnim kroku główny skrypt wykonuje w pętli wszystkie skrypty zebrane w tablicy.
var i, scripts = mynamespace.inline_scripts, max = scripts.length;
for (i = 0; i < max; max += 1) {
scripts[i]();
}

Dodanie elementu <script>


Najczęściej dołącza się skrypty do elementu <head> dokumentu, ale tak naprawdę można
dołączyć je do dowolnego elementu, na przykład do <body> (jak w przykładzie z JSONP).
W poprzednim przykładzie do dołączenia skryptów do <head> wykorzystano documentElement
— ponieważ documentElement wskazywał na element <html>, jego pierwszym potomkiem
było <head>.
document.documentElement.firstChild.appendChild(script);

Inny często stosowany zapis to:


document.getElementsByTagName("head")[0].appendChild(script);

Są to rozwiązania odpowiednie, gdy ma się pełną kontrolę nad kodem strony. Przypuśćmy
jednak, że tworzymy widget lub reklamę, która może znaleźć się na bardzo różnych stronach
WWW. Teoretycznie na stronie nie musi istnieć ani <head>, ani <body>, ale document.body
powinno działać prawidłowo nawet pomimo jawnego użycia elementu:
document.body.appendChild(script);

Istnieje jednak jeden znacznik, który musi istnieć na stronie, skoro wykonuje się skrypt —
znacznik skryptu. W przeciwnym razie nie byłoby żadnego powiązania kodu ze stroną.
Wykorzystując ten fakt, można użyć metody insertBefore() dla pierwszego istniejącego na
stronie elementu skryptu.
var first_script = document.getElementsByTagName('script')[0];
first_script.parentNode.insertBefore(script, first_script);

Zmienna first_script zawiera element skryptu, który strona musi posiadać, natomiast
zmienna script zawiera nowy element skryptu dodawany do strony.

Wczytywanie leniwe
Technika wczytywania leniwego dotyczy sytuacji, w której to zewnętrzne skrypty wczyty-
wane są po zdarzeniu load strony. Czasem warto rozbić kod na dwie części:
• Pierwsza część jest niezbędna do prawidłowej inicjalizacji kodu i przypisania zdarzeń do
elementów interfejsu.
• Część druga potrzebna jest dopiero po akcji użytkownika lub po wystąpieniu innych wa-
runków.
Celem jest jak najszybsze wczytanie strony w sposób progresywny i zajęcie czymś użytkow-
nika możliwie wcześnie. Pozostały kod wczytuje się w tle, gdy użytkownik jest zajęty i roz-
gląda się po stronie.

190 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


Wczytanie drugiej części kodu JavaScript wymaga jedynie dodania elementu skryptu do części
nagłówkowej lub głównej strony po zajściu zdarzenia wczytania.
... Właściwa treść strony WWW ...

<!-- koniec części drugiej -->


<script src="all_20100426.js"></script>
<script>
window.onload = function () {
var script = document.createElement("script");
script.src = "all_lazy_20100426.js";
document.documentElement.firstChild.appendChild(script);
};
</script>
</body>
</html>
<!-- koniec części trzeciej -->

Prawie we wszystkich aplikacjach leniwa część kodu jest większa niż część podstawowa, po-
nieważ interesujące działania takie jak przenoszenie elementów, użycie XHR i odtwarzanie
animacji zazwyczaj wykonuje się dopiero po akcji użytkownika.

Wczytywanie na żądanie
Poprzedni wzorzec wczytywał dodatkowy kod JavaScript bezwarunkowo po załadowaniu
strony WWW, zakładając, że kod ten będzie najprawdopodobniej potrzebny. Czy nie można
jednak zrobić lepiej? W wielu sytuacjach nic nie stoi na przeszkodzie, by wczytywać frag-
menty kodu tylko wtedy, gdy są naprawdę potrzebne.
Wyobraźmy sobie pasek boczny z kilkoma zakładkami. Kliknięcie zakładki powoduje wy-
słanie żądania XHR, by pobrać nową zawartość i zaanimować zmianę danych po uzyskaniu
odpowiedzi. Załóżmy, że zakładki to jedyne miejsce stosujące żądanie XHR i animacje. Czy
naprawdę trzeba pobierać ten kod, jeśli użytkownik nigdy nie przełączy zakładek?
Najwyższy czas na wzorzec wczytywania na żądanie. Można utworzyć funkcję require(),
która będzie przyjmowała nazwę pliku do wczytania, i funkcję wywołania zwrotnego, która
zostanie uruchomiona po wczytaniu skryptu.
Oto przykład użycia wspomnianej funkcji require():
require("extra.js", function () {
functionDefinedInExtraJS();
});

Jak można taką funkcję zaimplementować? Żądanie pobrania i wykonania dodatkowego ko-
du nie stanowi problemu — to dobrze znany wzorzec dynamicznego umieszczania elementu
<script>. Określenie momentu wczytania skryptu jest nieco bardziej złożone, głównie ze
względu na różnice między przeglądarkami.
function require(file, callback) {

var script = document.getElementsByTagName('script')[0],


newjs = document.createElement('script');

// IE
newjs.onreadystatechange = function () {
if (newjs.readyState === 'loaded' || newjs.readyState === 'complete') {
newjs.onreadystatechange = null;
callback();

Strategie wczytywania skryptów | 191


}
};

// inne
newjs.onload = function () {
callback();
};

newjs.src = file;
script.parentNode.insertBefore(newjs, script);
}

Kilka komentarzy na temat implementacji:


• W IE trzeba zarejestrować się jako odbiorca zdarzenia readystatechange, a następnie
sprawdzać istnienie tekstów "loaded" lub "complete" we właściwości readyState.
Wszystkie inne przeglądarki pominą ten kod.
• W przeglądarkach Firefox, Safari, Chrome i Opera zarejestrowanie się jako odbiorca do-
tyczy zdarzenia load (właściwość onload).
• Przedstawione rozwiązania nie zadziałają poprawnie dla przeglądarki Safari 2. Jeśli jej
obsługa jest niezbędna, trzeba okresowo sprawdzać, czy została zdefiniowana określona
zmienna (ustawiana przez wczytywany kod). Jej ustawienie oznacza, że nowy skrypt
został wczytany poprawnie.
Implementację można przetestować, stosując skrypt ze sztucznym opóźnieniem (symulujący
długie pobieranie zawartości) i nazwie ondemandjs.php.
<?php
header('Content-Type: application/javascript');
sleep(1);
?>
function extraFunction(logthis) {
console.log('wczytane i wykonane');
console.log(logthis);
}

Oto krótki test funkcji require():


require('ondemand.js.php', function () {
extraFunction('wczytane ze strony głównej');
document.body.appendChild(document.createTextNode('koniec!'));
});

Przedstawione fragmenty kodu spowodują wyświetlenie w konsoli dwóch wierszy tekstu


i umieszczenie na stronie tekstu „koniec!”. W pełni działający przykład jest dostępny pod
adresem http://www.jspatterns.com/book/8/ondemand.html.

Wstępne wczytywanie kodu JavaScript


Wcześniejsze wzorce starały się opóźnić wczytywanie kodu, którego wykonanie było nie-
zbędne na aktualnej stronie. Czasem warto również w trakcie korzystania przez użytkownika
z bieżącej strony wczytać kod, który będzie najprawdopodobniej potrzebny na stronie na-
stępnej. Gdy użytkownik zdecyduje się odwiedzić drugą stronę, dotyczący jej kod będzie już
znajdował się w pamięci przeglądarki, więc uzyska się wrażenie znacznie szybszego działa-
nia witryny.

192 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


Wczytywanie wstępne można zaimplementować za pomocą wzorca skryptów dynamicz-
nych. Oznacza to jednak, że skrypt zostanie przeanalizowany i wykonany. Analiza to naj-
częściej jedynie ułamek czasu spędzonego na pobieraniu skryptu, ale wykonanie go może
spowodować pojawienie się błędów (bo kod będzie zakładał jego uruchomienie na drugiej
stronie).
Możliwe jest pobranie skryptów do pamięci podręcznej przeglądarki bez ich wykonywania.
Podobna sztuczka działa również z plikami CSS i obrazami.
W IE zadanie jest bardzo proste i polega na użyciu znanego już wybiegu ze wzorcem logo-
wania danych na serwerze.
new Image().src = "preloadme.js";

We wszystkich innych przeglądarkach niezbędne jest użycie zamiast elementu skryptu ele-
mentu <object> i ustawienie jego atrybutu data na adres URL skryptu.
var obj = document.createElement('object');
obj.data = "preloadme.js";
document.body.appendChild(obj);

Aby obiekt nie był widoczny na stronie, warto ustawić jego szerokość (atrybut width) i wy-
sokość (atrybut height) na 0.
Stosunkowo łatwo utworzyć ogólną funkcję preload(), a także wykorzystać wzorzec usu-
wania niepotrzebnego kodu (rozdział 4.), by obsłużyć różnice między przeglądarkami.
var preload;
if (/*@cc_on!@*/false) { // wykrywanie IE za pomocą komentarzy warunkowych
preload = function (file) {
new Image().src = file;
};
} else {
preload = function (file) {
var obj = document.createElement('object'),
body = document.body;
obj.width = 0;
obj.height = 0;
obj.data = file;
body.appendChild(obj);
};
}

Wykorzystanie nowej funkcji jest bardzo proste:


preload("my_web_worker.js");

Wadą przedstawionego rozwiązania jest istnienie wykrywania agenta użytkownika, ale


niestety nie można tego uniknąć, bo zwykłe wykrywanie możliwości niewiele w tej sytu-
acji pomoże. W teorii można by sprawdzić, czy Image jest funkcją (typeof Image ==
"function" ). Niestety, wszystkie przeglądarki obsługują new Image() , ale część z nich
stosuje specjalne bufory dla obrazków, więc próba wczytania za pomocą takiego obiektu
czegoś, co obrazem nie jest (czyli skryptu), nie zadziała zgodnie z oczekiwaniami i spo-
woduje ponowne pobranie pliku.

Strategie wczytywania skryptów | 193


Wykrywanie rodzaju przeglądarki za pomocą komentarzy warunkowych to bardzo
interesująca technika. Jest bezpieczniejsza od sprawdzania tekstu navigator.userAgent,
bo ten użytkownik może dowolnie zmienić.
Zapis
var isIE = /*@cc_on!@*/false;
spowoduje ustawienie isIE na wartość false we wszystkich przeglądarkach (bo
zignorują one komentarz) poza IE, która przypisze wartość true ze względu na
negację zastosowaną w komentarzu warunkowym. Innymi słowy, IE wykona na-
stępujący kod:
var isIE = !false;

Wzorzec wczytywania wstępnego można zastosować dla różnych komponentów, nie tylko
dla skryptów. Przykładem może być strona logowania. Gdy użytkownik rozpoczyna wpisy-
wanie swojego imienia i nazwiska, można rozpocząć wstępne wczytywanie dodatkowych
danych dla następnej strony (oczywiście poza danymi uzależnionymi od użytkownika), bo
jest wielce prawdopodobne, że za chwilę będzie ona stroną bieżącą.

Podsumowanie
Podczas gdy poprzednie rozdziały książki dotyczyły przede wszystkim podstawowych wzor-
ców języka JavaScript niezależnych od środowiska uruchomieniowego, w niniejszym rozdziale
skupiliśmy się na wzorcach związanych tylko i wyłącznie z przeglądarkami internetowymi.
Poruszane były następujące tematy:
• Właściwy podział zadań (HTML — treść, CSS — prezentacja, JavaScript — zachowanie),
zastosowanie języka JavaScript jako rozszerzenia funkcjonalności i wykrywanie możli-
wości zamiast wersji przeglądarki (choć pod koniec rozdziału pojawiła się sytuacja wy-
magająca złamania tego wzorca).
• Kod używający DOM, czyli wzorce pozwalające przyspieszyć uzyskiwanie dostępu i wpro-
wadzanie modyfikacji do DOM głównie przez grupowanie zadań, gdyż każdy dostęp
do DOM jest kosztowny.
• Zdarzenia, obsługiwanie ich w uniwersalny sposób i wykorzystanie delegacji zdarzeń do
redukcji liczby przypisywanych funkcji obsługi zdarzeń i poprawy wydajności.
• Dwa wzorce pomagające radzić sobie z długimi i kosztownymi obliczeniami: wykorzy-
stanie setTimeout() do podziału obliczeń na mniejsze fragmenty i użycie w nowocze-
snych przeglądarkach dodatkowych wątków obliczeniowych.
• Różne wzorce komunikacji z serwerem bez ponownego wczytywania strony — XHR,
JSONP, ramki i obrazki.
• Kroki niezbędne do prawidłowego wdrożenia języka JavaScript w środowisku produk-
cyjnym — upewnianie się, że skrypty są łączone ze sobą (mniej plików do pobrania),
minifikowane i kompresowane (oszczędność do 85%), a w sytuacji idealnej również
umieszczane na serwerze CDN i przesyłane z nagłówkami Expires.
• Wzorce umieszczania skryptów na stronie internetowej w sposób zapewniający jak naj-
lepszą wydajność: różne techniki umieszczania elementu <script>, wykorzystanie prze-
syłania stron WWW fragmentami, a także ograniczenie wpływu dużych skryptów na ogólny
czas wczytywania strony przez wczytywanie leniwe, wstępne i na żądanie.

194 | Rozdział 8. DOM i wzorce dotyczące przeglądarek


Skorowidz

.htaccess, 185 constructor, właściwość, 18, 126


@class, 43 Content Delivery Network, Patrz CDN
@method, 43 Crockford, Douglas, 19, 113
@namespace, 43 Curry, Haskell, 87
@param, 41, 43 currying, Patrz funkcje, rozwijanie
@return, 41, 43
<script>, 186, 187
dodawanie elementu, 190
D
dynamiczne elementy, 189 default, 32
lokalizacja, 187 dekoratora, wzorzec, 145, 169
implementacja, 146, 147, 148, 149
A delegacje zdarzeń, wzorzec, 177
delete, operator, 24
addEventListener(), 175 dir(), 20
alert(), 20 Document Object Model, Patrz DOM
antywzorce, 16 dodatki syntaktyczne, 113
Apache, .htaccess, 185 dokumentacja, 41
aplikacja JSDoc, 41
częściowa, 85, 86, 89 YUIDoc, 41, 42, 44
funkcji, 84, 85 DOM, 172
internetowa, 171 dostęp, 173
apply(), 85, 133, 134 modyfikacja, 174
arguments.callee, 55, 83 dorozumiane zmienne globalne, 23, 24
Array, 56, 57 dziedziczenie, 18, 136
asynchroniczne, zdarzenia, 73 klasyczne, 115, 116, 126
atrybut, 17 nowoczesne, 115, 116
attachEvent(), 175 prototypowe, 129, 130
przez kopiowanie właściwości, 131, 132
wielobazowe, 121
B
bąbelkowanie zdarzeń, 177 E
bind(), 135, 136
break, 32 ECMAScript 5, 18, 19
dodatki, 130
Error(), 62
C ES5, Patrz ECMAScript 5
call(), 133, 134 eval(), 19
case, 32 unikanie, 32, 33
CDN, 186 Expires, nagłówek, 185
Closure Compiler, 46, 80 extend(), 97, 132
console, obiekt, 20 extendDeep(), 97

195
F J
fabryki, wzorzec, 141, 142, 143, 169 JavaScript, 15
fasady, wzorzec, 152, 153, 169 biblioteki, 94
Firebug, 132 jako język obiektowy, 16
for, pętla, 27, 28 sprawdzanie jakości kodu, 19
for-in, pętla, 29 środowisko uruchomieniowe, 18
Function(), 33, 66 JavaScript Object Notation, Patrz JSON
Function.prototype.apply(), 84 jQuery, biblioteka, 59, 132
funkcje, 17, 65, 66 JSDoc, 41
anonimowe, 66, 68 JSLint, 19, 47
czasowe, 73 JSON, 58
deklaracje, 67, 68 JSON with Padding, Patrz JSONP
konstruujące, 51, 52 JSON.parse(), 58, 59
name, właściwość, 68 JSON.stringify(), 59
natychmiastowe, 76, 77, 78, 79, 89 JSONP, 181, 182, 183
obsługi zdarzeń asynchronicznych, 73
pośredniczące, 126
prywatne, 99
K
rozwijanie, 84, 86, 87, 89 klasy, 17, 126
samodefiniujące się, 75, 76, 90 emulacja, 126, 127
samowywołujące się, 79 kod
terminologia, 66 konwencje, 34, 35, 36, 37, 38
właściwości, 82 łatwy w konserwacji, 21, 22
wywołania zwrotnego, 70 minifikowanie, 46
wywołanie, 85 ocenianie przez innych, 45, 46
zwracanie, 74, 89 usuwanie warunkowych wersji, 80, 81, 90
wielokrotne użycie, 115
G kodowania, wzorce, 16
komentarze, 40, 41
globalne zmienne, 22, 23, 24 kompresja, 185
dorozumiane, 23, 24 konsola, 20
gospodarza, obiekty, 17, 18 konstruktory, 54, 119
czyszczenie referencji, 125
pośredniczące, 126
H pożyczanie, 119, 121, 122
hasOwnProperty(), 29, 30 samowywołujące, 55
hoisting, Patrz przenoszenie deklaracji tymczasowe, 124, 126
HTML, wysyłanie pliku fragmentami, 188, 189 wartość zwracana, 53
HTMLCollection, 27, 28 konwencje kodu, 34, 35
białe spacje, 37, 38
nawias otwierający, 36, 37
I nawiasy klamrowe, 35, 36
inicjalizacja, 25 nazewnictwo, 38, 39, 40, 54
leniwa, 153 średniki, 37
init(), 79, 80 wcięcia, 35
instanceof, operator, 108 konwersja liczb, 34
instancja, 115 kopia
isArray(), 57 głęboka, 131
iteratora, wzorzec, 143, 144, 169 płytka, 131

196 | Skorowidz
L konfiguracyjne, 83, 84, 89
natychmiastowa inicjalizacja, 79, 90
leniwa inicjalizacja, 153 rdzenne, 17
leniwe wczytywanie, 190, 191 tworzenie, 51, 91
liczby, konwersja, 34 Object(), 18, 51, 143
literały Object.create(), 130
funkcji, 67 Object.prototype.toString(), 58
obiektów, 49, 50, 51, 98 obserwator, 163
tablicy, 56 obserwatora, wzorzec, 163, 166, 169
wyrażenia regularnego, 59, 60 obsługa zdarzeń, 175, 176
log(), 20 asynchronicznych, 73
lokalne zmienne, 22 onclick, atrybut, 175
open(), 180
Ł
P
łańcuchy wywołań, 112
parseInt(), 34
parseJSON(), 59
M pętle
Martin, Robert, 112 for, 27, 28
mediator, 160 for-in, 29
mediatora, wzorzec, 160, 169 piaskownicy, wzorzec, 103, 104, 105, 114
przykład, 160, 161, 162 dodawanie modułów, 105
uczestnicy, 160 globalny konstruktor, 104
method(), 113 implementacja konstruktora, 106
metody, 17, 49 pośrednika, wzorzec, 153, 155, 158, 159, 169
pożyczanie, 133, 134 preventDefault(), 152
prywatne, 95, 96 projektowe, wzorce, 16
publiczne, 99 prototype, właściwość, 18, 98
statyczne, 107, 108 modyfikacja, 31
uprzywilejowane, 96 prototypy, 18
minifikacja, 46, 185 łańcuch, 117, 118, 120, 121
moduły, 100, 101, 102 modyfikacja, 31
import zmiennych globalnych, 103 prywatność, 98
tworzące konstruktory, 102 współdzielenie, 123, 124
prywatność, problemy, 96
przeglądarki, wykrywanie, 194
N przenoszenie deklaracji, 26, 27
przestrzenie nazw, 22, 91, 92, 114
najmniejszego przywileju, zasada, 97
Firebug, 94
name, właściwość, 68
natychmiastowa inicjalizacja obiektu, 79, 90
nazewnictwo, konwencje, 38, 39, 40, 54 R
nazwane wyrażenie funkcyjne, 66, 67
new, słowo kluczowe, 54, 138 ramki, 184
nienazwane wyrażenie funkcyjne, 66, 68 rdzenne obiekty, 17
notacja literału obiektu, 49, 50, 51 RegExp(), 59, 60
rzutowanie niejawne, 32

O
S
obiekty, 17, 51
błędów, 62 Schönfinkel, Moses, 87
globalne, 22, 25 schönfinkelizacja, 87
gospodarza, 17, 18 send(), 180

Skorowidz | 197
serializacja, 82, 83 wielbłądzi, styl, 39
serwer, komunikacja, 179 window, właściwość, 22, 25
setInterval(), 33, 73 with, polecenie, 19
setTimeout(), 33, 73, 178 właściwości, 17, 49
singleton, 137, 138, 169 prywatne, 95, 96
składowe statyczne, 107, 110
prywatne, 96 wydajność, 184
statyczne, 107, 109 wyliczenie, 29
skrypty wyrażenia regularne, 59
łączenie, 184, 185 wyrażenie funkcyjne, 66, 67
obliczeniowe, 179 nazwane, 66, 67
strategie wczytywania, 186 nienazwane, 66
stałe, 110, 111 wywołanie funkcji, 85
stopPropagation(), 152 wywołanie jako obraz, 184
strategii, wzorzec, 149, 169 wywołanie zwrotne, 70, 71, 89
strict mode, Patrz tryb ścisły w bibliotekach, 74
String.prototype.replace(), 60 zakres zmiennych, 72
styl wielbłądzi, 39 wzorce, 11, 15
subskrybenta-dostawcy, wzorzec, 163, 169 antywzorce, 16
supermetody, 152 API, 89
switch, 31, 32 inicjalizacyjne, 89
SyntaxError(), 62 kodowania, 16
optymalizacyjne, 90
projektowe, 16
Ś
środowisko uruchomieniowe, 18 X
XHR, Patrz XMLHttpRequest
T XMLHttpRequest, 180, 181
that, 54, 55
this, 22, 53 Y
throw, instrukcja, 62
tryb ścisły, 19 Y.clone(), 132
TypeError(), 62 Y.delegate(), 178
typeof, 32, 57 Yahoo! Query Language, Patrz YQL
typy proste, otoczki, 61, 62 YQL, 157
YUI3, 132, 178
YUIDoc, 41, 42, 44
V przykład dokumentacji, 42, 44
var, 23
efekty uboczne pominięcia, 24 Z
problem rozrzuconych deklaracji, 26
wzorzec pojedynczego użycia, 25 zdarzenia, 175
asynchroniczne, 73
delegacje, 177
W obsługa, 175, 176
walidacja danych, 150 własne, 163
wątki, symulacja, 178 zmienne, 17
wczytywanie globalne, 22, 23, 24, 103
leniwe, 190, 191 lokalne, 22
na żądanie, 191
wstępne, 192, 193, 194

198 | Skorowidz

You might also like