Professional Documents
Culture Documents
Stoyan S. - Javascript. Wzorce
Stoyan S. - Javascript. Wzorce
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
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
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
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
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
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.
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ń.
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,
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).
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".
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.
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.
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.
��
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ą-
•
�
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.
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-
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. +
�
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.
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.
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
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.
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.
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.
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;
}
// …
}
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
}
// 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"
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.
// 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.
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.
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.
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 = [];
// …
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
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 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()
*/
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]);
}
}
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…
};
}
switch (inspect_me) {
case 0:
result = "zero";
break;
case 1:
result = "jeden";
break;
default:
result = "nieznany";
}
// 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"
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"
}());
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
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.
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);
}
// lepiej
if (true) {
alert(1);
} else {
alert(2);
}
lub
if (true)
{
alert("To prawda!");
}
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"
};
}
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() {}; .
// 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.
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.
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() {...}
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).
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 () {
// ...
}
};
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.
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;
};
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.
/**
* 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.
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.
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 = {};
49
// dodanie metody
dog.getName = function () {
return dog.name;
};
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.
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).
// 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"
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"
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) {
// return this;
};
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);
// 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.
// 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"
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;
}
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).
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();
}
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");
// 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
Aby uniknąć potencjalnych błędów przy tworzeniu dynamicznie generowanych tablic, znacz-
nie bezpieczniej jest stosować notację literałową.
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
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.
// 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ć:
// jsonstr ma wartość:
// {"name":"Fido","dob":"2010-04-11T22:36:22.436Z","legs":[1,2,3,4]}
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ą.
• m — dopasowanie wielowierszowe;
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
Ostatnia uwaga: wywołanie RegExp() bez new (czyli jako funkcji, a nie konstruktora) działa
identycznie jak wywołanie z new.
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"
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);
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";
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()
}
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.
Podsumowanie | 63
64 | Rozdział 3. Literały i konstruktory
ROZDZIAŁ 4.
Funkcje
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
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;
};
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ąć.
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.
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;
}
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() {};
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() {
// 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();
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ć.
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;
while (i) {
i 3= 1;
// złożona logika...
// wywołanie zwrotne:
if (callback) {
callback(found);
}
nodes.push(found);
}
return nodes;
};
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";
});
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;
};
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 === "function") {
callback.call(callback_obj, found);
}
// ...
};
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);
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);
};
};
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!");
};
};
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ń.
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 () {
alert(msg);
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.
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) {
}(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.
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;
};
}());
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.
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.
// inicjalizacja
init: function () {
console.log(this.gimmeMax());
// dalsze polecenia inicjalizacji...
}
}).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();
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);
};
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.
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];
};
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];
};
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);
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 : "") + "!";
}
};
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;
}
// aplikacja pełna
add.apply(null, [5, 4]); // 9
// aplikacja częściowa
var newadd = add.partialApply(null, [5]);
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).
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
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;
}
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
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.
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.
// 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.
// równoważne kodowi:
// var MYAPP = {
// modules: {
// module2: {}
// }
// };
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;
/*
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)
*/
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';
};
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();
Ł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.
// 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.
myobj.getName(); // "ojej"
Ten sam pomysł, ale w nieco innym wykonaniu przedstawia poniższy kod.
var myobj = (function () {
// składowe prywatne
var name = "ojej";
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.
Gadget.prototype = (function () {
// zmienna prywatna
var browser = "Mobile WebKit";
// prototyp składowych publicznych
return {
getBrowser: function () {
return browser;
}
};
}());
(function () {
function isArray(a) {
return toString.call(a) === astr;
}
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');
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
// interfejs publiczny
return {
inArray: function (needle, haystack) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return true;
}
}
},
Wzorzec modułu jest powszechnie stosowaną i bardzo zalecaną metodą organizacji kodu,
szczególnie gdy jest go naprawdę sporo.
// 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
MYAPP.utilities.Array = (function () {
// zależności
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.lang,
// koniec var
}());
}(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.
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.
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) {
// ...
});
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 = {};
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;
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.
// metoda statyczna
Gadget.isShiny = function () {
return "Oczywiście";
};
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 () {
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;
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ą.
// metoda uprzywilejowana
NewGadget.prototype.getLastId = function () {
return counter;
};
// nadpisanie konstruktora
return NewGadget;
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;
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
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);
}
};
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.
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().
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”.
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.
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.
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.
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"
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.
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();
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';
}
// konstruktor potomka
function Child(name) {
Parent.apply(this, arguments);
}
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);
}
Wynik tej operacji przedstawia rysunek 6.5. W przypadku duplikatów wygra ten, który będzie
przypisywany jako ostatni.
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.
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).
// testy
var kid = new Child();
kid.constructor.name; // "Parent"
kid.constructor === Parent; // true
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.
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.
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
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;
};
// 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();
}
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.
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);
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
for (i in parent) {
if (parent.hasOwnProperty(i)) {
if (typeof parent[i] === "object") {
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}
};
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;
}
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().
// 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"
};
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.
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.
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)));
};
};
}
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.
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.
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.
// standardowe działania
this.start_time = 0;
this.bang = "Wielki";
// zapamiętanie instancji
Universe.instance = 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;
// wygląda prawidłowo:
uni.constructor.name; // "Universe"
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;
};
// instancja
instance = new Universe();
return instance;
}
(function () {
var instance;
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');
// metoda przodka
CarMaker.prototype.drive = function () {
return "Brum, mam " + this.doors + " drzwi";
};
// 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());
}
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.
// [jak wyżej...]
return {
// [jak wyżej...]
rewind: function () {
index = 0;
},
current: function () {
return data[index];
}
};
}());
// powrót na początek
agg.rewind();
console.log(agg.current()); // 1
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"
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().
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');
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;
};
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 = [];
}
Sale.decorators.fedtax = {
getPrice: function (price) {
return price + price * 5 / 100;
}
};
Sale.decorators.quebec = {
getPrice: function (price) {
return price + price * 7.5 / 100;
}
};
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'
};
// komunikaty o błędach
// z aktualnej sesji walidacyjnej
messages: [],
// metoda interfejsu
// data to pary klucz-wartość
validate: function (data) {
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;
}
}
// ...
};
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.
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.
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;
id = src.href.split('--')[1];
Pośrednik | 155
src.parentNode.innerHTML = videos.getPlayer(id);
return;
}
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.
if (!info) {
http.makeRequest([id], "videos.updateList");
return;
}
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');
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")
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;
http.makeRequest(this.ids, "proxy.handler");
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).
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.
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.
// aktualizacja wyświetlacza
update: function (score) {
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');
},
scoreboard.update(score);
},
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;
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: []};
}
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();
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);
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.
Player.prototype.play = function () {
this.points += 1;
this.fire('play', this);
};
keys: {},
for (i in players) {
if (players.hasOwnProperty(i)) {
score[players[i].name] = players[i].points;
}
Obserwator | 167
}
this.fire('scorechange', score);
}
};
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.
Podsumowanie | 169
170 | Rozdział 7. Wzorce projektowe
ROZDZIAŁ 8.
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);
}
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ć.
// lepiej
var style = document.getElementById("result").style,
padding = style.padding,
margin = style.margin;
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)).
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);
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.
// 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;
}
// wyłączenie bąbelkowania
if (typeof e.stopPropagation === "function") {
e.stopPropagation();
}
if (typeof e.cancelBubble !== "undefined") {
e.cancelBubble = true;
}
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;
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");
e.halt();
}
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.
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');
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.
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'
];
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) {
return false;
}
if (xhr.status !== 200) {
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.
// 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;
},
// wykonanie żądania
remoteRequest: function () {
var script = document.createElement("script");
script.src = "server.php?callback=ttt.serverPlay&played=" +
´ttt.played.join(',');
document.body.appendChild(script);
},
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 !== " ") {
data = Math.ceil(Math.random() * 9);
}
this.get('cell-' + data).innerHTML = 'O';
this.played.push(data);
}
};
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.
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.
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.
Łą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.
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
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.
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.
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.
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 ...
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 () {
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]();
}
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.
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) {
// IE
newjs.onreadystatechange = function () {
if (newjs.readyState === 'loaded' || newjs.readyState === 'complete') {
newjs.onreadystatechange = null;
callback();
// inne
newjs.onload = function () {
callback();
};
newjs.src = file;
script.parentNode.insertBefore(newjs, script);
}
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);
};
}
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.
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