Spis treści O autorze
13
O recenzentach
15
Przedmowa
19
Co znajdziesz w tej książce? Konwencje
Rozdział 1. Wprowadzenie Trochę historii Zapowiedź zmian Teraźniejszość Przyszłość Programowanie obiektowe Obiekty Klasy Kapsułkowanie Agregacja Dziedziczenie Polimorfizm Programowanie obiektowe — podsumowanie Konfiguracja środowiska rozwijania aplikacji Niezbędne narzędzia Korzystanie z konsoli Firebug Podsumowanie
Rozdział 2. Proste typy danych, tablice, pętle i warunki Zmienne Wielkość liter ma znaczenie Operatory
19 20
23 24 25 26 26 27 27 28 28 29 29 30 30 31 31 32 33
35 35 36 37
Spis treści
Proste typy danych Ustalanie typu danych — operator typeof Liczby Liczby ósemkowe i szesnastkowe Wykładniki potęg Nieskończoność NaN
Łańcuchy znaków Konwersje łańcuchów Znaki specjalne
Typ boolean Operatory logiczne Priorytety operatorów Leniwe wartościowanie Porównywanie
Undefined i null Proste typy danych — podsumowanie Tablice Dodawanie i aktualizacja elementów tablicy Usuwanie elementów Tablice tablic Warunki i pętle Bloki kodu Warunki if Sprawdzanie, czy zmienna istnieje Alternatywna składnia if Switch
Pętle Pętla while Pętla do…while Pętla for Pętla for…in
40 41 41 41 42 43 45
45 46 47
48 49 51 52 53
54 56 56 57 58 58 60 60 61 62 63 63
65 66 66 66 69
Komentarze Podsumowanie Ćwiczenia
70 71 71
Rozdział 3. Funkcje
73
Czym jest funkcja? Wywoływanie funkcji Parametry Funkcje predefiniowane parseInt() parseFloat() isNaN() isFinite() Encode/Decode URIs eval() Bonus — funkcja alert()
6
74 74 74 76 76 78 79 79 80 80 81
Spis treści
Zasięg zmiennych Funkcje są danymi Funkcje anonimowe Wywołania zwrotne Przykłady wywołań zwrotnych Funkcje samowywołujące się Funkcje wewnętrzne (prywatne) Funkcje, które zwracają funkcje Funkcjo, przepiszże się! Domknięcia Łańcuch zakresów Zasięg leksykalny Przerwanie łańcucha za pomocą domknięcia
81 83 84 84 85 87 87 88 89 90 91 91 93
Domknięcie 1. Domknięcie 2. Domknięcie 3. i jedna definicja Domknięcia w pętli
Funkcje dostępowe Iterator Podsumowanie Ćwiczenia
Rozdział 4. Obiekty Od tablic do obiektów Elementy, pola, metody Tablice asocjacyjne Dostęp do własności obiektu Wywoływanie metod obiektu Modyfikacja pól i metod Wartość this Konstruktory Obiekt globalny Pole constructor Operator instanceof Funkcje zwracające obiekty Przekazywanie obiektów Porównywanie obiektów Obiekty w konsoli Firebug Obiekty wbudowane Object Array
94 95 96 96
98 99 100 100
103 103 105 105 106 107 108 109 109 110 112 112 113 114 114 115 117 117 118
Ciekawe metody obiektu Array
Function
120
122
Własności obiektu Function Metody obiektu Function Nowe spojrzenie na obiekt arguments
Boolean Number
123 125 126
127 128
7
Spis treści
String Ciekawe metody obiektu String
Math Date Metody działające na obiektach Date
RegExp Pola obiektów RegExp Metody obiektów RegExp Metody obiektu String, których parametrami mogą być wyrażenia regularne search() i match() replace() Wywołania zwrotne replace split() Przekazanie zwykłego tekstu zamiast wyrażenia regularnego
Obsługa błędów za pomocą obiektów Error Podsumowanie Ćwiczenia
Rozdział 5. Prototypy Pole prototype Dodawanie pól i metod przy użyciu prototypu Korzystanie z pól i metod obiektu prototype Własne pola obiektu a pola prototypu Nadpisywanie pól prototypu własnymi polami obiektu Pobieranie listy pól
isPrototypeOf() Ukryte powiązanie __proto__ Rozszerzanie obiektów wbudowanych Rozszerzanie obiektów wbudowanych — kontrowersje Pułapki związane z prototypami Podsumowanie Ćwiczenia
Rozdział 6. Dziedziczenie Łańcuchy prototypów Przykładowy łańcuch prototypów Przenoszenie wspólnych pól do prototypu Dziedziczenie samego prototypu Konstruktor tymczasowy — new F() Uber: dostęp do obiektu-rodzica Zamknięcie dziedziczenia wewnątrz funkcji Kopiowanie pól Uwaga na kopiowanie przez referencję! Obiekty dziedziczą z obiektów Głębokie kopiowanie object() Połączenie dziedziczenia prototypowego z kopiowaniem pól
8
130 132
135 136 138
140 141 142 143 143 144 145 146 146
146 150 151
155 155 156 157 158 159 160
162 163 165 166 167 169 170
171 172 172 175 177 178 180 181 182 184 186 187 189 190
Spis treści
Dziedziczenie wielokrotne Miksiny Dziedziczenie pasożytnicze Wypożyczanie konstruktora Pożycz konstruktor i skopiuj jego prototyp Podsumowanie Studium przypadku: rysujemy kształty Analiza Implementacja Testowanie Ćwiczenia
Rozdział 7. Środowisko przeglądarki
191 193 193 194 196 197 200 200 201 204 205
207
Łączenie JavaScriptu z kodem HTML BOM i DOM — przegląd BOM Ponownie odkrywamy obiekt window window.navigator Firebug jako ściąga window.location window.history window.frames window.screen window.open() i window.close() window.moveTo(), window.resizeTo() window.alert(), window.prompt(), window.confirm() window.setTimeout(), window.setInterval() window.document DOM Core DOM i HTML DOM Dostęp do węzłów DOM
207 208 209 209 210 210 211 212 213 214 215 216 216 217 219 219 221 222
Węzeł document documentElement Węzły-dzieci Atrybuty Dostęp do zawartości znacznika Uproszczone metody dostępowe DOM Rówieśnicy, body, pierwsze i ostatnie dziecko Spacer przez węzły DOM
223 224 224 225 226 227 228 230
Modyfikacja węzłów DOM
230
Modyfikacja stylu Zabawa formularzami
231 232
Tworzenie nowych węzłów
233
Metoda w pełni zgodna z DOM cloneNode() insertBefore()
Usuwanie węzłów
234 235 236
236
9
Spis treści
Obiekty DOM istniejące tylko w HTML Starsze sposoby dostępu do dokumentu document.write() Pola cookies, title, referrer i domain
Zdarzenia Kod obsługi zdarzeń wpleciony w atrybuty HTML Pola elementów Obserwatorzy zdarzeń DOM Przechwytywanie i bąbelkowanie Zatrzymanie propagacji Anulowanie zachowania domyślnego Obsługa zdarzeń w różnych przeglądarkach Typy zdarzeń XMLHttpRequest Wysłanie żądania Przetworzenie odpowiedzi Tworzenie obiektów XHR w IE w wersjach starszych niż 7 A jak asynchroniczny X jak XML Przykład Podsumowanie Ćwiczenia
Rozdział 8. Wzorce kodowania i wzorce projektowe Wzorce kodowania Izolowanie zachowania Warstwa treści Warstwa prezentacji Zachowanie Przykład wydzielenia warstwy zachowania
Przestrzenie nazw
238 239 240 240
242 242 242 243 244 246 248 248 249 250 251 252 253 254 254 254 257 258
261 262 262 262 263 263 263
264
Obiekt w roli przestrzeni nazw Konstruktory w przestrzeniach nazw Metoda namespace()
264 265 266
Rozgałęzianie kodu w czasie inicjalizacji Leniwe definicje Obiekt konfiguracyjny Prywatne pola i metody Metody uprzywilejowane Funkcje prywatne w roli metod publicznych Funkcje samowywołujące się Łańcuchowanie JSON Wzorce projektowe Singleton Singleton 2
267 268 269 270 271 272 273 273 274 275 276 276
Zmienna globalna Pole konstruktora Pole prywatne
10
277 277 278
Spis treści
Fabryka Dekorator Dekorowanie choinki
Obserwator Podsumowanie
Dodatek A Słowa zarezerwowane Lista słów zarezerwowanych mających specjalne znaczenie w języku JavaScript Lista słów zarezerwowanych na użytek przyszłych implementacji
278 280 280
282 285
287 287 288
Dodatek B Funkcje wbudowane
291
Dodatek C Obiekty wbudowane
295
Object Składowe konstruktora Object Składowe obiektów tworzonych przez konstruktor Object Array Składowe obiektów Array Function Składowe obiektów Function Boolean Number Składowe konstruktora Number Składowe obiektów Number String Składowe konstruktora String Składowe obiektów String Date Składowe konstruktora Date Składowe obiektów Date Math Składowe obiektu Math RegExp Składowe obiektów RegExp Obiekty Error Składowe obiektów Error
295 296 296 298 298 301 301 302 302 303 304 304 305 305 308 308 309 311 312 313 314 315 315
Dodatek D Wyrażenia regularne
317
Skorowidz
323
11
Spis treści
12
O recenzentach Dan Wellman mieszka wraz z żoną i trójką dzieci w domu w Southampton na południowym wybrzeżu Anglii. W ciągu dnia jego łagodne alter ego pracuje w małej, ale uznanej firmie zajmującej się handlem elektronicznym. W nocy przekształca się w wojownika, którego celem jest prawda, sprawiedliwość i mniej natrętny JavaScript. Od pięciu lat regularnie pisze artykuły, kursy i recenzje związane z informatyką i prawie nigdy nie odkleja się od klawiatury. Douglas Crockford jest dziełem amerykańskiego systemu edukacji publicznej. Zawsze głosuje i posiada własny samochód. Jest największym żyjącym autorytetem w sprawach JavaScriptu. Jest autorem książki JavaScript — mocne strony (wydawnictwo Helion, Gliwice 2009). Rozwijał systemy automatyzacji pracy biurowej. Badał gry i muzykę w firmie Atari. Był kierownikiem ds. technologii w Lucasfilm, a także kierownikiem ds. nowych mediów w Paramount. Był założycielem i dyrektorem generalnym firmy Electric Communities/Communities.com. Założył firmę State Software, a pracując w niej, odkrył standard wymiany danych JSON. Teraz jest architektem w Yahoo!. Gamaiel Zavala to specjalista ds. interfejsów użytkownika w firmie Yahoo! w Santa Monica w Kalifornii. Lubi pisać kod różnego rodzaju i zawsze stara się zrozumieć aplikacje na wszystkich poziomach, od pakietów i protokołów, przez technologie obsługujące żądania, aż do interfejsu użytkownika. Oprócz komputerów pochłania go jeszcze życie rodzinne z ukochaną żoną i małym synkiem. Jayme Cousins zaczął tworzyć komercyjne strony internetowe zaraz po ukończeniu geografii na uniwersytecie. Wśród jego projektów można wymienić promocję niszowego oprogramowania do analizy przestrzennej, nocne przygotowywanie internetowej wersji głównej gazety w jego mieście, drukowanie nazw ulic na mapach, malowanie domów, a także wykładanie technologii na uniwersyteckich kursach dla dorosłych. Obecnie mieszka przy klawiaturze w miejscowości
JavaScript. Programowanie obiektowe
London w Kanadzie wraz z żoną Heather i synkiem Alanem. Jayme wcześniej recenzował książkę Learning Mambo wydaną przez Packt. Lubi znajdować zastosowania technologii służące zwykłym ludziom i często ma wrażenie, że jego najważniejszym zadaniem jest tłumaczenie z technomowy na język ludzki. Obecnie Jayme prowadzi firmę In House Logic (www.inhouselogic.com), która oferuje tworzenie stron internetowych, doradztwo oraz szkolenie techniczne. Julie London jest inżynierem oprogramowania z ponad ośmioletnim doświadczeniem w budowaniu firmowych aplikacji sieciowych. Przez wiele lat programowała we Flashu, teraz jednak koncentruje się na innych technologiach klienckich, takich jak CSS, JavaScript i XSL. Mieszka w Los Angeles i pracuje jako inżynier ds. interfejsów użytkownika w Yahoo!. Nicholas C. Zakas to główny inżynier ds. interfejsów użytkownika w Yahoo! (w tym przede wszystkim strony głównej), współtwórca biblioteki YUI oraz nauczyciel JavaScriptu w Yahoo!. Jest autorem dwóch książek: JavaScript dla webmasterów. Zaawansowane programowanie oraz Ajax. Zaawansowane programowanie (obie wydane przez Helion), a także wielu artykułów na temat JavaScriptu dostępnych w sieci. Nicholas początkowo pracował jako webmaster w małej firmie programistycznej, później zajął się interfejsami użytkownika, by wreszcie całkowicie poświęcić się inżynierii oprogramowania. Przeniósł się do Doliny Krzemowej z Massachusetts w roku 2006 i dołączył do Yahoo!. Z Nicholasem można skontaktować się poprzez jego stronę, www.nczonline.net. Nicole Sullivan to guru ds. wydajności CSS. Mieszka w Kalifornii. Jej kariera zawodowa rozpoczęła się w roku 2000, gdy jej przyszły mąż (wówczas pracownik W3C) poinformował ją, że nie będzie mógł spać w nocy, jeśli jej strona nie zacznie pomyślnie przechodzić walidacji. Postanowiła dowiedzieć się, o jaki to „walidator” chodzi, i zapałała miłością do standardów. Zaczęła tworzyć strony spełniające zasady sformułowane w 508 ustępie amerykańskiej Ustawy o rehabilitacji1. Ponieważ zaczęła interesować się sprawami wydajności i skalowalności aplikacji sieciowych, rozpoczęła pracę w firmie zajmującej się handlem i promocją w Internecie, gdzie tworzyła oparte o CSS rozwiązania dla wielu europejskich i światowych marek, takich jak SFR, Club Med, SNCF, La Poste, FNAC, Accor Hotels i Renault. Obecnie Nicole pracuje dla Yahoo! w grupie Exceptional Performance („wyjątkowa wydajność”). Jej rola polega na poszukiwaniu oraz nauczaniu najlepszych praktyk wydajnościowych oraz rozwijaniu narzędzi takich jak YSlow, które pomagają innym inżynierom interfejsów tworzyć lepsze strony. Pod adresem www.stubbornella.org pisze o standardach, swoim psie oraz swojej obsesji na punkcie obiektowego CSS. Philip Tellis to dość leniwy maniak komputerowy pracujący dla Yahoo!. Lubi, gdy całą pracę odwala za niego komputer, a jeśli komputer nie potrafi — Philip po prostu go przeprogramowuje. 1
Mowa tam o dostępności technologii dla osób niepełnosprawnych — przyp. tłum.
16
O recenzentach
Jeśli akurat nie jest zajęty kodem, Philip jeździ na rowerze w okolicach Doliny Krzemowej lub próbuje zajmować się jedzeniem — oczywiście nigdy nie robi obu tych rzeczy naraz. Ross Harmes jest specjalistą ds. interfejsów użytkownika w Flickr w San Francisco. Jest również autorem książki Pro JavaScript Design Patterns („Profesjonalne wzorce projektowe dla JavaScriptu”). Niektóre z jego artykułów i projektów, na przykład pakiet YUI dla edytora TextMate, można znaleźć pod adresem www.techfoolery.com. Tenni Theurer dołączyła do Yahoo!, a konkretnie do grupy Exceptional Performance, na początku roku 2006. Następnie objęła rządy jako kierownik i przygotowała zespół projektowy do jego przewodniej roli w przyspieszaniu produktów Yahoo! dla wygody użytkowników na całym świecie. W tej chwili zarządza wynikami pracy zajmującej się wyszukiwaniem grupy Search Distribution. Tenni występowała na wielu konferencjach, w tym na Web 2.0 Expo, Ajax Experience, Rich Web Experience, AJAXWorld, BlogHer, WITI i CSDN-DrDobbs. Gościnnie udziela się na blogach Yahoo! Developer Network oraz Yahoo! User Interface Blog. Wcześniej pracowała w grupie Pervasive Computing („przetwarzanie bez granic”) w IBM, gdzie zajmowała się technologiami mobilnymi i bezpośrednio współpracowała z dużymi klientami podczas wdrożeń na szeroką skalę. Wayne Shea to inżynier oprogramowania w Yahoo!. Wśród jego projektów można wymienić badania nad poprawieniem wydajności sieci w urządzeniach mobilnych oraz tworzenie skalowalnych usług sieciowych wysokiej wydajności. Przed rozpoczęciem pracy w Yahoo! zajmował się tworzeniem przeglądarek dla telefonów komórkowych w firmach Openwave i ACCESS. Yavor Paunov jest wynikiem zdwojonego wysiłku wydziałów informatyki Politechniki Sofijskiej oraz Uniwersytetu Concordia w Montrealu. Pracował i w dwuosobowych spółkach, i w wielkich międzynarodowych korporacjach. Oprócz pracy interesuje go jeszcze muzyka na żywo oraz długie spacery z uroczym, pożerającym buty cocker-spanielem.
17
JavaScript. Programowanie obiektowe
18
Przedmowa Ta książka przedstawia JavaScript w jego prawdziwej postaci ekspresywnego, elastycznego prototypowego obiektowego języka programowania. Po okresie odrzucenia, gdy JavaScript traktowany był li tylko jako zabawka pozwalająca tworzyć bajeranckie przyciski zmieniające kolor po najechaniu na nie kursorem, ten ciekawy język zasłużenie wraca do łask i jest silniejszy niż kiedykolwiek przedtem. Dzisiejszy świat Web 2.0, pełny technologii AJAX, dużych aplikacji klienckich, aplikacji internetowych przypominających programy desktopowe, interaktywnych map i klientów pocztowych, istnieje w dużej mierze dzięki JavaScriptowi. Jeśli jeszcze nie znasz tego języka, teraz jest dobry moment na to, by wreszcie się go nauczyć. Do zrozumienia tej książki nie jest Ci potrzebna żadna wcześniejsza wiedza programistyczna — zaczniemy od zera i bez nerwów przejdziemy do bardziej zaawansowanych zagadnień.
Co znajdziesz w tej książce? Rozdział 1. przedstawia historię, teraźniejszość oraz przyszłość JavaScriptu, a także najważniejsze pojęcia związane z programowaniem obiektowym. Pokazuję także w nim, jak skonfigurować środowisko Firebug, które umożliwi Ci samodzielne eksperymentowanie z kodem w oparciu o przykłady z książki. Rozdział 2. omawia podstawy języka: zmienne, typy danych, tablice, pętle i instrukcje warunkowe. Rozdział 3. poświęcony jest funkcjom. Istnieje wiele różnych zastosowań funkcji w języku JavaScript i wszystkie z nich są przedstawione na kartach tego rozdziału. Mówię tu także o zakresie zmiennych oraz o funkcjach wbudowanych. Na koniec oswajam domknięcia, czyli ciekawą, ale często źle rozumianą funkcjonalność.
JavaScript. Programowanie obiektowe
Rozdział 4. wprowadza obiekty. Uczy, jak postępować z polami i metodami oraz jak na różne sposoby tworzyć obiekty. Przedstawia także obiekty wbudowane, takie jak Math i Date (dość ogólnie — szczegółów należy szukać w dodatku C). Rozdział 5. poświęcony jest prototypom. Rozdział 6. ma poszerzyć Twoje javascriptowe horyzonty — przedstawiam w nim wiele różnych sposobów na realizację dziedziczenia. Rozdział 7. jest poświęcony przeglądarce. Omawiam w nim BOM (Browser Object Model, obiektowy model przeglądarki), DOM (wprowadzony przez organizację W3C Document Object Model, czyli obiektowy model dokumentu), zdarzenia przeglądarki oraz AJAX. Rozdział 8. to omówienie wzorców projektowych, zarówno tych charakterystycznych dla JavaScriptu, jak i tych niezależnych od języka, przeniesionych do JavaScriptu z Księgi Czterech1, najważniejszej pracy poświęconej programistycznym wzorcom projektowym. Dodatek A zawiera listę słów zarezerwowanych języka. Dodatek B to wzbogacony przykładami przewodnik po funkcjach wbudowanych. Dodatek C jest bardzo szczegółową ściągą z wszystkich metod i pól wszystkich obiektów wbudowanych. Dodatek D opisuje wyrażenia regularne.
Konwencje Fragmenty tekstu przedstawiające informacje różnego typu zostały sformatowane w odmienny sposób. Poniżej znajdują się przykłady różnych stylów wraz z ich interpretacją. Kod programu, jeśli pojawia się wewnątrz tekstu, jest formatowany następująco: „Klucz oddziela się od wartości za pomocą dwukropka, jak w przykładzie klucz:wartość”. Bloki kodu formatowane są tak: var ksiazka = { tytul: 'Paragraf 22', wydana: 1961, autor: { 1
Chodzi o książkę Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, autorstwa Ericha Gammy, Richarda Helma, Ralpha Johnsona i Johna Vlissidesa, wydaną przez Wydawnictwa Naukowo-Techniczne — przyp. tłum.
20
Przedmowa
imie: 'Joseph', nazwisko: 'Heller' } };
Jeśli pewien fragment kodu będzie miał większe znaczenie niż pozostałe, zostanie wytłuszczony: function TwoDShape(){} // obsługa dziedziczenia TwoDShape.prototype = Shape.prototype; TwoDShape.prototype.constructor = TwoDShape;
Nowe pojęcia i ważne słowa także zostaną wytłuszczone. Słowa widoczne na ekranie, będące częścią menu lub wyświetlane w okienkach dialogowych, będą zapisane następująco: „kliknięcie przycisku Dalej spowoduje przejście do następnego slajdu”. Dobre rady Dobre rady będą wyglądały tak.
A tak będą wyglądały wskazówki, ostrzeżenia i uwagi.
21
JavaScript. Programowanie obiektowe
22
1 Wprowadzenie Co łączy Yahoo! Maps, Google Maps, Yahoo! Mail, My Yahoo!, Gmail, Digg, YouTube oraz szereg innych popularnych aplikacji „Web 2.0”? Wszystkie wymienione strony posiadają wielofunkcyjne, interaktywne interfejsy użytkownika, w dużej mierze oparte na kodzie w języku JavaScript. JavaScript początkowo pojawiał się jedynie w postaci jednolinijkowych wstawek w kodzie HTML, jednak obecnie język ten posiada o wiele bardziej zaawansowane zastosowania. Programiści wykorzystują jego obiektowość do tworzenia skalowalnych aplikacji składających się z elementów, których uniwersalność pozwala na ich ponowne wykorzystanie. Najpopularniejszy dzisiaj paradygmat tworzenia stron internetowych wyróżnia trzy warstwy: warstwę struktury (HTML), warstwę prezentacyjną (CSS) oraz warstwę zachowania, za którą odpowiada właśnie JavaScript. Najczęściej spotykanym środowiskiem działania aplikacji napisanych w języku JavaScript są przeglądarki internetowe, jednak możliwości jest więcej. Przy pomocy JavaScriptu można tworzyć różnego rodzaju widżety, dodatki i rozszerzenia. Nauka tego języka naprawdę się opłaca, ponieważ umożliwia on rozwijanie bardzo różnych aplikacji. Ta książka przedstawia język JavaScript ze szczególnym uwzględnieniem jego obiektowej natury. Zaczynamy od zera — do jej zrozumienia nie jest Ci potrzebna żadna wcześniejsza wiedza programistyczna. Jeden z rozdziałów jest poświęcony środowisku przeglądarki, jednak pozostała część książki zawiera informacje, które stosują się do wszystkich środowisk. Przejdźmy zatem do rozdziału 1., który krótko przedstawia historię języka JavaScript. Wprowadzę w nim również podstawowe pojęcia związane z programowaniem obiektowym.
JavaScript. Programowanie obiektowe
Trochę historii Internet powstał jako zbiór statycznych dokumentów HTML, powiązanych za pomocą hiperłączy (linków). Dość wcześnie, gdy tylko zwiększyły się popularność i rozmiar sieci, autorom stron przestały wystarczać dostępne narzędzia. Widoczna stała się potrzeba poprawienia interakcji z użytkownikiem. U jej podstaw leżała chęć ograniczenia liczby połączeń z serwerem w celu wykonania prostych zadań, takich jak walidacja formularzy. Pojawiły się dwie możliwości: aplety Javy (który nie odniosły sukcesu) oraz język LiveScript, zaproponowany przez firmę Netscape w roku 1995. Został on dołączony do przeglądarki Netscape 2.0 pod nazwą JavaScript. Możliwość zmieniania uznawanych wcześniej za statyczne elementów stron internetowych została bardzo ciepło przyjęta, w wyniku czego inne przeglądarki zostały dostosowane do obsługi JavaScriptu. Internet Explorer (IE) firmy Microsoft w wersji 3.0 został wzbogacony o język JScript, który był kopią JavaScriptu rozszerzoną o kilka funkcjonalności przeznaczonych tylko dla IE. W wyniku coraz większych różnic pomiędzy przeglądarkami podjęto próbę ustandaryzowania różnych implementacji języka. Europejskie Stowarzyszenie Producentów Komputerów1 (ECMA) stworzyło specyfikację ECMAScript. Obecnie obowiązuje standard ECMA-262. JavaScript jest jego najpopularniejszą implementacją. Nagły wzrost popularności JavaScriptu miał miejsce w czasie Pierwszej Wojny Przeglądarkowej (1996 – 2001). Był to okres tak zwanej bańki internetowej. O udział w rynku walczyli dwaj główni producenci przeglądarek: Netscape i Microsoft. Obie firmy starały się skusić klienta za pomocą coraz to nowych dodatków i ozdóbek wprowadzanych do przeglądarek oraz do stosowanych w nich wersji JavaScriptu. To właśnie wtedy wiele osób wyrobiło sobie złą opinię na temat języka, który — w wyniku wspomnianych działań oraz braku standaryzacji — bez przerwy się zmieniał. Pisanie programów było koszmarem: skrypt napisany w oparciu o jedną przeglądarkę za nic nie chciał działać w drugiej. Na dodatek producenci, skoncentrowani na dodawaniu nowych funkcjonalności, zapomnieli dostarczyć odpowiednich narzędzi do rozwijania aplikacji. Niespójności pomiędzy przeglądarkami irytowały programistów, jednak była to tylko część problemu. Drugą częścią byli sami autorzy stron, którzy upychali w witrynach zbyt wiele zbędnych funkcjonalności. Chętnie korzystali z wszystkich nowych możliwości dostarczanych przez przeglądarkę, przez co strony były „ulepszane” o kwiatki takie jak animacje na pasku stanu, jaskrawe kolory, migające napisy, trzęsące się okna przeglądarek, płatki śniegu, obiekty podążające za kursorem itp., co często utrudniało korzystanie ze stron. Tego typu nadużycia są drugim powodem złej reputacji języka JavaScript. Między innymi przez nie „prawdziwi” programiści (programiści uznanych języków, takich jak Java czy C/C++) uznali JavaScript za niewiele więcej niż zabawkę przeznaczoną dla projektantów interfejsów.
1
Obecnie Europejskie Stowarzyszenie na rzecz Standaryzacji Systemów Informacyjnych i Komunikacyjnych — przyp. tłum.
24
Rozdział 1. • Wprowadzenie
Wyrazy sprzeciwu wobec JavaScriptu doprowadziły do sytuacji, w której w niektórych projektach sieciowych zabronione zostało jakiekolwiek programowanie po stronie klienta — wszystkie funkcjonalności miał obsługiwać jedynie przewidywalny i wiarygodny serwer. Rzeczywiście, jaki sens miałoby podwajanie czasu wytwarzania aplikacji i spędzanie całości dodatkowego czasu na rozwiązywaniu problemów związanych z różnym działaniem kodu w różnych przeglądarkach?
Zapowiedź zmian Wszystko zmieniło się po zakończeniu Pierwszej Wojny Przeglądarkowej. Zmiany (na lepsze) w sposobie wytwarzania aplikacji sieciowych zostały zapoczątkowane przez kilka procesów: Q Microsoft wygrał wojnę i na okres około pięciu lat (co w czasie internetowym odpowiada
wieczności) firma wstrzymała się od dodawania nowych funkcjonalności do przeglądarki Internet Explorer oraz do JavaScriptu. Dzięki temu programiści innych przeglądarek zyskali czas na dogonienie lub nawet przewyższenie możliwości IE. Q Ruch na rzecz standardów sieciowych zyskał przychylność zarówno programistów, jak i producentów przeglądarek. To oczywiste, że programiści nie chcieli kodować wszystkich funkcjonalności dwa (lub więcej) razy na wypadek, gdyby coś nie działało w którejś z przeglądarek, a właśnie przed tym chronią ich uzgodnione standardy. Co prawda nadal nie istnieje środowisko, które spełniałoby wszystkie możliwe standardy, jednak można mieć nadzieję, że pojawi się ono w przyszłości. Q Technologie i sposoby programowania osiągnęły bardzo dojrzały poziom, na którym można już zajmować się zagadnieniami takimi jak użyteczność, dostępność czy progresywne ulepszanie. Dzięki nowym, zdrowszym metodologiom programiści zaczęli uczyć się lepszych sposobów korzystania z narzędzi, które były dostępne od dość dawna. Po wydaniu aplikacji takich jak Gmail czy Google Maps, które intensywnie wykorzystują programowanie po stronie klienta, jasne stało się, że JavaScript to dojrzały, jedyny w swoim rodzaju i potężny prototypowy język obiektowy. Najlepszym przykładem jego ponownego odkrycia jest szeroka akceptacja funkcjonalności dostarczanych przez obiekt XMLHttpRequest, który kiedyś obsługiwany był jedynie przez IE, jednak został zaimplementowany w wielu przeglądarkach. XMLHttpRequest umożliwia wykonywanie żądań HTTP i pobieranie zawartości z serwera w celu aktualizacji pewnych części strony bez konieczności przeładowywania jej całej. Dzięki XMLHttpRequest narodził się nowy gatunek aplikacji sieciowych, które przypominają samodzielne aplikacje desktopowe. Określa się je mianem aplikacji AJAX.
25
JavaScript. Programowanie obiektowe
Teraźniejszość Ciekawą cechą JavaScriptu jest to, że musi on działać wewnątrz środowiska. Najpopularniejszym środowiskiem jest przeglądarka, jednak istnieją inne możliwości. JavaScript może działać na serwerze, na pulpicie lub wewnątrz tzw. rich media. Obecnie JavaScript pozwala tworzyć: Q Duże aplikacje sieciowe o bogatych interfejsach (aplikacje działające w środowisku sieciowym, takie jak Gmail). Q Kod po stronie serwera. Może to być kod przypominający skrypty ASP lub uruchamiany przy użyciu narzędzi takich jak Rhino (silnik JavaScriptowy napisany w Javie). Q Aplikacje rich media (Flash, Flex). Tworzy się je przy użyciu ActionScriptu, który
jest oparty o ECMAScript. Q Skrypty, które automatyzują zadania administracyjne na pulpicie w Windows.
Wykorzystuje się do tego Windows Scripting Host. Q Rozszerzenia i wtyczki dla wielu samodzielnych aplikacji, takich jak Firefox, Dreamweaver czy Fiddler. Q Aplikacje sieciowe, które przechowują informacje w bazie off-line na komputerze użytkownika. Służy do tego Google Gears. Q Widżety Yahoo! i Mac Dashboard, a także aplikacje Adobe Air, które działają
na maszynie użytkownika. Lista nie wyczerpuje wszystkich możliwości. JavaScript pojawił się wewnątrz stron internetowych, jednak w tej chwili bez większej przesady można powiedzieć, że jest wszechobecny.
Przyszłość Można jedynie zgadywać, co przyniesie przyszłość, jednak jest dość pewne, że znajdzie się w niej miejsce dla JavaScriptu. Przez dość długi czas język ten był niedoceniany i rzadko stosowany (a raczej zbyt często stosowany w niepoprawny sposób), jednak codziennie pojawiają się nowe, coraz ciekawsze i bardziej kreatywne zastosowania. W miejsce jednolinijkowych wpisów, często osadzonych wewnątrz atrybutów znaczników HTML (np. w onclick), pojawiają się skomplikowane, dobrze zaprojektowane i przemyślane rozszerzalne aplikacje i biblioteki. JavaScript zaczął być traktowany poważnie, a programiści ponownie odkrywają jego obiektowe funkcjonalności. Jeszcze niedawno w ogłoszeniach o pracę JavaScript pojawiał się w sekcji „mile widziane”, jednak coraz częściej znajomość tego języka ma decydujące znaczenie podczas podejmowania decyzji o zatrudnieniu programisty aplikacji sieciowych. Podczas rozmów o pracę można usłyszeć „Czy JavaScript jest językiem obiektowym? Dobrze. To jak zaimplementować dziedziczenie?”. Po przeczytaniu tej książki będziesz w stanie śpiewająco zaliczyć rozmowę o pracę jako programista JavaScriptu. Może nawet uda Ci się błysnąć informacją nieznaną oceniającemu. 26
Rozdział 1. • Wprowadzenie
Programowanie obiektowe Zanim na poważnie zajmiemy się JavaScriptem, przypomnijmy sobie, czym właściwie jest programowanie obiektowe. Oto lista pojęć, które często pojawiają się podczas rozmów o programowaniu obiektowym: Q obiekt, metoda, pole; Q klasa; Q kapsułkowanie; Q agregacja; Q dziedziczenie; Q polimorfizm.
Przyjrzyjmy się każdemu z nich z osobna.
Obiekty Jak sama nazwa wskazuje, w programowaniu obiektowym zasadniczą rolę odgrywają obiekty. Obiekt w sposób programistyczny reprezentuje byt (osobę lub rzecz). Może reprezentować dowolny byt: coś ze świata fizycznego lub bardziej abstrakcyjny koncept. Przyglądając się zwykłemu obiektowi (na przykład kotu), widzimy, że posiada on pewne cechy charakterystyczne (kolor, imię, masa ciała) oraz że może wykonać określone czynności (miauczeć, spać, chować się, uciekać). W programowaniu obiektowym cechy obiektu nazywamy jego polami2, a jego czynności — metodami. Istnieje pewna analogia pomiędzy terminologią programowania obiektowego a językiem mówionym: Q Obiekty najczęściej nazywa się przy użyciu rzeczowników (książka, osoba). Q Metody to czasowniki. Q Wartości pól to przymiotniki.
Rozważmy zdanie „Czarny kot śpi na mojej głowie”. „Kot” (rzeczownik) będzie obiektem, „czarny” (przymiotnik) to wartość pola o nazwie kolor, a „śpi” to czynność, czyli metoda obiektu. Na potrzeby objaśniania analogii możemy uznać, że „na mojej głowie” precyzuje sposób spania, zatem przekażemy go jako parametr metodzie śpij.
2
Zamiast „pól” mogą pojawić się „dane”, „właściwości”, „atrybuty” — przyp. tłum.
27
JavaScript. Programowanie obiektowe
Klasy W świecie rzeczywistym obiekty można grupować według pewnych kryteriów. Drozd i orzeł to ptaki, zatem można powiedzieć, że należą do klasy Ptak. W programowaniu obiektowym klasa to pewien szablon lub przepis na obiekt. Inna nazwa obiektu to „instancja” — mówimy, że orzeł jest instancją klasy Ptak. Można stworzyć różne obiekty przy użyciu tej samej klasy, ponieważ klasa jest tylko wzorem. Obiekty to konkretne instancje oparte o ten wzór. Istnieje różnica pomiędzy JavaScriptem a „klasycznymi” językami obiektowymi, takimi jak C++ lub Java. Należy od razu uświadomić sobie, że w języku JavaScript nie ma klas — wszystko jest oparte na obiektach. Istnieją prototypy, które także są obiektami (szczegółowo omówię je trochę później). W klasycznym języku obiektowym wydajemy polecenie „utwórz mi nowy obiekt o nazwie Robert, który jest instancją klasy Osoba”. W prototypowym języku obiektowym mówi się „Wezmę istniejący już obiekt Osoba i użyję go ponownie jako prototypu nowego obiektu, który nazwę Robert”.
Kapsułkowanie Kapsułkowanie to kolejna koncepcja związana z programowaniem obiektowym. Chodzi o to, że obiekt zawiera w sobie dwie rzeczy: Q dane (przechowywane w postaci pól); Q sposoby działania na danych (metody).
Z pojęciem kapsułkowania wiąże się termin hermetyzacji (inaczej ukrywania informacji). Może on oznaczać różne rzeczy, jednak w programowaniu obiektowym ma konkretne znaczenie. Wyobraźmy sobie pewien obiekt, na przykład odtwarzacz MP3. Użytkownik ma do dyspozycji interfejs: przyciski, wyświetlacz itp. Interfejs można wykorzystać w celu sprawienia, by obiekt zrobił coś przydatnego, na przykład odtworzył piosenkę. Użytkownik nie wie, co dokładnie dzieje się wewnątrz odtwarzacza. Co więcej, z reguły zupełnie go to nie obchodzi. Innymi słowy, implementacja interfejsu jest ukryta przed użytkownikiem. Ten sam mechanizm jest używany w programowaniu obiektowym, gdy obiekt jest wykorzystywany poprzez swoje metody. Nie ma znaczenia, czy programista sam napisał kod, czy pochodzi on z zewnętrznej biblioteki. Programista nie musi wiedzieć, jak dokładnie działa metoda. W językach kompilowanych często w ogóle nie można odczytać kodu, który sprawia, że obiekt działa tak, jak powinien. JavaScript jest językiem interpretowanym, co sprawia, że kod jest widoczny, jednak idea jest ta sama — programista pracuje z interfejsem obiektu, bez zawracania sobie głowy implementacją. Kolejnym zagadnieniem związanym z hermetyzacją jest widoczność metod i pól. W niektórych językach metody i pola mogą zostać opisane jako publiczne, prywatne lub chronione (ang. public, private, protected). Ta kategoryzacja określa poziom dostępu użytkownika do poszczególnych części obiektu. Dla przykładu pola i metody prywatne są dostępne jedynie dla
28
Rozdział 1. • Wprowadzenie
wewnętrznej implementacji obiektu, podczas gdy do pól publicznych dostęp może uzyskać każdy. W języku JavaScript wszystkie metody i pola są publiczne, jednak przedstawię sposoby ochrony danych wewnątrz obiektu w celu zapewnienia prywatności.
Agregacja Łączenie kilku obiektów w jeden nazywa się agregacją lub kompozycją. Agregacja pozwala na podział problemu na mniejsze części, którymi łatwiej jest zarządzać („dziel i zwyciężaj”). Jeśli problem jest tak złożony, że nie da się szczegółowo ogarnąć jego całości, można podzielić go na mniejsze fragmenty, które, jeśli to konieczne, można dalej dzielić. W ten sposób można myśleć o danym problemie na kilku różnych poziomach abstrakcji. Komputer to bardzo złożony obiekt. Nie sposób wyobrazić sobie wszystkich czynności, które muszą zostać wykonane podczas jego uruchamiania. Jednak można podzielić problem, mówiąc, że należy uruchomić wszystkie obiekty, z których składa się komputer: monitor, mysz, klawiaturę itd. Następnie można zagłębić się w konstrukcję każdego z tych obiektów. W ten sposób tworzy się obiekty składające się z części, które mogą zostać ponownie wykorzystane. Inna analogia: obiekt książka mógłby posiadać (agregować) jeden lub więcej obiektów autor, obiekt wydawca, kilka obiektów rozdział, indeks itd.
Dziedziczenie Dziedziczenie to bardzo elegancki sposób korzystania z już istniejącego kodu. Przykładowo wyobraźmy sobie, że istnieje ogólny obiekt Osoba, posiadający pola takie jak nazwisko czy data urodzenia, który posiada zaimplementowane funkcjonalności, takie jak chodzenie, mówienie czy spanie. Nagle okazuje się, że jest nam potrzebny obiekt Programista. Oczywiście można od nowa zaimplementować wszystkie pola i metody, które posiada Osoba. Rozsądniej jednak powiedzieć, że Programista dziedziczy po Osobie, i oszczędzić sobie trochę pracy. W obiekcie Programista konieczne będzie jedynie zaimplementowanie specjalistycznych funkcjonalności, takich jak „pisz kod”, natomiast za podstawowe czynności będzie odpowiadał kod klasy Osoba. W klasycznym programowaniu obiektowym klasy dziedziczą z innych klas. Ponieważ jednak JavaScript nie posiada klas, obiekty dziedziczą z innych obiektów. Kiedy obiekt dziedziczy z innego obiektu, najczęściej dodaje on nowe metody do już istniejących, tym samym rozszerzając stary obiekt. Zamiennie można stosować następujące dwa zdania: „B dziedziczy z A” i „B rozszerza A”. Obiekt dziedziczący może także zmienić definicję niektórych odziedziczonych metod, dostosowując je do swoich potrzeb. Nie zmienia się wówczas interfejs ani nazwa metody, jednak zmienia się zachowanie metody po jej wywołaniu. Zmianę działania odziedziczonej metody określa się mianem „przesłonięcia”3 lub „nadpisania”. 3
Nie mylić z „przeładowaniem” lub „przeciążeniem” — przyp. tłum.
29
JavaScript. Programowanie obiektowe
Polimorfizm W powyższym przykładzie obiekt Programista odziedziczył wszystkie metody od swojego rodzica, klasy Osoba. Oznacza to między innymi, że oba obiekty posiadają metodę mów. Wyobraźmy sobie, że gdzieś w kodzie znajduje się zmienna o nazwie Robert, a my nie wiemy, czy Robert jest Osobą, czy Programistą. Niezależnie od tego możemy wywołać metodę mów na obiekcie Robert i kod zadziała. Możliwość wywołania tej samej metody na różnych obiektach, przy czym obiekty mogą odpowiadać w różny sposób w zależności od typu, nazywamy polimorfizmem.
Programowanie obiektowe — podsumowanie Jeśli nie znasz się jeszcze na programowaniu obiektowym i nie masz pewności, czy w pełni rozumiesz wszystkie związane z nim terminy, nie martw się. Wkrótce zaczniemy pisać i analizować kod, a wtedy okaże się, że wszystkie te abstrakcyjne koncepcje w praktyce okazują się proste i pożyteczne. Na wszelki wypadek powtórzmy poznane terminy. Opis
Koncepcja
Robert jest człowiekiem (obiektem).
obiekty
Robert posiada dane osobowe — data urodzenia: 1 czerwca 1980, płeć: męska, włosy: czarne.
pola
Robert potrafi wykonać następujące polecenia: jedz, śpij, pij, śnij, mów i oblicz swój wiek.
metody
Robert jest instancją klasy Programista.
klasa (w klasycznym programowaniu obiektowym)
Robert jest wzorowany na innym obiekcie, o nazwie Programista.
prototyp (w prototypowym programowaniu obiektowym)
Robert posiada dane (takie jak data urodzenia) i metody, które działają na tych danych (takie jak oblicz wiek).
kapsułkowanie
Nie musimy wiedzieć, jak dokładnie działa metoda obliczająca wiek. hermetyzacja Obiekt może posiadać pewne prywatne dane, takie jak liczba dni w lutym w roku przestępnym — nie wiemy tego, i wcale nie chcemy wiedzieć. Robert jest częścią obiektu o nazwie Zespół, razem z Julią, która jest obiektem typu Projektant, oraz Jackiem, obiektem typu Kierownik Projektu.
30
agregacja, kompozycja
Rozdział 1. • Wprowadzenie
Opis
Koncepcja
Projektant, Kierownik Projektu oraz Programista to obiekty dziedziczące z obiektu Osoba.
dziedziczenie
Można wywołać metody Robert:mów, Jula:mów oraz Jacek:mów, z których polimorfizm, przesłanianie metod każda zadziała w inny sposób (Robert pewnie opowie o wydajności, Julia o urodzie, a Jacek o terminach). Każdy z obiektów odziedziczył metodę mów po obiekcie Osoba, a następnie dostosował ją do własnych potrzeb.
Konfiguracja środowiska rozwijania aplikacji Jeśli chodzi o pisanie kodu, autor książki przyjął zasadę „zrób to sam”, ponieważ jest gorącym zwolennikiem poglądu, że najlepszym sposobem na poznanie języka programowania jest programowanie. Z tego powodu nie istnieje żadne repozytorium, z którego można by pobrać kod przedstawiony w książce, by następnie przekleić go na swoją stronę. Wręcz przeciwnie — należy samodzielnie napisać kod, sprawdzić, jak działa, a potem ulepszać go w miarę potrzeb. Podczas sprawdzania przykładów zamieszczonych w książce warto korzystać z konsoli Firebug. Już tłumaczę, jak to zrobić.
Niezbędne narzędzia Domyślam się, że jako programista korzystasz z Firefoksa podczas codziennego przeglądania stron internetowych. Jeśli tak nie jest, zrób sobie przysługę i zainstaluj go od razu. Jest darmowy i działa na wszystkich najpopularniejszych platformach — pod Windowsem, Linuksem i Mac OS. Można go pobrać ze strony http://www.mozilla.com/firefox/. Istnieje wiele ciekawych rozszerzeń do Firefoksa (wszystkie napisane w języku JavaScript!). Jednym z nich jest Firebug — niezastąpione narzędzie do programowania stron, które posiada szereg przydatnych opcji. Można go pobrać ze strony http://www.getfirebug.com/. Po instalacji należy uruchomić Firefoksa i przejść na dowolną stronę, a następnie wcisnąć F12 (pod Windows) albo kliknąć małą ikonkę owada w prawym dolnym rogu okna przeglądarki. W ten sposób otwiera się najciekawszą dla nas część narzędzia Firebug — konsolę.
31
JavaScript. Programowanie obiektowe
Korzystanie z konsoli Firebug
Kod można wpisywać bezpośrednio w konsoli Firebug — zostanie on wykonany po wciśnięciu Enter. Wartość zwracana przez kod jest wypisywana na konsoli. Kod jest wykonywany w kontekście aktualnie załadowanej strony. By to sprawdzić, wpisz document.location.href — zwrócony zostanie adres URL bieżącej strony. Konsola została wyposażona w funkcję automatycznego uzupełniania, podobną do tej w linii komend systemu operacyjnego. Przykładowo jeśli wpiszesz docu i wciśniesz Tab, docu zostanie uzupełnione do postaci document. Jeśli następnie dopiszesz . (operator kropki), kolejne naciśnięcia Tab będą powodowały iterowanie po wszystkich dostępnych polach i metodach obiektu document. Przy pomocy klawiszy góra ( ) i dół ( ) można przewijać listę już wykonanych poleceń w celu wykonania któregoś z nich. W konsoli mamy do dyspozycji tylko jedną linię, ale można wydać więcej poleceń, jeśli oddzieli się je za pomocą średników (;). Jeśli potrzebujesz więcej miejsca lub więcej linii, możesz otworzyć konsolę w trybie wielu linii: w tym celu musisz kliknąć strzałkę w górę, która znajduje się po prawej stronie wejściowej linii konsoli. Efekt jest pokazany na rysunku4 na następnej stronie. Przykład pokazuje wykorzystanie konsoli do wpisania kodu, który podmieni logo na stronie google.com na dowolnie wybrany obraz. Jak widać, kod można testować w czasie rzeczywistym na dowolnej stronie.
4
Kod nie zadziała w polskiej wersji wyszukiwarki, ponieważ logo Google nie zostało tam otoczone znacznikami img — przyp. tłum.
32
Rozdział 1. • Wprowadzenie
Na tym etapie warto zmienić ustawienia jednej z opcji konfiguracyjnych Firefoksa, by włączyć wysoki poziom ostrzeżeń w konsoli Firebug. Ostrzeżenia nie są błędami, jednak nie powinny one pojawiać się w kodzie dobrej jakości. Przykładowo użycie niezadeklarowanej zmiennej nie jest błędem, ale nie jest też dobrą praktyką, zatem Firefox wygeneruje ostrzeżenie, które pojawi się w konsoli, jeśli ustawimy wysoki (strict) poziom ostrzeżeń. Robi się to w następujący sposób: 1. Wpisz about:config w pasku adresowym przeglądarki. 2. Wyszukaj linie zawierające strict, wpisując to słowo w polu Filter, i wciśnij Enter. 3. Wykonaj dwuklik na linii javascript.options.strict. Wartość powinna zostać ustawiona na true.
Podsumowanie W tym rozdziale opowiedziałem, jak powstał JavaScript oraz na jakim etapie swojego rozwoju znajduje się dzisiaj. Przedstawiłem najważniejsze pojęcia związane z programowaniem obiektowym oraz pokazałem, że JavaScript nie jest klasycznym, tylko prototypowym językiem obiektowym. Pokazałem też, jak uczyć się programowania w tym języku przy pomocy konsoli Firebug. Jesteśmy gotowi, by zagłębić się w techniczne aspekty języka i poznać jego zaawansowane funkcjonalności obiektowe. Więcej informacji na tematy poruszone w tym rozdziale można znaleźć na przestawionych poniżej stronach. 33
JavaScript. Programowanie obiektowe
Q Na stronie YUI Theater (http://developer.yahoo.com/yui/theater/) umieszczono
kilka bardzo wartościowych wykładów (w języku angielskim) Douglasa Crockforda. Część pierwsza, „Theory of the DOM”, poświęcona jest historii przeglądarek, a część druga, „The JavaScript Programming Language”, przedstawia między innymi historię JavaScriptu. Q Najważniejsze terminy związane z programowaniem obiektowym przedstawiono w odpowiednim artykule w Wikipedii: http://en.wikipedia.org/wiki/Object-oriented_ programming. Wersja polska (http://pl.wikipedia.org/wiki/Programowanie_ obiektowe) jest w tej chwili uboższa od angielskiej. Warto także zapoznać się z dokumentacją języka Java na stronie firmy Sun (http://java.sun.com/docs/books/tutorial/java/concepts/index.html), należy jednak pamiętać, że mowa tam o językach obiektowych korzystających z klas. Q Możliwości dzisiejszego JavaScriptu są dobrze widoczne na stronie z widżetami Yahoo! (http://widgets.yahoo.com/), na stronie Google Maps (http://maps.google.com/) oraz w wersji języka graficznego Processing przetłumaczonego na JavaScript (http://ejohn.org/blog/processingjs/).
34
2 Proste typy danych, tablice, pętle i warunki Zanim przejdziemy do obiektowych funkcjonalności JavaScriptu, przyjrzyjmy się jego podstawom. Ten rozdział przedstawia: Q proste typy danych, takie jak łańcuchy znaków i liczby; Q tablice; Q popularne operatory, takie jak +, -, delete i typeof; Q polecenia sterujące, takie jak pętle oraz warunki if…else.
Zmienne Zmienne służą do przechowywania danych. Podczas pisania programów wygodniej jest korzystać ze zmiennych niż z samych danych, jako że łatwiej jest napisać pi niż 3.141592653589793, zwłaszcza jeśli dane te są potrzebne w więcej niż jednym miejscu programu. Dane przechowywane wewnątrz zmiennej można zmienić, stąd nazwa. Zmienne stosuje się także wtedy, gdy wartość danych nie jest znana podczas pisania kodu, na przykład gdy wartość jest wynikiem działania przeprowadzanego już po uruchomieniu programu. By móc korzystać ze zmiennej, należy wykonać dwa kroki: Q zadeklarować zmienną, Q zainicjalizować ją, czyli nadać jej wartość.
JavaScript. Programowanie obiektowe
Do deklaracji zmiennych służy słówko var. Na przykład: var var var var
a; toJestZmienna; _i_to_tez; mix12trzy;
Nazwy zmiennych mogą składać się z dowolnej kombinacji liter, cyfr oraz znaku podkreślnika (_). Nie można jednak zacząć od cyfry, dlatego nie jest poprawna poniższa deklaracja: var 2trzy4piec;
Inicjalizacja zmiennej to nadanie jej wartości po raz pierwszy. Można zrobić to na dwa sposoby: Q najpierw zadeklarować zmienną, a potem ją zainicjalizować; Q zadeklarować i zainicjalizować zmienną za pomocą jednego polecenia. Oto przykład zastosowania drugiego podejścia: var a = 1;
Zmienna o nazwie a ma teraz wartość 1. Można zadeklarować (i, jeśli ktoś chce, zainicjalizować) kilka zmiennych za pomocą jednego słówka var. Wystarczy tylko rozdzielić deklaracje za pomocą przecinków: var v1, v2, v3 = 'halo', v4 = 4, v5;
Wielkość liter ma znaczenie W nazwach zmiennych rozróżniane są wielkie i małe litery. Można to sprawdzić przy użyciu konsoli Firebug. Wpisz w konsoli poniższy kod, wciskając Enter na końcu każdej linii: var wielkosc_liter_ma_znaczenie = 'male'; var WIELKOSC_LITER_MA_ZNACZENIE = 'wielkie'; wielkosc_liter_ma_znaczenie WIELKOSC_LITER_MA_ZNACZENIE
Możesz przyspieszyć wpisywanie kodu, w trzeciej linii wpisując jedynie wie i wtedy wciskając klawisz Tab. Konsola uzupełni nazwę zmiennej do postaci wielkosc_liter_ma_znaczenie. Analogicznie, w czwartej linii wystarczy wpisać WIE. Wynik został pokazany na rysunku na następnej stronie. W pozostałej części książki nie będę już umieszczał zrzutów ekranu, a jedynie tekst wpisywany i otrzymany w konsoli:
36
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
>>> var wielkosc_liter_ma_znaczenie = 'male'; >>> var WIELKOSC_LITER_MA_ZNACZENIE = 'wielkie'; >>> wielkosc_liter_ma_znaczenie
"male" >>> WIELKOSC_LITER_MA_ZNACZENIE
"wielkie" W liniach zaczynających się trzema znakami większości (>>>) widać kod wpisany przez programistę, pozostała część to wynik operacji wydrukowany w konsoli. Chcę jeszcze raz zachęcić Cię do wpisywania w konsoli przykładu za każdym razem, gdy się pojawi, i może zmieniania go trochę — w ten sposób lepiej zrozumiesz, jak dokładnie działa kod.
Operatory Operatory pobierają jedną lub dwie wartości (lub zmienne) jako argumenty, wykonują na nich pewną operację, a następnie zwracają wartość wynikową. Poniżej przedstawiam prosty przykład użycia operatora, w celu uzgodnienia terminologii: >>> 1 + 2
3 W przykładzie: Q + jest operatorem. Q Operacją (działaniem) jest dodawanie.
37
JavaScript. Programowanie obiektowe
Q Wartości wejściowe (inaczej argumenty) to 1 i 2. Q Wynikiem jest liczba 3.
Zamiast wartości 1 i 2 można użyć zmiennych. Można również użyć zmiennej do przechowania wyniku operacji, co pokazuje następny przykład: >>> var a = 1; >>> var b = 2; >>> a + 1;
2 >>> b + 2;
4 >>> a + b
3 >>> var c = a + b; >>> c
3 Poniższa tabela zawiera podstawowe operatory arytmetyczne: Symbol operatora
Operacja
+
Dodawanie
Przykład >>> 1 + 2
3 Odejmowanie
-
>>> 99.99 – 11
88.99 Mnożenie
*
>>> 2 * 3
6 /
Dzielenie
%
Modulo, czyli reszta z dzielenia
>>> 6 / 4
1.5 >>> 6 % 3
0 >>> 5 % 3
2 Czasami potrzebna jest możliwość sprawdzenia, czy liczba jest parzysta, czy nieparzysta. Operator % bardzo to ułatwia. Liczby nieparzyste dzielone przez 2 zwrócą wartość 1, zaś liczby parzyste zwrócą 0. >>> 4 % 2
0 >>> 5 % 2
1
38
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Symbol operatora
Operacja
Przykład
++
Zwiększenie (inkrementacja) wartości o 1
Inkrementację dzielimy na postinkrementację i preinkrementację. O postinkrementacji mówimy, gdy wartość wejściowa jest zwiększana po jej zwróceniu. >>> var a = 123;var b = a++; >>> b
123 >>> 1
124
Preinkrementacja ma miejsce, gdy wartość najpierw jest zwiększana, a potem zwracana. >>> var a = 123;var b = ++a; >>> b
124 >>> 1
124 --
Zmniejszenie (dekrementacja) wartości o 1
Postdekrementacja >>> var a = 123;var b = a--; >>> b
122 >>> 1
124
Predekrementacja >>> var a = 123;var b = --a; >>> b
122 >>> 1
122
Proste przypisanie wartości, na przykład var a = 1, również jest operacją. Znak = to operator prostego przypisania. Istnieje rodzina operatorów, które są połączeniem operatorów przypisania i działania arytmetycznego. Są to tak zwane operatory złożone. Pozwalają one zmniejszyć ilość kodu programu. Poniżej przedstawiam kilka przykładów. >>> var a = 5 >>> a += 3;
8 a += 3; to skrócona wersja wyrażenia a = a + 3;
39
JavaScript. Programowanie obiektowe
>>> a -= 3;
5 a -=3; odpowiada a = a – 3;
Podobnie: >>> a *= 2;
10 >>> a /= 5;
2 >>> a %= 2
0 Oprócz operatorów arytmetycznych i przypisania istnieją jeszcze inne operatory, które pojawią się w tym i kolejnych rozdziałach książki.
Proste typy danych Każda wartość, jakiej można użyć w kodzie, jest pewnego typu. W języku JavaScript istnieją następujące proste typy danych: 1. Liczba — może to być liczba zmiennoprzecinkowa lub całkowita, na przykład 1, 100, 3.14. 2. Łańcuch znaków (string) — ciąg znaków dowolnej długości, na przykład "a", "jeden", "pięć lub sześć". 3. Boolean (wartość boolowska) — może przyjmować wartości true (prawda) lub false (fałsz). 4. Niezdefiniowany — jeśli spróbujesz pobrać wartość zmiennej, która nie istnieje, otrzymasz specjalną wartość undefined. To samo stanie się podczas próby odczytu zmiennej, która została zadeklarowana, ale jeszcze nie otrzymała wartości. JavaScript nada jej wówczas wartość undefined. 5. Null — jest to kolejny specjalny typ danych. Obejmuje on tylko jedną wartość, null, która oznacza brak wartości, wartość pustą, nic. Różnica pomiędzy null a undefined jest taka, że zmienna o wartości null jest uważana za zdefiniowaną, tyle że jej wartością jest nic. Wkrótce pokażę to na przykładach. Każda zmienna, która nie należy do żadnego z wymienionych pięciu typów prostych, jest obiektem. Nawet null czasem uważa się za obiekt, choć nietypowy — byt, którego właściwie nie ma. Obiektami zajmiemy się szczegółowo w rozdziale 4. Teraz wystarczy zapamiętać, że zmienna może albo: 40
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Q być typu prostego (pięć typów danych opisanych powyżej), albo Q być obiektem.
Ustalanie typu danych — operator typeof Jeśli nie wiemy, jakiego typu jest zmienna lub wartość, możemy użyć operatora typeof. Operator ten zwraca tekst reprezentujący typ danych. Zwracaną wartością może być "number" (liczba), "string", "boolean", "undefined", "object" lub "function". W kolejnych podrozdziałach pokażę działanie typeof na przykładzie wszystkich pięciu prostych typów danych.
Liczby Najprostszym przykładem liczby jest liczba całkowita. Jeśli przypiszesz zmiennej wartość 1 i użyjesz operatora typeof, zwrócony zostanie łańcuch znaków "number". W poniższym przykładzie widać również, że słówko var jest obowiązkowe tylko podczas pierwszego nadania wartości zmiennej. >>> var n = 1; >>> typeof n;
"number" >>> n = 1234; 1234 >>> typeof n;
"number" Istnieją jeszcze liczby zmiennoprzecinkowe (ułamki): >>> var n2 = 1.23; >>> typeof n2;
"number" typeof można również wywołać na wartości nieprzypisanej do żadnej zmiennej: >>> typeof 123;
"number"
Liczby ósemkowe i szesnastkowe Jeśli liczba zaczyna się cyfrą 0, jest uznawana za liczbę ósemkową. Przykładowo ósemkowa liczba 0377 odpowiada dziesiętnej liczbie 255.
41
JavaScript. Programowanie obiektowe
>>> var n3 = 0377; >>> typeof n3;
"number" >>> n3;
255 W ostatniej linii przykładu wypisana została dziesiętna reprezentacja liczby ósemkowej. Częściej od liczb ósemkowych używane są liczby szesnastkowe (heksadecymalne), które wykorzystuje się między innymi do definiowania kolorów w arkuszach stylów CSS. W CSS kolory można definiować na kilka sposobów. Oto dwa z nich: Q Wykorzystanie wartości dziesiętnych do określenia ilości R (czerwieni), G (zieleni) oraz B (niebieskiego) za pomocą wartości od 0 do 255. Przykładowo rgb(0, 0, 0) oznacza kolor czarny, a rgb(255, 0, 0) to czerwony (maksymalna ilość czerwieni i ani trochę zielonego lub niebieskiego). Q Wykorzystanie liczb szesnastkowych do określenia ilości danego koloru za pomocą tylko dwóch znaków. Przykładowo #000000 to czarny, a #ff0000 to czerwony. Jest tak dlatego, że ff w systemie szesnastkowym odpowiada dziesiętnej liczbie 255. Liczby szesnastkowe w języku JavaScript zaczynają się od 0x. >>> var n4 = 0x00; >>> typeof n4;
"number" >>> n4;
0 >>> var n5 = 0xff; >>> typeof n5;
"number" >>> n5;
255
Wykładniki potęg 1e1 (co można zapisać również jako 1e+1, 1E1 lub 1E+1) odpowiada liczbie jeden z jednym zerem, czyli liczbie 10. Analogicznie, 2e+3 oznacza liczbę 2 z trzema zerami, czyli 2000. >>> 1e1
10
42
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
>>> 1e+1
10 >>> 2e+3
2000 >>> typeof 2e+3;
"number" Zapis 2e+3 oznacza, że należy przesunąć kropkę dziesiętną (w tradycyjnym zapisie w języku polskim jest to przecinek) przy liczbie 2 w prawo 3 pozycje. Zapis 2e-3 oznaczałby, że należy przesunąć kropkę w lewo.
>>> 2e-3
0.002 >>> 123.456E-3
0.123456 >>> typeof 2e-3
"number"
Nieskończoność JavaScript posiada specjalną wartość o nazwie Infinity (nieskończoność). Służy ona do reprezentacji liczb, które są zbyt duże, by JavaScript mógł je obsłużyć. Infinity jest liczbą, co można sprawdzić za pomocą operatora typeof. Można także szybko sprawdzić, że możliwe jest wpisanie liczby z 308 zerami, jednak 309 zer to już za dużo. Jeśli Cię to interesuje, największa liczba, jakiej możesz użyć, to 1.7976931348623157e+308, a najmniejsza 5e-324. >>> Infinity
Infinity >>> typeof Infinity
"number" >>> 1e309
Infinity
43
JavaScript. Programowanie obiektowe
>>> 1e308
1e+308 Wynikiem dzielenia przez zero jest nieskończoność. >>> var a = 6 / 0; >>> a
Infinity Nieskończoność oznacza największą liczbę (a raczej liczbę nieco większą od największej), a co z liczbą najmniejszą? Zapisujemy ją jako nieskończoność ze znakiem minus, czyli minus nieskończoność. >>> var i = -Infinity; >>> i
-Infinity >>> typeof i
"number" Czy to oznacza, że korzystamy z czegoś, co ma rozmiar dokładnie dwóch nieskończoności — od zera do nieskończoności, a potem od zera w dół do minus nieskończoności? No cóż, ma to jedynie wartość rozrywkową — nie da się wykorzystać tego faktu w żaden praktyczny sposób. Jeśli dodamy do siebie nieskończoność i minus nieskończoność, nie otrzymamy zera, tylko wartość NaN (Not a Number, czyli wartość niebędąca liczbą). >>> Infinity - Infinity
NaN >>> -Infinity + Infinity
NaN Każde inne działanie arytmetyczne z nieskończonością jako argumentem zwróci wartość Infinity: >>> Infinity - 20
Infinity >>> -Infinity * 3
-Infinity >>> Infinity / 2
Infinity >>> Infinity - 99999999999999999
Infinity 44
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
NaN Czym zatem jest NaN, które widzieliśmy przed chwilą? Wychodzi na to, że wbrew swojej nazwie „wartość niebędąca liczbą” jest specjalną wartością, która… także jest liczbą. >>> typeof NaN
"number" >>> var a = NaN; >>> a
NaN NaN otrzymasz, jeśli spróbujesz wykonać działanie na czymś, co powinno być liczbą, i to działanie nie powiedzie się. Na przykład jeśli spróbujesz pomnożyć 10 przez znak "f", wynikiem będzie NaN, ponieważ w oczywisty sposób nie da się ustalić wyniku takiego mnożenia. >>> var a = 10 * "f"; >>> a
NaN NaN jest jak wirus: nawet jeśli tylko jeden z argumentów działania ma wartość NaN, cały wynik można wyrzucić do kosza. >>> 1 + 2 + NaN
NaN
Łańcuchy znaków Łańcuch znaków (string) to ciąg znaków służący do reprezentacji pewnego tekstu. W języku JavaScript każda wartość umieszczona w cudzysłowie (lub otoczona apostrofami) zostanie uznana za łańcuch znaków. Oznacza to, że 1 jest liczbą, ale "1" jest łańcuchem. Operator typeof zastosowany na zmiennej lub wartości tego typu zwróci łańcuch "string". >>> var s = "jakieś znaki"; >>> typeof s;
"string" >>> var s = 'znaki i cyfry 123 5.87'; >>> typeof s;
"string" Oto przykład liczby użytej w kontekście tekstowym: >>> var s = '1'; >>> typeof s;
"string"
45
JavaScript. Programowanie obiektowe
Jeśli użyjesz pustego cudzysłowu, nadal będzie to łańcuch, tyle że pusty: >>> var s = ""; typeof s;
"string" Wcześniej widzieliśmy już przykład użycia operatora + na argumentach będących liczbami — realizował on dodawanie. Jeśli użyjemy tego samego operatora na łańcuchach znaków, spowoduje on wykonanie operacji konkatenacji (czyli sklejenia) łańcuchów. >>> var s1 = "raz"; var s2 = "dwa"; var s = s1 + s2; s;
"razdwa" >>> typeof s;
"string" Podwójne znaczenie operatora + może prowadzić do błędów. Z tego powodu dobrze jest przed konkatenacją upewnić się, że argumenty są łańcuchami, a przed dodawaniem — że są liczbami. Nieco później pokażę różne sposoby takiego sprawdzania.
Konwersje łańcuchów Jeśli jako argument działania arytmetycznego zostanie przekazany łańcuch, zostanie on zamieniony (przekonwertowany) na liczbę (dotyczy to wszystkich działań z wyjątkiem dodawania, ponieważ w przypadku łańcuchów operator + oznacza konkatenację). >>> var s = '1'; s = 3 * s; typeof s;
"number" >>> s
3 >>> var s = '1'; s++; typeof s;
"number" >>> s
2 Leniwa metoda zamiany dowolnego łańcucha znaków reprezentującego liczbę na liczbę to pomnożenie go przez 1 (lepsza metoda sprowadza się do wywołania funkcji o nazwie parseInt(), co pokażę w następnym rozdziale): >>> var s = "100"; typeof s;
"string" >>> s = s * 1;
100 46
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
>>> typeof s;
"number" Jeśli konwersja się nie powiedzie, wynikiem będzie NaN: >>> var d = '101 dalmatyńczyków'; >>> d * 1
NaN Leniwy sposób zamiany dowolnej wartości na łańcuch znaków polega na jej konkatenacji z pustym łańcuchem: >>> var n = 1; >>> typeof n;
"number" >>> n = "" + n;
"1" >>> typeof n;
"string"
Znaki specjalne Niektóre łańcuchy znaków mają specjalne znaczenie, co pokazuje poniższa tabela: Łańcuch znaków
Znaczenie
\
\ to znak uniku.
\\
Jeśli napis ma zawierać apostrofy lub cudzysłowy, trzeba poprzedzić je znakiem uniku, aby nie zostały uznane za koniec napisu.
\' \"
Jeśli napis ma zawierać wsteczny ukośnik (\), należy wpisać \\.
Przykład >>> var s = 'Apostrof w środku łańcucha '!';
Powyższe przypisanie spowoduje błąd, ponieważ JavaScript uzna, że utworzyliśmy napis 'Apostrof w środku łańcucha ', i nie będzie umiał przetworzyć reszty. Poprawne są następujące fragmenty kodu: >>> >>> >>> >>> >>>
var var var var var
s s s s s
= = = = =
'Apostrof w "Apostrof w "Apostrof w 'Powiedział "Powiedział
środku łańcucha \'!'; środku łańcucha \'!"; środku łańcucha '!"; "cześć"!'; \"cześć\"!";
Zastosowanie znaku uniku ze znakiem uniku: >>> var s = "1\\2"; s;
"1\2"
47
JavaScript. Programowanie obiektowe
Łańcuch znaków
Znaczenie
\n
Koniec linii.
Przykład >>> var s = '\n1\n2\n3\n' >>> s
" 1 2 3 " \r
Powrót karetki.
Wszystkie poniższe przypisania: >>> var s = '1\r2'; >>> var s = '1\n\r2'; >>> var s = '1\r\n2';
dadzą wynik: >>> s
"1 2" Znak tabulacji.
\t
>>> var s = "1\t2" >>> s
"1 2" \u
Łańcuch \u pozwala korzystać ze znaków Unicode. Należy po nim podać kod znaku.
Oto moje bułgarskie imię zapisane cyrylicą: >>> "\u0421\u0442\u043E\u044F\u043D"
"
"
Są jeszcze dodatkowe znaki specjalne, których używa się bardzo rzadko: \b (znak powrotu), \v (pionowa tabulacja) oraz \f (wysunięcie strony).
Typ boolean Do typu boolean należą tylko dwie wartości: true (prawda) oraz false (fałsz). Używa się ich bez cudzysłowów: >>> var b = true; typeof b;
"boolean" >>> var b = false; typeof b;
"boolean"
48
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Jeśli true lub false umieści się w cudzysłowie, staną się one łańcuchami znaków: >>> var b = "true"; typeof b;
"string"
Operatory logiczne Istnieją trzy operatory, tak zwane operatory logiczne, które działają na wartościach boolowskich. Są to: Q ! — logiczny operator NOT (negacja) Q && — logiczny operator AND („i”, koniunkcja) Q || — logiczny operator OR („lub”, alternatywa)
Wszyscy wiemy, że jeśli coś nie jest prawdziwe, jest fałszywe. Oto to samo twierdzenie wyrażone za pomocą JavaScriptu i logicznego operatora !: >>> var b = !true; >>> b;
false Podwójne użycie ! przywróci pierwotną wartość: >>> var b = !!true; >>> b;
true Jeśli użyjemy operatora logicznego na wartości, która nie jest typu boolean, zostanie ona zamieniona na ten typ. >>> var b = "jeden"; >>> !b;
false W powyższym przykładzie łańcuch "jeden" został zamieniony na wartość boolowską true, a następnie zanegowany — wynikiem negacji true jest false. W kolejnym przykładzie stosujemy podwójny operator negacji, zatem wynikiem będzie true. >>> var b = "jeden"; >>> !!b;
true Użycie podwójnej negacji jest prostym sposobem zmiany dowolnej wartości na jej odpowiednik w typie boolean. Co prawda nie będzie to potrzebne często, jednak warto zrozumieć, w jaki sposób wartości są zamieniane na typ boolean. Otóż większość wartości zostanie zamieniona na true. Oto wyjątki (które zostaną zamienione na false):
49
JavaScript. Programowanie obiektowe
Q pusty łańcuch "" (lub ''), Q null, Q undefined, Q liczba 0, Q liczba NaN, Q boolowskie false.
O powyższych wartościach mówi się czasem, że są fałszywe, podczas gdy pozostałe wartości są prawdziwe (w tym między innymi łańcuchy "0" oraz "false"). Przejdźmy teraz do przykładów pozostałych dwóch operatorów logicznych — operatorów AND oraz OR. Jeśli użyjemy AND (&&), wynik ma wartość true wtedy i tylko wtedy, gdy wszystkie argumenty mają wartość true. W przypadku OR (!!) wynik ma wartość true wtedy i tylko wtedy, gdy przynajmniej jeden z operatorów ma wartość true. >>> var b1 = true; var b2 = false; >>> b1 || b2
true >>> b1 && b2
false Poniższa tabela zawiera wszystkie możliwe operacje na dwóch argumentach i ich wyniki: Operacja
Wynik
true && true
true
true && false
false
false && true
false
false && false
false
true || true
true
true || false
true
false || true
true
false || false
false
Można użyć kilku operatorów logicznych w jednym wyrażeniu: >>> true && true && false && true
false >>> false || true || false
true
50
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Można także łączyć operatory && z ||. W takim wypadku należy skorzystać z nawiasów, by jednoznacznie określić kolejność wykonywania działań. Na przykład: >>> false && false || true && true
true >>> false && (false || true) && true
false
Priorytety operatorów Być może zastanawiasz się, dlaczego pierwsze z powyższych wyrażeń (false && false || true && true) zwróciło wartość true. Odpowiedź jest związana z priorytetami operatorów. Pewnie pamiętasz z lekcji matematyki, że: >>> 1 + 2 * 3
7 Jest tak dlatego, że mnożenie ma priorytet wyższy niż dodawanie, zatem najpierw obliczane jest 2 * 3, co odpowiada zapisowi: >>> 1 + (2 * 3)
7 W przypadku operatorów logicznych jest podobnie. Najwyższy priorytet ma operator !. Odpowiadające mu działanie jest wykonywane najpierw, chyba że wyrażenie zawiera nawiasy, które każą zrobić co innego. Następny w kolejności jest operator &&, a ostatni ||. Innymi słowy: >>> false && false || true && true
true jest równoważne: >>> (false && false) || (true && true)
true Dobra rada Zamiast polegać na priorytetach operatorów, używaj nawiasów. Dzięki temu Twój kod będzie bardziej czytelny i łatwiejszy do zrozumienia.
51
JavaScript. Programowanie obiektowe
Leniwe wartościowanie Jeśli wyrażenie zawiera kilka następujących po sobie operatorów logicznych, a wynik staje się oczywisty przed osiągnięciem końca wyrażenia, pozostałe operacje nie zostaną wykonane, ponieważ nie mają wpływu na wynik. Na przykład: >>> true || false || true || false || true
true Ponieważ wszystkie operatory są operatorami OR i mają ten sam priorytet, wynik ma wartość true, jeśli co najmniej jeden z argumentów to true. Skoro pierwszy argument ma wartość true,
silnik JavaScript postanawia być leniwy (no dobrze, wydajny) i rezygnuje ze sprawdzania kodu, który nie może wpłynąć na wynik. Można sprawdzić to zachowanie, wpisując w konsoli: >>> var b = 5; >>> true || (b = 6)
true >>> b
5 >>> true && (b = 6)
6 >>> b
6 Przy okazji widzimy kolejne ciekawe zachowanie JavaScriptu — jeśli argumentem operacji logicznej jest wyrażenie nieboolowskie (niemające wartości logicznej), jako wynik zostanie zwrócona wartość tego wyrażenia. >>> true || "coś"
true >>> true && "coś"
"coś" Zasadniczo należy unikać tego zachowania, ponieważ sprawia ono, że kod staje się nieczytelny. Czasami używa się tego mechanizmu do definiowania zmiennych, kiedy nie wiadomo, czy nie zostały one zdefiniowane wcześniej. W poniższym przykładzie, jeśli zmienna v jest zdefiniowana, jej wartość zostaje zachowana; w przeciwnym przypadku zmienna zostaje zainicjalizowana wartością 10. var mojaliczba = mojaliczba || 10;
52
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Zapis jest prosty i elegancki, jednak w niektórych sytuacjach może wywieść nas na manowce. Jeśli zmienna mojaliczba jest zdefiniowana i posiada wartość 0 (lub inną z sześciu fałszywych wartości), działanie kodu będzie inne niż zamierzone.
Porównywanie Istnieje jeszcze jeden zbiór operatorów, które jako wynik zwracają wartość boolean. Są to tak zwane operatory porównania. Opisuje je poniższa tabela. Symbol operatora
Opis
Przykład
==
Sprawdzenie równości:
>>> 1 == 1
zwraca true, jeśli wartości argumentów są sobie równe. Przed porównaniem argumenty są sprowadzane do tego samego typu.
true >>> 1 == 2
false >>> 1 == '1'
true ===
!=
Sprawdzenie równości oraz zgodności typów:
>>> 1 === '1'
zwraca true, jeśli wartości argumentów są sobie równe i są tego samego typu. Ten sposób porównywania jest zasadniczo lepszy i bezpieczniejszy, ponieważ nie jest przeprowadzana żadna niekontrolowana konwersja typów.
false
Sprawdzenie różności:
>>> 1 != 1
zwraca true, jeśli argumenty nie są sobie równe (po konwersji typów).
false
>>> 1 === 1
true
>>> 1 != '1'
false >>> 1
true !==
Sprawdzenie różności bez konwersji typów:
>>> 1 !== 1
zwraca true, jeśli argumenty nie są sobie równe lub są różnego typu.
false >>> 1 !== '1'
true >
Zwraca true, jeśli lewy argument jest większy od prawego.
>>> 1 > 1
false >>> 33 > 22
true >=
Zwraca true, jeśli lewy argument jest większy od prawego lub argumenty są równe.
>>> 1 >= 1
true
53
JavaScript. Programowanie obiektowe
Symbol operatora
Opis
<
Zwraca true, jeśli lewy argument jest mniejszy od prawego.
Przykład >>> 1 < 1
false >>> 1 < 2
true Zwraca true, jeśli lewy argument jest mniejszy od prawego lub argumenty są równe.
<=
>>> 1 <= 1
true >>> 1 <= 2
true
Warto zapamiętać, że NaN nie jest równe niczemu innemu, nawet sobie. >>> NaN == NaN
false
Undefined i null Wartość undefined otrzymasz, jeśli spróbujesz użyć zmiennej, która nie istnieje lub której nie została przypisana żadna wartość. Jeśli zadeklarujesz zmienną bez inicjalizacji, JavaScript automatycznie nada jej wartość undefined. Próba odwołania się do takiej zmiennej zakończy się błędem: >>> foo
foo is not defined Operator typeof użyty na nieistniejącej zmiennej zwróci łańcuch "undefined": >>> typeof foo
"undefined" Jeśli zmienna została zadeklarowana, ale nie nadano jej wartości, odwołanie się do niej nie powoduje błędu, jednak operator typeof nadal zwraca wartość "undefined": >>> var somevar; >>> somevar >>> typeof somevar
"undefined" Z wartością null jest trochę inaczej. JavaScript sam nie nadaje zmiennym tej wartości — może to zrobić jedynie programista w swoim kodzie.
54
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
>>> var somevar = null
null >>> somevar
null >>> typeof somevar
"object" Chociaż różnica pomiędzy null i undefined może się wydawać niewielka, są sytuacje, w których ma kluczowe znaczenie. Możemy na przykład otrzymać odmienne wyniki działań arytmetycznych: >>> var i = 1 + undefined; i;
NaN >>> var i = 1 + null; i;
1 Dzieje się tak dlatego, że null i undefined są w różny sposób zamieniane na inne typy proste. Poniższe przykłady pokazują możliwe przekształcenia. Konwersja na liczbę: >>> 1*undefined
NaN >>> 1*null
0 Konwersja na typ boolean: >>> !!undefined
false >>> !!null
false Konwersja na łańcuch znaków: >>> "" + null
"null" >>> "" + undefined
"undefined"
55
JavaScript. Programowanie obiektowe
Proste typy danych — podsumowanie Podsumujmy krótko, co zostało powiedziane do tej pory: Q Istnieje pięć prostych typów danych: Q Q Q
liczba, łańcuch znaków (string), boolean,
undefined (niezdefiniowany), Q null. Q Wszystko, co nie jest typu prostego, jest obiektem. Q
Q Typ liczbowy służy do przechowywania dodatnich lub ujemnych liczb całkowitych
i zmiennoprzecinkowych, liczb szesnastkowych i ósemkowych, liczb zapisanych za pomocą wykładników potęg oraz specjalnych liczb NaN, Infinity oraz –Infinity. Q Łańcuchy to ciągi znaków w cudzysłowie lub apostrofach. Q Możliwe wartości typu boolean to true i false. Q Typ danych null składa się z tylko jednej wartości: null. Q Typ danych undefined składa się z tylko jednej wartości: undefined. Q Podczas konwersji na typ boolean wszystkie wartości zostaną zamienione na true,
z wyjątkiem sześciu wartości fałszywych: Q
" "
Q
null
Q
undefined
Q
0
Q
NaN
Q
false
Tablice Skoro znasz już proste typy danych, pora przejść do nieco ciekawszej struktury danych — tablicy. By zadeklarować zmienną i jako wartość przypisać jej pustą tablicę, należy użyć pustych nawiasów kwadratowych: >>> var a = []; >>> typeof a;
"object"
56
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Operator typeof zwraca "object", ale na razie nie będziemy się tym martwić — zastanowimy się nad tym faktem nieco później, gdy przejdę do omawiania obiektów. Tablicę zawierającą trzy elementy definiuje się w następujący sposób: >>> var a = [1,2,3];
Jeśli w konsoli Firebug wpiszemy nazwę tablicy, zostanie wypisana jej zawartość: >>> a
[1, 2, 3] Czym zatem jest tablica? Jest po prostu listą wartości. Nie jest konieczne używanie jednej zmiennej do przechowania jednej wartości — można trzymać dowolnie wiele wartości jako elementy jednej tablicy. Tylko jak się do nich odwołać? Elementy są indeksowane liczbami, począwszy od 0. Pierwszy element ma indeks (inaczej pozycję) 0, drugi ma indeks 1, i tak dalej. Oto trzyelementowa tablica z poprzedniego przykładu: Indeks
Wartość
0
1
1
2
2
3
Aby odwołać się do elementu tablicy, należy podać indeks, otoczony nawiasami kwadratowymi. a[0] oznacza pierwszy element tablicy a, a[1] drugi i tak dalej. >>> a[0]
1 >>> a[1]
2
Dodawanie i aktualizacja elementów tablicy Za pomocą indeksów można nie tylko odczytywać elementy tablicy, ale także je aktualizować (zmieniać). Poniższy przykład pokazuje zmianę trzeciego elementu tablicy (czyli elementu o indeksie 2). Następnie tablica jest wypisywana na ekran. >>> a[2] = 'trzy';
"trzy" >>> a
[1, 2, "trzy"]
57
JavaScript. Programowanie obiektowe
Można dodać nowe elementy, korzystając z nieistniejącego wcześniej indeksu. >>> a[3] = 'cztery';
"cztery" >>> a
[1, 2, "trzy", "cztery"] Jeśli dodając nowy element tablicy, poda się zbyt wysoki indeks — tak że pomiędzy nowym elementem a pozostałymi elementami tablicy zostanie przerwa — elementy z „pośrednimi” indeksami zostaną wypełnione wartością undefined. Na przykład: >>> a[6] = 'nowy';
"nowy" >>> a
[1, 2, 3, undefined, undefined, undefined, "nowy"]
Usuwanie elementów Do usuwania elementów służy operator delete. W praktyce nie usuwa on elementu, tylko ustawia jego wartość na udefined. W związku z tym po usunięciu elementu długość tablicy nie ulega zmianie. >>> var a = [1, 2, >>> delete a[1];
true >>> a
[1, undefined, 3]
Tablice tablic Tablica może zawierać dowolne wartości, w tym inne tablice. >>> var a = [1, "dwa", false, null, undefined]; >>> a
[1, "dwa", false, null, undefined] >>> a[5] = [1,2,3]
[1, 2, 3] >>> a
[1, "dwa", false, null, undefined, [1, 2, 3]]
58
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Przyjrzyjmy się teraz tablicy posiadającej dwa elementy, z których każdy sam jest tablicą: >>> var a = [[1,2,3],[4,5,6]]; >>> a
[[1, 2, 3], [4, 5, 6]] Pierwszym elementem jest a[0], które jest tablicą. >>> a[0]
[1, 2, 3] Aby odwołać się do elementów zagnieżdżonej tablicy, należy użyć następnego zestawu nawiasów kwadratowych. >>> a[0][0]
1 >>> a[1][2]
6 Warto wiedzieć, że za pomocą nawiasów kwadratowych można odwoływać się do poszczególnych znaków wewnątrz łańcucha. >>> var s = 'raz'; >>> s[0]
"r" >>> s[1]
"a" >>> s[2]
"z" Tablicami można bawić się na wiele innych sposobów (opowiem o nich w rozdziale 4.), jednak na teraz wystarczy zapamiętać, że: Q Tablica jest magazynem danych. Q Tablica zawiera indeksowane elementy. Q Indeksy zaczynają się od zera i zwiększają się o jeden dla każdego kolejnego
elementu. Q Do elementów tablicy odwołujemy się za pomocą indeksów otoczonych nawiasami
kwadratowymi. Q Tablica może zawierać dowolne dane, w tym inne tablice.
59
JavaScript. Programowanie obiektowe
Warunki i pętle Warunki są prostą, ale użyteczną metodą kontroli przepływu sterowania za pomocą fragmentu kodu. Pętle umożliwiają powtarzanie pewnych operacji bez konieczności powtarzania kodu. W tym podrozdziale zajmiemy się: Q warunkami if, Q instrukcjami switch, Q pętlami while, do…while, for oraz for…in.
Bloki kodu Podczas tworzenia warunków i pętli często będzie pojawiało sie pojęcie bloku kodu, dlatego od razu wyjaśnijmy sobie, co ono oznacza. Blok kodu to zero lub więcej wyrażeń otoczonych nawiasami klamrowymi. { }
var a = 1; var b = 3;
Bloki można zagnieżdżać wewnątrz innych bloków, praktycznie w nieskończoność: {
}
var var var { c { }
}
a = 1; b = 3; c, d; = a + b; d = a - b;
Dobre rady Używaj średników na końcu linii. Pomimo tego, że średnik nie jest obowiązkowy, jeśli linia zawiera
tylko jedno wyrażenie, warto wyrobić sobie nawyk ich stosowania. Dla zwiększenia czytelności kodu wyrażenia wewnątrz bloku powinny być umieszczone w osobnych liniach zakończonych średnikiem. Stosuj wcięcia wewnątrz nawiasów klamrowych. Niektórzy stosują wcięcia wielkości znaku tabulacji,
inni wielkości czterech spacji, a jeszcze inni dwóch spacji. Wielkość wcięcia nie ma znaczenia, pod warunkiem, że będziesz konsekwentny. W powyższym listingu stosuję wcięcia wielkości dwóch spacji. Zewnętrzny blok jest przesunięty o dwie spacje względem nawiasów, następny o cztery, a najbardziej wewnętrzny — o sześć.
60
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Korzystaj z nawiasów klamrowych. Jeśli blok składa się tylko z jednego wyrażenia, można je pomi-
nąć, ale dla celów czytelności oraz łatwości utrzymania kodu należy zawsze je stosować, nawet gdy nie jest to obowiązkowe.
Przejdźmy zatem do pętli i warunków! Uwaga: większość przykładów wymaga przejścia do trybu wielu linii w konsoli Firebug.
Warunki if Oto prosty przykład warunku if: var result = ''; if (a > 2) { result = 'a jest większe od 2'; }
Części wyrażenia warunkowego if to: Q instrukcja if („jeżeli”); Q warunek w nawiasie; Q blok kodu, który ma zostać wykonany, jeśli warunek jest spełniony.
Warunek (część w nawiasie) zawsze zwraca wartość logiczną (boolean). Może zawierać: Q operację logiczną: !, &&, ||; Q porównanie, np. ===, !=, >; Q każdą wartość lub zmienną, którą można zamienić na boolean; Q kombinację powyższych.
Wyrażenie warunkowe if może jeszcze zawierać opcjonalną część else. W części else umieszcza się blok kodu, który ma zostać wykonany, gdy warunek nie zostanie spełniony (będzie miał wartość false). if (a > 2) { result = 'a jest wieksze od 2'; } else { result = 'a NIE jest wieksze od 2'; }
Pomiędzy if a else może się znaleźć dowolnie wiele warunków if…else. Na przykład: if (a > 2 || a < -2) { result = 'a nie jest pomiędzy -2 i 2'; } else if (a === 0 && b === 0) { result = 'a i b mają wartość 0';
61
JavaScript. Programowanie obiektowe
} else if (a === b) { result = 'a i b są równe'; } else { result = 'Poddaję się!';
Warunki można zagnieżdżać, umieszczając nowe warunki wewnątrz bloków. if (a === 1) { if (b === 2) { result = 'a ma wartość 1, zaś b ma wartość 2'; } else { result = 'a ma wartość 1, ale b nie ma wartości 2'; } } else { result = 'a nie ma wartości 1, nie wiem nic na temat b'; }
Sprawdzanie, czy zmienna istnieje Często przydatna okazuje się możliwość sprawdzenia, czy dana zmienna istnieje. Leniwy sposób polega na umieszczeniu zmiennej jako warunku wyrażenia if, na przykład if(zmienna) {...}, jednak nie jest to najlepsza metoda. Spójrzmy na przykład, który sprawdza, czy zmienna o nazwie somevar istnieje, a jeśli tak, to ustawia wartość zmiennej result na 'tak': >>> var result = ''; >>> if (somevar){result = 'tak';}
somevar is not defined >>> result;
"" Kod najwyraźniej działa, ponieważ result nie ma wartości 'tak'. Jednak są problemy. Po pierwsze, wygenerowane zostało ostrzeżenie: somevar is not defined (zmienna somevar nie istnieje), a jako spece od JavaScriptu nie chcemy, by nasz kod powodował takie zachowania. Po drugie, sam fakt, że if(somevar) zwróciło false, wcale nie musi oznaczać, że zmienna nie została zdefiniowana. Może być tak, że somevar istnieje, ale zawiera jedną z fałszywych wartości, takich jak false albo 0. Lepiej sprawdzić istnienie zmiennej za pomocą typeof. >>> if (typeof somevar !== "undefined"){result = 'tak';} >>> result;
"" Operator typeof zawsze zwraca łańcuch, który można porównać z "undefined". Należy pamiętać, że ten sam wynik osiągniemy, jeśli zmienna somevar istnieje (została zadeklarowana), ale nie przypisano jej jeszcze wartości. Dlatego testowanie za pomocą typeof tak naprawdę służy sprawdzeniu, czy zmienna posiada wartość (inną niż "undefined").
62
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
>>> var somevar; >>> if (typeof somevar !== "undefined"){result = 'tak';} >>> result;
"" >>> somevar = undefined; >>> if (typeof somevar !== "undefined"){result = 'tak';} >>> result;
"" Jeśli zmienna została zdefiniowana i zainicjalizowana wartością inną niż undefined, typem zwracanym przez typeof nie będzie już "undefined ". >>> somevar = 123; >>> if (typeof somevar !== "undefined"){result = 'tak';} >>> result;
"tak"
Alternatywna składnia if Jeśli warunek jest bardzo prosty, można skorzystać z alternatywnej składni if. Warunek zawarty w poniższym fragmencie kodu: var a = 1; var result = ''; if (a === 1) { result = "a ma wartość jeden"; } else { result = "a nie ma wartości jeden"; }
można zapisać w skrócony sposób: var result = (a === 1) ? "a ma wartość jeden" : "a nie ma wartości jeden";
Tej składni powinno używać się jedynie w przypadku bardzo prostych warunków. Postaraj się jej nie nadużywać, ponieważ zmniejsza ona czytelność kodu. Znak ? nazywamy operatorem trójkowym.
Switch Jeśli podczas pisania warunków zdasz sobie sprawę, że Twój kod zawiera zbyt wiele części else…if, należy rozważyć zamianę warunku if na switch. var a = '1'; var result = ''; switch (a) {
63
JavaScript. Programowanie obiektowe
case 1: result = 'Liczba 1'; break; case '1': result = 'Łańcuch 1'; break; default: result = 'Nie wiem'; break; } result
Zmiennej result zostanie przypisana wartość 'Łańcuch 1'. Wyrażenie warunkowe switch posiada następujące części: Q Instrukcję switch. Q Wyrażenie w nawiasie. Najczęściej znajduje się tam zmienna, ale może to być
wszystko, co zwraca jakąś wartość. Q Bloki case otoczone nawiasami klamrowymi. Q Po każdej instrukcji case następuje pewne wyrażenie. Wartość tego wyrażenia jest porównywana z wartością wyrażenia podanego zaraz po instrukcji switch. Jeśli w wyniku porównania zwrócona zostanie wartość true, uruchomiony zostanie
kod po dwukropku. Q Blok case powinien zostać zakończony instrukcją break. Nie jest to obowiązkowe, ale jeśli nie wstawimy break, po wykonaniu kodu związanego z daną wartością case
wykonany zostanie blok następny w kolejności, co z reguły nie jest pożądanym zachowaniem. Q Część default wyrażenia warunkowego nie jest obowiązkowa. Kod po dwukropku zostanie wykonany, jeśli wartości po switch nie uda się dopasować do żadnej wartości w blokach case. Oto procedura wykonania warunku switch, krok po kroku: 1. Oblicz i zapamiętaj wartość wyrażenia w nawiasie po instrukcji switch. 2. Przejdź do pierwszego bloku case, porównaj jego wartość z wartością z kroku 1. 3. Jeśli wynik porównania z kroku drugiego zwraca true, wykonaj kod aktualnego bloku case. 4. Po wykonaniu bloku case sprawdź, czy blok kończy się instrukcją break. Jeśli tak, wyjdź z wyrażenia warunkowego. 5. Jeśli nie pojawiło się słowo break lub w kroku drugim zwrócona została wartość false, przejdź do następnego bloku case. Powtórz kroki od 2. do 5. 6. Jeśli dotarłeś tutaj (wykonanie procedury nie zakończyło się na kroku 4.), wykonaj kod w części default.
64
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Dobre rady Stosuj wcięcia w liniach zawierających case i głębsze wcięcia w liniach z kodem opisującym bloki. Nie zapominaj o stosowaniu break. Niekiedy chce się świadomie ominąć break, jednak takie sytuacje zdarzają się bardzo rzadko. Okre-
śla się je mianem spadków. Zawsze należy je dokumentować, ponieważ na pierwszy rzut oka mogą wyglądać jak przypadkowe pominięcia słówka break. Z drugiej strony, może się zdarzyć, że programista chce ominąć cały blok kodu po case, tak by dwa różne wyrażenia case dzieliły ten sam kod. Jest to możliwe, ale nie zmienia to zasady, która mówi, że kod następujący po wyrażeniu case powinien kończyć się instrukcją break. Jeśli chodzi o formatowanie kodu, nie ma znaczenia, czy wyrównasz break do poziomu case, czy do poziomu kodu wewnątrz bloku: najważniejsze, abyś postępował konsekwentnie.
Korzystaj z default. Dzięki temu możesz mieć pewność, że po wykonaniu bloku switch otrzymasz
istotny wynik, nawet jeśli żadne z wyrażeń case nie zostało dopasowane do wartości po switch.
Pętle Instrukcje warunkowe if…else oraz switch pozwalają na to, by Twój kod, gdy znajdzie się na skrzyżowaniu, mógł wybierać różne ścieżki w zależności od pewnego warunku. Pętlę, dla odmiany, można porównać do ronda, wokół którego będzie kręcił się kod, zanim wróci na główną drogę. Ile razy okrąży rondo? Zależy to od wyniku sprawdzania pewnego warunku, które ma miejsce przed (lub po) każdą iteracją. Powiedzmy, że podróżujesz (a właściwie Twój program podróżuje) z punktu A do punktu B. W pewnym momencie osiągasz punkt, w którym następuje sprawdzenie warunku C. Od wyniku tego sprawdzenia zależy, czy program wejdzie w pętlę L. Po wejściu w pętlę wykonujesz jedną iterację. Następnie ponownie sprawdzasz warunek, by dowiedzieć się, czy potrzebna jest kolejna iteracja. Wreszcie możesz udać się do punktu B. Istnieją pętle nieskończone, w których warunek zawsze jest spełniony, a kod pozostaje w pętli „na zawsze”. Taka sytuacja prawie zawsze jest wynikiem błędu logicznego. W języku JavaScript istnieją cztery rodzaje pętli: Q pętla while, Q pętla do…while, Q pętla for, Q pętla for…in.
65
JavaScript. Programowanie obiektowe
Pętla while
Pętla while jest najprostszym typem pętli. Wygląda tak: var i = 0; while (i < 10) { i++; }
Po instrukcji while następuje para nawiasów z warunkiem oraz blok kodu w nawiasach klamrowych. Dopóki warunek będzie miał wartość true, blok kodu będzie wykonywany wciąż od nowa.
Pętla do…while Pętle do…while jest bardzo podobna do pętli while. Przykład: var i = 0; do { i++; } while (i < 10)
W przypadku tej pętli najpierw pojawia się instrukcja do, po której następuje blok kodu, a dopiero po nim pojawia się warunek. Oznacza to, że niezależnie od prawdziwości warunku blok kodu zostanie wykonany przynajmniej raz. Jeśli w poprzednich dwóch przykładach zmienna i otrzyma wartość 11 zamiast 0, blok kodu w pierwszym przykładzie (z pętlą while) nie zostanie wykonany, a i nadal będzie miało wartość 11, natomiast w drugim przykładzie (pętla do…while) blok zostanie wykonany jeden raz, a i otrzyma wartość 12.
Pętla for for jest najczęściej stosowanym rodzajem pętli, więc warto się do niej przyzwyczaić. Składnia tej pętli jest nieco bardziej złożona.
66
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Poza warunkiem C i blokiem kodu L mamy tu następujące elementy: Q Inicjalizacja — kod, który jest wykonywany przed wejściem programu w pętlę
(na diagramie oznaczona jako 0). Q Inkrementacja — kod, który jest wykonywany po każdej iteracji (na diagramie
oznaczona jako ++). Pętlę for najczęściej stosuje się w następujący sposób: Q W części inicjalizacyjnej definiuje się zmienną, najczęściej o nazwie i, na przykład var i = 0;. Q W części warunkowej porównuje się i z pewną wartością graniczną, na przykład i < 100. Q W części inkrementacyjnej zwiększa się i o 1, na przykład i++.
Pełen przykład: var kara = ''; for (var i = 0; i < 100; i++) { kara += 'Nigdy więcej tego nie zrobię, '; }
Wszystkie trzy części (inicjalizacja, warunek, inkrementacja) mogą zawierać wiele wyrażeń, rozdzielonych przecinkami. Przykład można napisać nieco inaczej, umieszczając definicję zmiennej kara w części inicjalizacyjnej pętli: for (var i = 0, kara = ''; i < 100; i++) { kara += 'Nigdy więcej tego nie zrobię, '; }
Czy do części inkrementacyjnej można przenieść całe ciało pętli? Tak, zwłaszcza jeśli zawiera ono tylko jedną linię. Otrzymamy wtedy dziwną pętlę pozbawioną ciała: for (var i = 0, kara = ''; i < 100; i++, kara += 'Nigdy więcej tego nie zrobię, ') { // puste ciało pętli }
Zasadniczo wszystkie trzy części są opcjonalne. Ten sam przykład można jeszcze zapisać tak: var i = 0, kara = ''; for (;;) { kara += 'Nigdy więcej tego nie zrobię, '; if (++i == 100) { break; } }
67
JavaScript. Programowanie obiektowe
Chociaż kod po ostatnich zmianach działa dokładnie tak samo jak jego pierwotna wersja, jest dłuższy i trudniejszy do zrozumienia. Ten sam wynik można jeszcze osiągnąć za pomocą pętli while. Pętle for mają jednak tę zaletę, że kod pisany przy ich użyciu jest lepszej jakości, ponieważ sama składnia pętli wymusza logiczny podział na trzy części (inicjalizacja, warunek, inkrementacja), przez co kod jest lepiej przemyślany i trudniej utknąć w sytuacji z nieskończoną pętlą. Pętle for można zagnieżdżać. Poniżej przedstawiam przykład pętli, która, zagnieżdżona w innej pętli, tworzy łańcuch znaków składający się z 10 wierszy i 10 kolumn gwiazdek. Zmienna i reprezentuje wiersz, zmienna j kolumnę w wynikowej macierzy. var res = '\n'; for(var i = 0; i < 10; i++) { for(var j = 0; j < 10; j++) { res += '* '; } res+= '\n'; }
Wynikiem jest następujący łańcuch znaków: " ********** ********** ********** ********** ********** ********** ********** ********** ********** ********** "
68
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Oto inny przykład, w którym wykorzystałem zagnieżdżone pętle oraz operację modulo w celu narysowania płatka śniegu: var res = '\n', i, j; for(i = 1; i <= 7; i++) { for(j = 1; j <= 15; j++) { res += (i * j) % 8 ? ' ' : '*'; } res+= '\n'; }
Wynik: " * * * * * ******* * * * * * "
Pętla for…in Pętli for…in używa się w celu iteracji po elementach tablicy (lub obiektu, co zobaczymy trochę później). Jest to jedyne zastosowanie tej pętli — nie można stosować jej w ogólnych przypadkach, tak jak można to robić z pętlami for czy while. Poniżej znajduje się przykład zastosowania tej pętli w celu przejścia przez wszystkie elementy tablicy. Chciałbym jednak podkreślić, że pokazuję go jedynie w celach informacyjnych, jako że for…in najlepiej nadaje się do pracy z obiektami, a do obsługi tablic wystarczy zwykła pętla for. Przykład przedstawia iterację po elementach tablicy i wypisanie indeksu oraz wartości każdego z elementów. var a = [.'a', 'b', 'c', 'x', 'y', 'z']; var result = '\n'; for (var i in a) { wynik += 'indeks: ' + i + ', wartość: ' + a[i] + '\n'; }
69
JavaScript. Programowanie obiektowe
Wynik: " indeks: 0, wartość: a indeks: 1, wartość: b indeks: 2, wartość: c indeks: 3, wartość: x indeks: 4, wartość: y indeks: 5, wartość: z "
Komentarze Ostatnia rzecz w tym rozdziale: komentarze. Wewnątrz kodu w języku JavaScript można umieszczać komentarze, które są ignorowane podczas wykonania i nie mają żadnego wpływu na działanie programu. Są jednak niezastąpione, kiedy zaglądasz do kodu po kilku miesiącach od jego napisania lub jeśli Twój kod ma być utrzymywany przez kogoś innego. Dozwolone są dwa typy komentarzy: Q Komentarze jednolinijkowe — zaczynają się sekwencją znaków //, a kończą wraz z końcem linii. Q Komentarze wielolinijkowe — zaczynają się od /*, a kończą */. Sekwencja */ może znajdować się w tej samej linii lub wiele linii dalej. Ignorowane jest wszystko, co znajdzie się pomiędzy tymi znakami. Przykłady: // początek linii var a = 1; // inne miejsce w linii /* komentarz wielolinijkowy umieszczony w jednej linii */ /* komentarz zajmujący kilka linii */
Istnieją narzędzia, takie jak JSDoc, które potrafią analizować kod i pobierać istotne informacje z komentarzy.
70
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Podsumowanie W tym rozdziale opowiedziałem, z jakich elementów buduje się programy w języku JavaScript. Znasz już proste typy danych: Q liczba, Q łańcuch znaków (string), Q boolean, Q undefined, Q null.
Poznaliśmy także sporo operatorów: Q operatory arytmetyczne: +, -, *, / i %; Q operatory inkrementacji i dekrementacji: ++ i --; Q operatory przypisania: =, +=, -=, *=, /= i %=; Q specjalne operatory: typeof i delete; Q operatory logiczne: &&, || i !; Q operatory porównania: ==, ===, !=, !==, <, >, >= i <=.
Następnie pokazałem Ci, jak korzystać z tablic w celu przechowywania danych oraz jak kontrolować przepływ sterowania programu za pomocą warunków (if…else lub switch) i pętli (while, do…while, for, for…in). To dosyć dużo informacji, dlatego zachęcam Cię do wykonania ćwiczeń przedstawionych poniżej i postawienia sobie zasłużonej piątki w dzienniku przed przejściem do następnego rozdziału. Będzie jeszcze ciekawiej!
Ćwiczenia 1. Jaki będzie wynik wykonania każdej z poniższych linii w konsoli? Dlaczego? Q
var a; typeof a;
Q
var s = '1s'; s++;
Q
!!"false"
Q
!!undefined
Q
typeof -Infinity
Q
10 % "0"
Q
undefined == null
71
JavaScript. Programowanie obiektowe
Q
false === ""
Q
typeof "2E+2"
Q
a = 3e+3; a++;
2. Jaką wartość będzie miała zmienna v po wykonaniu następującej operacji? >>> var v = v || 10;
Poeksperymentuj, nadając wcześniej v wartości 100, 0, null, a także kasując jej wartość (delete v). 3. Napisz skrypt, który wypisuje tabliczkę mnożenia. Wskazówka: użyj pętli zagnieżdżonej w innej pętli.
72
3 Funkcje Opanowanie funkcji ma kluczowe znaczenie podczas nauki każdego języka programowania, a w przypadku JavaScriptu jest jeszcze ważniejsze niż zwykle. Jest tak dlatego, że w tym języku funkcje mają bardzo wiele zastosowań i w dużej mierze to dzięki nim JavaScript jest tak elastyczny i ekspresywny. W miejscach, gdzie w innych językach programowania trzeba by było stosować specjalną składnię w celu wykorzystania obiektowości, JavaScript udostępnia funkcje. Ten rozdział omawia: Q definiowanie funkcji i korzystanie z nich, Q przekazywanie funkcjom parametrów, Q funkcje predefiniowane dostępne za darmo, Q zasięg zmiennych, Q podejście, zgodnie z którym funkcje to tylko dane specjalnego typu.
Zrozumienie powyższych tematów da nam solidne oparcie przed przejściem do kolejnej części rozdziału, w której przedstawione zostaną pewne ciekawe zastosowania funkcji: Q funkcje anonimowe; Q wywołania zwrotne; Q samowywołujące się funkcje; Q funkcje wewnętrzne (zdefiniowane wewnątrz innych funkcji); Q funkcje, które zwracają inne funkcje; Q funkcje, które zmieniają swoją definicję; Q domknięcia.
JavaScript. Programowanie obiektowe
Czym jest funkcja? Funkcje pozwalają zgrupować pewną ilość kodu, nadać jej nazwę, a następnie ponownie wykorzystać przy użyciu tej właśnie nazwy. Spójrzmy na przykład: function sum(a, b) { var c = a + b; return c; }
Z jakich części składa się funkcja? Q Słowo kluczowe function. Q Nazwa funkcji, w przykładzie jest to sum. Q Oczekiwane parametry (argumenty), w tym wypadku a i b. Funkcja może mieć ich
zero lub więcej. Jeśli jest ich więcej niż jeden, parametry rozdziela się przecinkami. Q Blok kodu, nazywany ciałem funkcji. Q Instrukcja return, która umożliwia zwrócenie obliczonej wartości funkcji. Funkcja
zawsze zwraca wartość. Jeśli nie robi tego w sposób jawny, niejawnie zwraca wartość undefined. Zwróć uwagę, że funkcja może zwrócić tylko jedną wartość. Jeśli potrzebne jest zwrócenie większej liczby wartości, należy umieścić je w tablicy i zwrócić tablicę jako wartość funkcji.
Wywoływanie funkcji Aby skorzystać z funkcji, należy ją wywołać. Funkcję wywołuje się poprzez podanie jej nazwy i argumentów umieszczonych w nawiasie. Wywołajmy zatem funkcję sum(), przekazując jej dwa argumenty i przypisując zwracaną przez nią wartość zmiennej result. >>> var result = sum(1, 2); >>> result;
3
Parametry Podczas definiowania funkcji można określić oczekiwane parametry. Funkcja nie musi pobierać parametrów, ale jeśli oczekuje, że je otrzyma, a programista podczas wywoływania funkcji zapomni o ich podaniu, JavaScript przypisze im wartość undefined. W poniższym przykładzie funkcja zwraca wartość NaN, ponieważ próbuje dodać 1 do undefined:
74
Rozdział 3. • Funkcje
>>> sum(1)
NaN JavaScript nie wybrzydza podczas pobierania parametrów. Jeśli otrzyma ich więcej, niż jest potrzebne, dodatkowe parametry zostaną zignorowane: >>> sum(1, 2, 3, 4, 5)
3 Na dodatek możliwe jest pisanie funkcji, które mogą przyjmować różną liczbę parametrów. Jest to możliwe dzięki tablicy arguments, która jest automatycznie tworzona wewnątrz każdej funkcji. Oto funkcja, której działanie polega na zwracaniu wszystkich przekazanych jej argumentów: >>> function args() { return arguments; } >>> args();
[] >>> args( 1, 2, 3, 4, true, 'ninja');
[1, 2, 3, 4, true, "ninja"] Tablica arguments pozwoli nam poprawić funkcję sum() tak, by przyjmowała ona dowolną liczbę parametrów i dodawała je wszystkie. function sumaNaSterydach() { var i, res = 0; var liczba_parametrow = arguments.length; for (i = 0; i < liczba_parametrow; i++) { res += arguments[i]; } return res; }
Jeśli podczas testowania wywołasz tę funkcję z inną niż wcześniej liczbą parametrów (lub nawet bez parametrów), zobaczysz, że działa tak, jak powinna: >>> sumaNaSterydach(1, 1, 1);
3 >>> sumaNaSterydach(1, 2, 3, 4);
10 >>> sumaNaSterydach(1, 2, 3, 4, 4, 3, 2, 1);
20 >>> sumaNaSterydach(5);
5
75
JavaScript. Programowanie obiektowe
>>> sumaNaSterydach();
0 Wyrażenie arguments.length zwraca liczbę parametrów podanych podczas wywołania funkcji. Jeśli nie rozumiesz jego składni, nie przejmuj się, wrócimy do tego w następnym rozdziale. Wtedy także dowiesz się, że arguments w rzeczywistości nie jest tablicą, ale obiektem tablicopodobnym.
Funkcje predefiniowane Istnieje pewna liczba funkcji, które zostały wbudowane w silnik JavaScriptu i z których można korzystać do woli. Przyjrzyjmy się im. Warto poeksperymentować z tymi funkcjami i przyjrzeć się ich argumentom i wartościom zwracanym, by móc później korzystać z nich w wygodny sposób. Oto lista funkcji wbudowanych: Q parseInt() Q parseFloat() Q isNaN() Q isFinite() Q encodeURI() Q decodeURI() Q encodeURIComponent() Q decodeURIComponent() Q eval() Zasada czarnej skrzynki Z reguły podczas korzystania z funkcji Twój program nie musi wiedzieć, jakie czynności są wykonywane wewnątrz danej funkcji. Możesz myśleć o funkcjach jako o czarnych skrzynkach — podajesz im pewne wartości (w postaci parametrów wejściowych) i odbierasz od nich zwracane wyniki. Jest to prawdziwe dla wszystkich funkcji — tych wbudowanych w język JavaScript, tych pisanych przez Ciebie oraz tych stworzonych przez Twoich współpracowników lub nieznanych Ci programistów.
parseInt() parseInt() pobiera argument dowolnego typu (najczęściej łańcuch znaków) i próbuje zamienić go na liczbę całkowitą. Jeśli operacja się nie powiedzie, zwrócona zostanie wartość NaN. >>> parseInt('123')
123 76
Rozdział 3. • Funkcje
>>> parseInt('abc123')
NaN >>> parseInt('1abc23')
1 >>> parseInt('123abc')
123 Funkcja pobiera jeszcze opcjonalny drugi argument, który określa podstawę, opisującą typ liczby: dziesiętny, szesnastkowy, binarny itp. Przykładowo: nie ma sensu próba zamiany pobrania liczby dziesiętnej z łańcucha "FF", zatem wynikiem będzie NaN, jednak jeśli potraktujemy "FF" jako liczbę szesnastkową, otrzymamy wynik 255. >>> parseInt('FF', 10)
NaN >>> parseInt('FF', 16)
255 Spróbujmy teraz sparsować liczby o różnych podstawach: 10 (liczba dziesiętna) i 8 (liczba ósemkowa). >>> parseInt('0377', 10)
377 >>> parseInt('0377', 8)
255 Jeśli drugi argument nie zostanie podany, za podstawę uznawana jest liczba 10, z następującymi wyjątkami: Q Jeśli jako pierwszy argument przekazany zostanie łańcuch zaczynający się od 0x, drugiemu argumentowi (jeśli nie został podany) przypisana zostanie wartość 16 (liczba zostanie uznana za szesnastkową). Q Jeśli pierwszy parametr zaczyna się od 0, drugi otrzyma wartość 8. >>> parseInt('377')
377 >>> parseInt('0377')
255 >>> parseInt('0x377')
887
77
JavaScript. Programowanie obiektowe
Najbezpieczniejszym rozwiązaniem jest określanie podstawy za każdym razem. Jeśli tego nie zrobisz, kod prawdopodobnie zadziała w 99% przypadków (ponieważ najczęściej parsuje się liczby dziesiętne), jednak jeśli trafisz na liczbę zapisaną w innym systemie, możesz osiwieć, zanim uda Ci się znaleźć przyczynę błędu. Wyobraź sobie na przykład, że parsujesz pola formularza, który reprezentuje kalendarz, i że użytkownik wpisał 08, mając na myśli sierpień. Jeśli nie podasz podstawy, otrzymasz wynik inny niż oczekiwany.
parseFloat() parseFloat() działa podobnie do parseInt(), ale oczekuje ułamków. Pobiera ona tylko jeden
parametr. >>> parseFloat('123')
123 >>> parseFloat('1.23')
1.23 >>> parseFloat('1.23abc.00')
1.23 >>> parseFloat('a.bc1.23')
NaN Podobnie jak parseInt(), parseFloat() podda się po napotkaniu pierwszego znaku, z którym nie będzie umiała sobie poradzić, nawet jeśli pozostała część tekstu zawiera poprawne liczby. >>> parseFloat('a123.34')
NaN >>> parseFloat('a123.34')
NaN >>> parseFloat('12a3.34')
12 parseFloat(), w przeciwieństwie do parseInt(), jest w stanie poprawnie zinterpretować zapis
wykładniczy. >>> parseFloat('123e-2')
1.23 >>> parseFloat('123e2')
12300
78
Rozdział 3. • Funkcje
>>> parseInt('1e10')
1
isNaN() Przy pomocy isNaN() można sprawdzić, czy wartość wejściowa jest liczbą, której można bezpiecznie używać w operacjach arytmetycznych. isNaN() pozwala w wygodny sposób dowiedzieć się, czy funkcjom parseInt() i parseFloat() udało się sparsować liczbę. >>> isNaN(NaN)
true >>> isNaN(123)
false >>> isNaN(1.23)
false >>> isNaN(parseInt('abc123'))
true Ta funkcja także stara się zamienić parametr wejściowy na liczbę: >>> isNaN('1.23')
false >>> isNaN('a1.23')
true Funkcja isNaN() jest potrzebna także dlatego, że liczba NaN nie jest równa samej sobie. Wynikiem porównania NaN === NaN będzie false!
isFinite() Funkcja isFinite() sprawdza, czy wartość parametru wejściowego to liczba różna od Infinity i różna od NaN. >>> isFinite(Infinity)
false >>> isFinite(-Infinity)
false
79
JavaScript. Programowanie obiektowe
>>> isFinite(12)
true >>> isFinite(1e308)
true >>> isFinite(1e309)
false Jeśli dziwią Cię dwa ostatnie wyniki, przypominam, że zgodnie z tym, co napisałem w poprzednim rozdziale, największą dopuszczalną liczbą w języku JavaScript jest 1.7976931348623157e+308.
Encode/Decode URIs W adresach URL (Uniform Resource Locator) i URI (Uniform Resource Identifier) niektóre znaki mają specjalne znaczenie. Jeśli chcemy mieć pewność, że zostaną one zapisane poprawnie (czyli jeśli chcemy zastosować sekwencję uniku), możemy skorzystać z funkcji encodeURI() lub encodeURIComponent(). Pierwsza z nich zwróci poprawny adres URL, druga założy, że przekazany jej parametr jest tylko częścią URL (na przykład zawiera parametry żądania), i odpowiednio zakoduje wszystkie nietypowe znaki. >>> var url = 'http://www.packtpub.com/scr ipt.php?q=this and that'; >>> encodeURI(url);
"http://www.packtpub.com/scr%20ipt.php?q=this%20and%20that" >>> encodeURIComponent(url);
"http%3A%2F%2Fwww.packtpub.com%2Fscr%20ipt.php%3Fq%3Dthis% 20and%20that" Działanie przeciwne do encodeURI() i encodeURIComponent() mają funkcje decodeURI() i decode ´URIComponent(). W starszym kodzie można natknąć się na starsze funkcje escape() i unescape(), jednak są one przestarzałe i nie należy ich stosować.
eval() Funkcja eval() pobiera łańcuch znaków i uruchamia go jako kod w języku JavaScript: >>> eval('var ii = 2;') >>> ii
2 eval('var ii = 2;') działa dokładnie tak samo jako var ii = 2;
80
Rozdział 3. • Funkcje
Są sytuacje, w których eval() się przydaje, jednak w miarę możliwości należy tej funkcji unikać. Z reguły można zastosować inne rozwiązania, które w większości przypadków są bardziej eleganckie i łatwiejsze w utrzymaniu. Weterani JavaScriptu jak mantrę powtarzają zdanie „eval is evil” („eval to samo zło”). Można wymienić następujące wady tej funkcji: Q Wydajność: wykonywanie kodu „na żywo” jest wolniejsze od wykonywania kodu
zapisanego w skrypcie. Q Bezpieczeństwo: JavaScript ma duże możliwości, co oznacza, że przy jego „pomocy” można coś zepsuć. Jeśli nie możesz ufać źródłu, z którego pochodzi wejście przekazywane do eval(), nie wywołuj tej funkcji.
Bonus — funkcja alert() Spójrzmy jeszcze na bardzo popularną funkcję alert(). Nie należy ona do rdzenia języka (nie ma jej w specyfikacji ECMA), ale można z niej korzystać w środowisku przeglądarki. Pozwala ona na wyświetlanie komunikatów w okienku dialogowym. Czasami przydaje się to podczas testowania i debugowania aplikacji, chociaż w tym celu lepiej korzystać z debugera Firebug. Na poniższym rysunku widać efekt wykonania kodu alert("halo!").
Pamiętaj tylko, że okienko dialogowe blokuje wątek przeglądarki, co oznacza, że żaden inny kod nie zostanie wykonany, zanim użytkownik nie kliknie OK. Jeśli aplikacja jest często aktualizowaną aplikacją AJAX, to alert() nie jest najlepszym pomysłem.
Zasięg zmiennych Warto zwrócić uwagę, zwłaszcza, jeśli jest się osobą, która wcześniej programowała w innym języku, że zmienne w języku JavaScript nie są definiowane w obrębie bloku, tylko funkcji. Oznacza to, że jeśli zmienna została zdefiniowana wewnątrz funkcji, nie jest widoczna poza nią. Natomiast zmienna zdefiniowana wewnątrz bloku if lub for jest widoczna poza blokiem. Zmienne globalne to zmienne używane poza funkcjami, natomiast zmienne lokalne to zmienne używane wewnątrz funkcji. Kod wewnątrz funkcji ma dostęp zarówno do zmiennych globalnych, jak i do swoich zmiennych lokalnych.
81
JavaScript. Programowanie obiektowe
W poniższym przykładzie: Q funkcja f() ma dostęp do zmiennej global, Q poza funkcją f() zmienna local nie istnieje. var global = 1; function f() { var local = 2; global++; return global; } >>> f();
2 >>> f();
3 >>> local
local is not defined Ponadto należy mieć na uwadze, że jeśli do deklaracji zmiennej nie zostanie użyta instrukcja var, zmienna będzie miała zasięg globalny. Spójrzmy na przykład:
Co się stało? Funkcja f() zawiera zmienną local. Przed wywołaniem funkcji zmienna nie istnieje. Jednak podczas pierwszego wywołania funkcji zmienna jest tworzona i ma zasięg globalny. Dlatego jeśli wówczas spróbujemy sięgnąć do zmiennej local, okaże się ona dostępna. Dobre rady Staraj się ograniczać liczbę zmiennych globalnych. Wyobraź sobie dwie osoby pracujące nad dwiema
różnymi funkcjami w tym samym skrypcie, które przypadkowo postanawiają nadać tę samą nazwę zmiennej globalnej. Może to doprowadzić do nieoczekiwanych wyników i trudnych do wykrycia błędów. Zawsze deklaruj zmienne za pomocą instrukcji var.
82
Rozdział 3. • Funkcje
Poniższy przykład ilustruje ważny aspekt podziału na zmienne lokalne i globalne. var a = 123; function f() { alert(a); var a = 1; alert(a); } f();
Być może spodziewasz się, że pierwszy alert() wyświetli 123 (wartość globalnej zmiennej a), a drugi wyświetli 1 (wartość lokalnej zmiennej a). Jednak stanie się inaczej. Pierwszy alert() pokaże "undefined". Stanie się tak dlatego, że wewnątrz funkcji zasięg lokalny jest ważniejszy od globalnego. Zmienna lokalna nadpisuje zmienną globalną o tej samej nazwie. Podczas wykonywania pierwszego alert(), a nie było jeszcze zdefiniowane (stąd wartość undefined), ale już istniało w lokalnej przestrzeni nazw.
Funkcje są danymi Zrozumienie tego punktu widzenia będzie na późniejszym etapie bardzo ważne — funkcje tak naprawdę są danymi. Oznacza to, że następujące dwie metody definiowania funkcji są równoważne: function f(){return 1;} var f = function(){return 1;}
Drugi z pokazanych sposobów definiowania funkcji określa się mianem zapisu literałowego funkcji. Jeśli na zmiennej, której została przypisana wartość będąca funkcją, wywołamy operator typeof, zwróci on łańcuch znaków "function". >>> function f(){return 1;} >>> typeof f
"function" Zatem: funkcje w języku JavaScript są specjalnym typem danych. Posiadają dwie istotne cechy: Q zawierają kod, Q są wykonywalne (mogą być wywoływane). Wiesz już, że funkcje wywołuje się poprzez podanie nawiasu po ich nazwie. Następny przykład pokazuje, że ta metoda zadziała niezależnie od sposobu definicji funkcji. Widać w nim także, że funkcja jest traktowana jak normalna wartość, którą można przypisać nowej zmiennej lub nawet wykasować. >>> var sum = function(a, b) {return a + b;} >>> var add = sum; >>> delete sum
true 83
JavaScript. Programowanie obiektowe
>>> typeof sum;
"undefined" >>> typeof add;
"function" >>> add(1, 2);
3 Ponieważ funkcje to dane przypisane do zmiennych, stosujemy tę samą konwencję nazw co przy nazywaniu zmiennych — nazwa funkcji nie może zaczynać się liczbą i może zawierać dowolną kombinację liter, cyfr oraz znaku podkreślnika.
Funkcje anonimowe JavaScript pozwala na rozrzucanie fragmentów danych po całym programie. Wyobraź sobie, że Twój program zawiera następujący fragment kodu: >>> "test"; [1,2,3]; undefined; null; 1;
Kod wygląda dość dziwnie, ponieważ nie robi nic pożytecznego, jednak jest poprawny i nie spowoduje błędu. Można powiedzieć, że zawiera dane anonimowe, czyli nieprzypisane do żadnej zmiennej i nieposiadające nazwy. Wiesz już, że funkcje można traktować jak wszystkie inne dane. W związku z tym ich także można używać bez podania nazwy: >>> function(a){return a;}
Anonimowe fragmenty danych w kodzie nie mogą być zbyt przydatne, chyba że są funkcjami. W takim wypadku istnieją dwa bardzo eleganckie zastosowania tych danych: Q Funkcję anonimową można przekazać jako parametr do innej funkcji. Funkcja odbierająca ten parametr może przeprowadzić operacje na otrzymanej funkcji. Q Funkcje anonimowe możne definiować i od razu uruchamiać.
Przyjrzyjmy się uważniej obu zastosowaniom funkcji anonimowych.
Wywołania zwrotne Skoro funkcje to dane, które można przypisać zmiennym, to można je definiować, kasować, kopiować… Dlaczego zatem nie miałoby być możliwe przekazywanie ich jako parametrów do innych funkcji?
84
Rozdział 3. • Funkcje
Oto przykład funkcji, która pobiera dwie funkcje jako parametry, wywołuje je, po czym zwraca wynik będący sumą zwróconych przez nie wartości: function wywolaj_i_dodaj(a, b){ return a() + b(); }
Zdefiniujmy teraz dwie pomocnicze funkcje, które będą zwracały ustalone wartości: function return } function return }
jeden() { 1; dwa() { 2;
Możemy przekazać je oryginalnej funkcji i obejrzeć wynik: >>> wywolaj_i_dodaj(jeden, dwa);
3 Jako parametry można także przekazywać funkcje anonimowe. Wówczas zamiast definiowania jeden() i dwa() wystarczyłoby napisać: wywolaj_i_dodaj(function(){return 1;}, function(){return 2;})
Jeśli funkcja A zostaje przekazana funkcji B i B wywołuje A, często mówi się, że A jest wywołaniem zwrotnym (ang. callback function). Jeśli A nie ma nazwy, to jest anonimowym wywołaniem zwrotnym. Jakie zastosowania mają takie funkcje? Spójrzmy na przykłady, które ilustrują następujące zalety wywołań zwrotnych: Q Można przekazywać funkcje bez konieczności ich nazywania, co oznacza, że potrzebnych jest mniej zmiennych globalnych. Q Jeśli przeniesiemy obowiązek wywołania funkcji na inną funkcję, nasz kod będzie
krótszy. Q Wywołania zwrotne mogą korzystnie wpłynąć na wydajność aplikacji.
Przykłady wywołań zwrotnych Przeanalizujmy częsty scenariusz: mamy funkcję, która zwraca wartość, przekazywaną następnie kolejnej funkcji. W naszym przykładzie pierwsza funkcja, pomnozRazyDwa(), przyjmuje trzy parametry, przechodzi przez nie w pętli oraz zwraca tablicę zawierającą wynik. Druga funkcja, dodajJeden(), pobiera wartość, dodaje do niej jeden, po czym zwraca wynik.
85
JavaScript. Programowanie obiektowe
function pomnozRazyDwa(a, b, c) { var i, ar = []; for(i = 0; i < 3; i++) { ar[i] = arguments[i] * 2; } return ar; } function dodajJeden(a) { return a + 1; }
Przetestujmy te funkcje: >>> pomnozRazyDwa(1, 2, 3);
[2, 4, 6] >>> dodajJeden(100)
101 Załóżmy teraz, że chcemy, by tablica myarr zawierała trzy elementy, z których każdy przejdzie przez obie funkcje. Zacznijmy od pomnozRazyDwa(). >>> var myarr = []; >>> myarr = pomnozRazyDwa(10, 20, 30);
[20, 40, 60] Możemy teraz wywoływać funkcję dodajJeden() w pętli, raz dla każdego elementu tablicy: >>> for (var i = 0; i < 3; i++) {myarr[i] = addOne(myarr[i]);} >>> myarr
[21, 41, 61] Wszystko zadziała, ale jest tu pole do poprawek. Po pierwsze, przykład uruchamia dwie pętle, które mogą być kosztowne, jeśli powtórzeń jest wiele. Żądany wynik można otrzymać przy użyciu jednej tylko pętli. Oto, jak zmienić funkcję pomnozRazyDwa() tak, by jako parametr przyjmowała funkcję i wywoływała ją przy każdej iteracji: function pomnozRazyDwa(a, b, c, callback) { var i, ar = []; for(i = 0; i < 3; i++) { ar[i] = callback(arguments[i] * 2); } return ar; }
Zmieniona wersja funkcji pozwala na wykonanie tej samej pracy przy pomocy jednego wywołania. Przekazuje się do niego wartości początkowe oraz funkcję, która ma zostać wywołana na każdej z tych wartości.
86
Rozdział 3. • Funkcje
>>> myarr = pomnozRazyDwa(1, 2, 3, dodajJeden);
[3, 5, 7] Zamiast definiowania funkcji dodajJeden() można skorzystać z funkcji anonimowej, dzięki czemu zdefiniowana zostanie jedna zmienna globalna mniej. >>> myarr = pomnozRazyDwa(1, 2, 3, function(a){return a + 1});
[3, 5, 7] Oczywiście tej samej funkcji można jako parametr przekazać różne funkcje anonimowe: >>> myarr = multiplyByTwo(1, 2, 3, function(a){return a + 2});
[4, 6, 8]
Funkcje samowywołujące się Omówiliśmy już funkcje anonimowe i wywołania zwrotne. Przejdźmy teraz do innego zastosowania funkcji anonimowych — wywoływania funkcji zaraz po ich zdefiniowaniu. Oto przykład: ( function(){ alert('uuu!'); } )()
Początkowo może to wyglądać groźnie, ale tak naprawdę to proste — funkcję anonimową umieszcza się w nawiasie, po którym następuje inny nawias (w przykładzie jest pusty). Drugi nawias oznacza „uruchom teraz”. To w nim umieszcza się ewentualne parametry funkcji. ( function(imie){ alert('Cześć ' + imie + '!'); } )('stary')
Jedną z zalet samowywołujacych się funkcji anonimowych jest to, że kod zostanie wykonany bez tworzenia nadmiaru zmiennych. Minus jest taki, że tej samej funkcji nie da się uruchomić dwukrotnie (chyba że znajdzie się wewnątrz pętli lub innej funkcji). Dlatego anonimowe funkcje samowywołujące najlepiej nadają się do wykonywania jednokrotnych zadań inicjalizacyjnych.
Funkcje wewnętrzne (prywatne) Skoro funkcje są zwykłymi wartościami, nic nie stoi na przeszkodzie, by zdefiniować funkcję wewnątrz innej funkcji.
87
JavaScript. Programowanie obiektowe
function a(param) { function b(theinput) { return theinput * 2; }; return 'Wynik wynosi ' + b(param); };
Stosując drugą notację definiowania funkcji, możemy napisać: var a = function(param) { var b = function(theinput) { return theinput * 2; }; return 'Wynik wynosi ' + b(param); };
Kiedy globalna funkcja a() zostanie wywołana, wywoła także lokalną funkcję b(). Jako że b() jest lokalna, nie jest dostępna spoza a(), dlatego nazywamy ją funkcją prywatną. >>> a(2);
"The result is 4" >>> a(8);
"The result is 16" >>> b(2);
b is not defined Ze stosowania funkcji prywatnych płyną następujące korzyści: Q Nie dochodzi do zaśmiecenia globalnej przestrzeni nazw (zmniejszone ryzyko kolizji nazw). Q Prywatność: na zewnątrz widoczne są tylko te funkcje, które programista chce udostępnić. Funkcjonalności nieprzeznaczone dla reszty aplikacji są ukryte.
Funkcje, które zwracają funkcje Wspominałem już, że funkcja zawsze zwraca wartość, a jeśli nie robi tego w sposób jawny, to niejawnie zwracana jest wartość undefined. Funkcja zwraca dokładnie jedną wartość, która z powodzeniem może być inną funkcją. function a() { alert('A!'); return function(){ alert('B!'); }; }
88
Rozdział 3. • Funkcje
Widoczna powyżej funkcja a() wykonuje swoją pracę (mówi 'A!') i zwraca inną funkcję, która robi coś innego (mówi 'B!'). Wynik można przypisać jakiejś zmiennej i używać jej jako normalnej funkcji. >>> var newFunc = a(); >>> newFunc();
Pierwsza linia powyższego kodu spowoduje wyświetlenie okienka z wiadomością 'A!', a druga — okienka z wiadomością 'B!'. Jeśli funkcja zwracana przez inną funkcję ma zostać wykonana natychmiast, bez potrzeby przypisywania jej do nowej zmiennej, wystarczy dodać jeszcze jeden nawias. Wynik końcowy będzie taki sam jak wcześniej. >>> a()();
Funkcjo, przepiszże się! Ponieważ funkcje potrafią zwracać funkcje, możliwe jest zastąpienie oryginalnej funkcji tą zwracaną. Wróćmy do poprzedniego przykładu. Wartość zwróconą przez wywołanie a() można przypisać zmiennej a, nadpisując w ten sposób istniejącą funkcję: >>> a = a();
Powyższa linia przy pierwszym uruchomieniu spowoduje wyświetlenie 'A!', jednak jej drugie uruchomienie wyświetli 'B!'. Opisany mechanizm jest przydatny, jeśli funkcja wykonuje pewne jednorazowe zadanie. Po zakończeniu zadania zmiennej przechowującej funkcję przypisywana jest nowa wartość, dzięki czemu operacje nie muszą być powtarzane za każdym razem, gdy ktoś wywoła funkcję. W ostatnim przykładzie funkcja została przedefiniowana z zewnątrz — pobraliśmy zwróconą wartość i przypisaliśmy ją funkcji. Jednakże możliwe jest również przepisanie funkcji od środka. function a() { alert('A!'); a = function(){ alert('B!'); }; }
Przy pierwszym wywołaniu funkcja: Q Wyświetli 'A!' (załóżmy, że to właśnie jest nasze jednorazowe zadanie inicjalizacyjne). Q Zmieni definicję globalnej zmiennej a, przypisując jej nową funkcję. Każde kolejne wywołanie będzie powodowało wyświetlenie 'B!'.
89
JavaScript. Programowanie obiektowe
Oto inny przykład, który łączy kilka technik omówionych na ostatnich kilku stronach: var a = function() { function inicjalizacja(){ var setup = 'już'; } function normalnaPraca() { alert('praca wre!'); } inicjalizacja(); return normalnaPraca; }();
W przykładzie: Q Mamy funkcje prywatne: inicjalizacja() i normalnaPraca(). Q Mamy funkcję samowywołującą się: funkcja a() jest wywoływana dzięki nawiasowi
po jej definicji. Q Pierwsze wywołanie a() polega na wywołaniu funkcji inicjalizacja() i zwróceniu referencji do zmiennej normalnaPraca, która jest funkcją. Zwróć uwagę na brak
nawiasów przy zwracanej wartości — nie ma ich dlatego, że zwracamy do funkcji referencję, a nie wynik wywołania tejże funkcji. Q Jako że kod zaczyna się od var a =, wartość zwrócona przez samowywołującą się funkcję zostanie przypisana zmiennej a. Jeśli chcesz sprawdzić, czy poprawnie rozumiesz omówiony zakres materiału, spróbuj odpowiedzieć na poniższe pytania. Jakie będzie zachowanie napisanego przed chwilą programu, gdy: Q zostanie wgrany po raz pierwszy? Q po wgraniu zostanie wywołane a()? Przedstawione mechanizmy okazują się bardzo przydatne w środowisku przeglądarki. Różne przeglądarki mogą realizować konkretne zadania na różne sposoby. Przy założeniu, że właściwości przeglądarki nie zmienią się pomiędzy wywołaniami funkcji, możemy stworzyć funkcję, która wybierze sposób działania najlepiej dopasowany do danej przeglądarki, po czym w odpowiedni sposób zmieni swoją definicję, dzięki czemu tylko raz będzie musiała wykrywać typ przeglądarki. Konkretne przykłady zastosowania tego scenariusza będzie można zobaczyć na dalszych stronach książki.
Domknięcia Pozostała część tego rozdziału jest poświęcona domknięciom (czyż istnieje lepszy sposób na zamknięcie rozdziału?). Domknięcia początkowo mogą wydawać się trudne do zrozumienia, dlatego nie zniechęcaj się, jeśli nie pojmiesz wszystkiego od razu. Postaraj się doczytać rozdział
90
Rozdział 3. • Funkcje
do końca i poeksperymentować z przykładami, a jeśli niektóre zagadnienia nadal nie będą jasne, możesz do nich wrócić później, kiedy inne mechanizmy omówione w tym rozdziale nie będą już sprawiały Ci żadnego kłopotu. Zanim zajmiemy się domknięciami, powtórzmy i rozszerzmy trochę pojęcia zakresu w języku JavaScript.
Łańcuch zakresów Jak już Ci wiadomo, JavaScript nie wyróżnia żadnych zakresów ograniczonych nawiasami klamrowymi, ale istnieje zakres funkcji. Zmienna zdefiniowana wewnątrz funkcji nie jest widoczna poza tą funkcją, natomiast zmienna zdefiniowana wewnątrz bloku kodu (np. po if lub w pętli for) jest dostępna poza blokiem. >>> var a = 1; function f(){var b = 1; return a;} >>> f();
1 >>> b
b is not defined Zmienna a należy do globalnej przestrzeni nazw, podczas gdy zmienna b tylko do zakresu funkcji f(). Dlatego: Q Wewnątrz f() widoczne są zarówno a i b. Q Wewnątrz f() widoczna jest zmienna a, ale nie zmienna b.
Jeśli zdefiniujesz funkcję n() osadzoną w f(), n() będzie miała dostęp do zmiennych ze swojego zakresu, a także do zmiennych swoich „rodziców”. W takim wypadku mówimy o łańcuchu zakresów, który może być dowolnie długi (głęboki). var a = 1; function f(){ var b = 1; function n() { var c = 3; } }
Zasięg leksykalny Funkcje w języku JavaScript mają zasięg leksykalny. Oznacza to, że funkcje tworzą swoje własne środowisko (zakres) podczas definicji, a nie podczas wywołania. Spójrzmy na przykład:
91
JavaScript. Programowanie obiektowe
>>> function f1(){var a = 1; f2();} >>> function f2(){return a;} >>> f1();
a is not defined Wewnątrz funkcji f1() wywołujemy funkcję f2(). Ponieważ zmienna lokalna a znajduje się także wewnątrz f1(), ktoś mógłby się spodziewać, że f2() będzie miała dostęp do a, jednak tak nie jest. W momencie definicji f2() (a nie w momencie wywołania) nigdzie nie było śladu a. f2(), podobnie jak f1(), ma dostęp jedynie do własnego zakresu oraz do zakresu globalnego. f1() i f2() nie współdzielą zakresów lokalnych. Podczas definiowania funkcja zapamiętuje swoje środowisko, to znaczy swój łańcuch zakresów. Nie znaczy to wcale, że funkcja pamięta każdą konkretną zmienną, która pojawiła się w tym zakresie. Wręcz przeciwnie — zmienne można dodawać, usuwać i uaktualniać, a funkcja zawsze będzie widziała najnowszy, aktualny stan zmiennych. Jeśli rozszerzymy przykład o deklarację globalnej zmiennej a, stanie się ona widoczna dla f2(), ponieważ f2() zna ścieżkę do zmiennych globalnych i ma dostęp do całości tego środowiska. Zwróć uwagę na to, że f1() zawiera wywołanie f2(), które działa — mimo że f2() nie została jeszcze zdefiniowana. f1() musi tylko posiadać wiedzę o własnym zakresie, by wszystko, co się w nim pojawi, stawało się automatycznie dostępne dla f1(). >>> function f1(){var a = 1; f2();} >>> function f2(){return a;} >>> f1();
a is not defined >>> var a = 5; >>> f1();
5 >>> a = 55; >>> f1();
55 >>> delete a;
true >>> f1();
a is not defined Przedstawiony mechanizm sprawia, że JavaScript jest bardzo elastyczny — można dodawać zmienne, usuwać je, a potem dodawać je ponownie. Możesz poeksperymentować, kasując funkcję f2(), a potem definiując ją ponownie, ale z innym ciałem. Funkcja f1() nadal będzie działać, ponieważ musi znać jedynie sposób dostępu do swojego zakresu — nie jest jej potrzebna wiedza o tym, co kiedyś do tego zakresu należało. Ciąg dalszy przykładu:
92
Rozdział 3. • Funkcje
true >>> f1()
f2 is not defined >>> var f2 = function(){return a * 2;} >>> var a = 5;
5 >>> f1();
10
Przerwanie łańcucha za pomocą domknięcia Zaczniemy od ilustracji. Poniżej widzisz zakres globalny. Wyobraź go sobie jako wszechświat.
Może on zawierać zmienne, takie jak a, i funkcje, jak F.
Funkcje posiadają własną przestrzeń, którą mogą wykorzystywać do przechowywania innych zmiennych (i funkcji). W pewnym momencie rysunek będzie wyglądał mniej więcej tak:
93
JavaScript. Programowanie obiektowe
Jeśli jesteś w punkcie a, jesteś w przestrzeni globalnej. Jeśli w punkcie b, który należy do przestrzeni funkcji F, masz dostęp do przestrzeni globalnej oraz do przestrzeni F. Jeśli znalazłeś się w punkcie c, który należy do funkcji N, możesz sięgnąć do przestrzeni globalnej, przestrzeni F oraz N. Nie da się sięgnąć z a do b, ponieważ punkt b nie jest widoczny poza F. Możesz natomiast uzyskać dostęp z c do b lub z N do b. Ciekawe rzeczy (domknięcie) zaczynają się dziać, gdy jakimś sposobem N wydostaje się z F i trafia do przestrzeni globalnej.
Co się wtedy dzieje? N jest w tej samej przestrzeni globalnej co a. Jako że funkcje pamiętają środowisko, w którym zostały zdefiniowane, N nadal ma dostęp do przestrzeni F, a co za tym idzie dostęp do b. Jest to ciekawe dlatego, że N znajduje się tam gdzie a, a jednak N ma dostęp do b, zaś a nie. Jak N udaje się przerwać łańcuch? Istnieją dwa sposoby: N może zostać zmienną globalną (pominięcie var) lub może zostać zwrócona przez F do przestrzeni globalnej. Zobaczmy, jak to wygląda w praktyce.
Domknięcie 1. Przyjrzyj się uważnie tej funkcji: function f(){ var b = "b"; return function(){ return b; } }
94
Rozdział 3. • Funkcje
Funkcja zawiera lokalną zmienną b, która nie jest dostępna z przestrzeni globalnej: >>> b
b is not defined Zwróć uwagę na wartość zwracaną przez f(): jest ona inną funkcją. Możesz o niej myśleć jako o N z przedstawionych powyżej rysunków. Nowa funkcja ma dostęp do swojej przestrzeni prywatnej, do przestrzeni funkcji f() oraz do przestrzeni globalnej. Widzi zatem również b. Ponieważ f() można wywołać w przestrzeni globalnej (jest funkcją globalną), możesz ją wywołać i przypisać zwracaną przez nią wartość innej zmiennej globalnej. Wynikiem będzie nowa funkcja globalna, która ma dostęp do prywatnej przestrzeni f(). >>> var n = f(); >>> n();
"b"
Domknięcie 2. Przykład, który nastąpi za chwilę, pozwala uzyskać ten sam wynik co przykład wcześniejszy, jednak z zastosowaniem nieco innych metod. Funkcja f() nie będzie zwracała funkcji, a zamiast tego utworzy nową, globalną funkcję n() wewnątrz swojego ciała. Zacznijmy od deklaracji zmiennej, do której później przypiszemy nową funkcję. Nie jest to obowiązkowe, ale zawsze warto deklarować zmienne. Definicja funkcji f() może wyglądać tak: var n; function f(){ var b = "b"; n = function(){ return b; } }
Co się stanie po wywołaniu f()? >>> f();
Wewnątrz przestrzeni f() definiowana jest nowa funkcja. Ponieważ nie została użyta instrukcja var, funkcja jest globalna. W czasie definicji funkcja n() znajdowała się wewnątrz f(), zatem ma dostęp do zakresu zmiennych f(). n() zachowa prawo dostępu nawet wtedy, gdy stanie się częścią przestrzeni globalnej. >>> n();
"b"
95
JavaScript. Programowanie obiektowe
Domknięcie 3. i jedna definicja W oparciu o to, co zostało powiedziane do tej pory, możemy powiedzieć, że domknięcie jest tworzone, gdy funkcja zachowuje dostęp do zakresu rodzica po tym, jak rodzic zwrócił ją do globalnej przestrzeni nazw. Argument przekazany funkcji wewnątrz niej jest dostępny jako zmienna globalna. Możesz stworzyć funkcję zwracającą inną funkcję, która z kolei zwraca argument przekazany rodzicowi. function f(arg) { var n = function(){ return arg; }; arg++; return n; }
Funkcję można wywołać w następujący sposób: >>> var m = f(123); >>> m();
124 Zauważ, że zmienna arg została zwiększona już po definicji funkcji, a pomimo tego m() zwróciła aktualną wartość. Jest to kolejny dowód na to, że funkcje są związane ze swoimi zakresami, a nie z przechowywanymi tam w danym momencie zmiennymi i ich wartościami.
Domknięcia w pętli Pokażę teraz coś, co często prowadzi do bardzo trudnych do wykrycia błędów, ponieważ na pierwszy rzut oka wydaje się, że nie ma tam miejsca na pomyłkę. Napiszmy pętlę o trzech iteracjach, która za każdym przebiegiem zwraca numer pętli. Funkcje zostaną dodane do tablicy, która na koniec zostanie zwrócona. Oto nasza funkcja: function f() { var a = []; var i; for(i = 0; i < 3; i++) { a[i] = function(){ return i; } } return a; }
96
Rozdział 3. • Funkcje
Wywołajmy ją teraz, przypisując wynikową tablicę zmiennej a. >>> var a = f();
Mamy zatem tablicę z trzema funkcjami. Wywołajmy je, podając nawiasy po każdym elemencie tablicy. Oczekiwane zachowanie to wypisanie numerów iteracji: 0, 1 i 2. Spróbujmy: >>> a[0]()
3 >>> a[1]()
3 >>> a[2]()
3 Hm, niezupełnie to mieliśmy na myśli. Co się stało? Utworzyliśmy trzy domknięcia, które wskazują na tę samą lokalną zmienną i. Domknięcia nie pamiętają wartości, tylko przechowują referencję do zmiennej i — dlatego zwracają jej aktualną wartość. Po wyjściu z pętli wartością zmiennej i jest 3. Wszystkie funkcje wskazują na tę samą wartość. (Dla lepszego zrozumienia pętli zastanów się, dlaczego wartością i jest 3, a nie 2). Jak zatem zaimplementować poprawne zachowanie? Potrzebne nam są trzy różne zmienne. Eleganckie rozwiązanie polega na wykorzystaniu kolejnego domknięcia: function f() { var a = []; var i; for(i = 0; i < 3; i++) { a[i] = (function(x){ return function(){ return x; } })(i); } return a; }
Uzyskamy oczekiwany wynik: >>> var a = f(); >>> a[0]();
0 >>> a[1]();
1
97
JavaScript. Programowanie obiektowe
>>> a[2]();
2 W tej wersji nie tworzymy funkcji zwracającej i, tylko przekazujemy i innej, samowywołującej się funkcji. W tej funkcji i staje się lokalną zmienną x i za każdym razem ma inną wartość. Ten sam wynik można uzyskać przy użyciu „normalnej” (czyli niesamowywołującej się) funkcji wewnętrznej. Kluczem do sukcesu jest wykorzystanie środkowej funkcji do ustalenia wartości i podczas danej iteracji. function f() { function makeClosure(x) { return function(){ return x; } } var a = []; var i; for(i = 0; i < 3; i++) { a[i] = makeClosure(i); } return a; }
Funkcje dostępowe Chcę opowiedzieć o jeszcze dwóch sposobach wykorzystania domknięć. Pierwszy z nich polega na utworzeniu funkcji dostępowych get (pobranie wartości) i set (ustawienie wartości). Załóżmy, że posiadasz zmienną, która może przyjmować wartości tylko ze ściśle określonego zbioru. Nie chcesz odkrywać tej zmiennej, ponieważ chcesz zabezpieczyć się przed sytuacją, w której pewien fragment kodu nada jej niedozwoloną wartość. Rozwiązaniem jest utworzenie schronienia dla tej zmiennej wewnątrz pewnej funkcji i stworzenie dwóch dodatkowych funkcji, które będą odczytywały i ustawiały jej wartość. Funkcja ustawiająca wartość może zawierać pewną logikę, która nie pozwoli na nadanie zmiennej wartości spoza dozwolonego zbioru (jednak dla uproszczenia przykładu pomińmy walidację). Funkcje dostępowe powinny znaleźć się wewnątrz tej samej funkcji, która zawiera tajną zmienną, tak by dzieliły ten sam zakres: var getValue, setValue; (function() { var secret = 0; getValue = function(){ return secret; };
98
Rozdział 3. • Funkcje
setValue = function(v){ secret = v; }; })()
Funkcja, która opakowuje zmienną i dwie funkcje dostępowe, jest tutaj samowywołującą się funkcją anonimową. Definiuje ona setValue() i getValue() jako funkcje globalne, podczas gdy zmienna secret pozostaje lokalna i nie jest dostępna bezpośrednio. >>> getValue()
0 >>> setValue(123) >>> getValue()
123
Iterator Ostatni przykład domknięcia (a zarazem ostatni przykład w tym rozdziale) pokazuje wykorzystanie domknięć w celu osiągnięcia funkcjonalności iteratora. Wiesz już, jak wykorzystać pętlę do przejścia przez wszystkie elementy zwykłej tablicy. Możesz jednak napotkać bardziej złożoną strukturę danych, w której kolejność elementów jest określana przez bardziej złożony zestaw reguł. Wówczas skomplikowaną logikę rozwiązującą problem „kto następny?” umieszczasz w wygodnej w użyciu funkcji next(). Następnie wywołujesz next() za każdym razem, gdy chcesz pobrać kolejną wartość. Na potrzeby przykładu wykorzystamy jednak zwykłą tablicę, a nie złożoną strukturę danych. Oto funkcja inicjalizacyjna, która pobiera tablicę, a także definiuje prywatny wskaźnik i, zawsze wskazujący następny element w tablicy: function setup(x) { var i = 0; return function(){ return x[i++]; }; }
Wywołanie funkcji setup() z parametrem będącym tablicą danych spowoduje automatyczne utworzenie funkcji next(). >>> var next = setup(['a', 'b', 'c']);
Dalej czekają nas sam przyjemności: wywołując wciąż tę samą funkcję, przejdziemy przez wszystkie elementy tablicy.
99
JavaScript. Programowanie obiektowe
>>> next();
"a" >>> next();
"b" >>> next();
"c"
Podsumowanie Właśnie skończyliśmy podstawowy kurs pojęć związanych z funkcjami. Przejście do konceptów programowania obiektowego oraz do wzorców wykorzystywanych w nowoczesnym programowaniu w języku JavaScript powinno być dla Ciebie proste. Do tej pory unikaliśmy funkcjonalności obiektowych, ale od tej chwili nie będziemy już tego robić. Powtórzmy materiał przedstawiony w tym rozdziale. Omówione zostały następujące kwestie: Q Definiowanie i wywoływanie funkcji. Q Parametry funkcji i ich elastyczność. Q Funkcje wbudowane: parseInt(), parseFloat(), isNaN(), isFinite(), eval(),
a także cztery funkcje do kodowania i dekodowania adresów URL. Q Zakres zmiennych: nie ma zakresu związanego z nawiasami klamrowymi, istnieje
zakres funkcji, funkcje mają zakres leksykalny, obowiązuje zasada łańcucha zakresów. Q Funkcje to dane — funkcję można przypisać zmiennej, z czego wynika szereg ciekawych zastosowań, wśród których można wymienić: Q Q Q
prywatne funkcje i zmienne, funkcje anonimowe, wywołania zwrotne,
samowywołujące się funkcje, Q funkcje zmieniające swoją definicję. Q Domknięcia. Q
Ćwiczenia 1. Napisz funkcję, która przekształca szesnastkową definicję koloru (np. niebieski to "0000FF") na reprezentację RGB (np. "rgb(0, 0, 255)"). Nazwij funkcję getRGB() i przetestuj ją za pomocą następującego kodu:
100
Rozdział 3. • Funkcje
>>> var a = getRGB("#00FF00"); >>> a;
"rgb(0, 255, 0)" 2. Co pojawi się w konsoli po uruchomieniu każdej z poniższych linii kodu? >>> >>> >>> >>> >>> >>>
parseInt(1e1) parseInt('1e1') parseFloat('1e1') isFinite(0/10) isFinite(20/0) isNaN(parseInt(NaN));
3. Co pojawi się w okienku alert() po wykonaniu następującego kodu? var a = 1; function f() { var a = 2; function n() { alert(a); } n(); } f();
4. Wszystkie poniższe przykłady spowodują wyświetlenie "Uuu!". Czy potrafisz powiedzieć dlaczego? 4.1 var f = alert; eval('f("Uuu!")');
4.2 var e; var f = alert; eval('e=f')('Uuu!');
4.3 ( function(){ return alert; } )()('Uuu!');
101
JavaScript. Programowanie obiektowe
102
4 Obiekty Skoro znasz już na wylot podstawowe typy danych, tablice oraz funkcje, przyszła pora na to, co najciekawsze — obiekty. W tym rozdziale dowiesz się: Q jak tworzyć obiekty i jak ich używać, Q czym są funkcje nazywane konstruktorami, Q jak korzystać z wbudowanych obiektów JavaScriptu.
Od tablic do obiektów Jak już wiesz z rozdziału 2., tablica jest listą wartości. Każdej wartości odpowiada indeks, przy czym indeksy kolejnych elementów zaczynają się od zera i są zwiększane o jeden dla każdej kolejnej wartości. >>>> var myarr = ['czerwony', 'niebieski', 'żółty', 'fioletowy']; >>> myarr;
["czerwony", "niebieski", "żółty", "fioletowy"] >>> myarr[0]
"czerwony" >>> myarr[3]
"fioletowy"
JavaScript. Programowanie obiektowe
Jeśli wstawimy indeksy do jednej kolumny tablicy, a wartości do drugiej, otrzymamy następującą tablicę par klucz – wartość: Klucz
Wartość
0
czerwony
1
niebieski
2
żółty
3
fioletowy
Obiekty różnią się od tablic między innymi tym, że programista samodzielnie definiuje klucze. Nie musisz ograniczać się do liczbowych indeksów. Możesz korzystać z bardziej przyjaznych nazw, takich jak nazwisko, data_urodzenia czy wiek. Przeanalizujmy więc nasz pierwszy, prosty obiekt: var bohater = { gatunek: 'Żółw', specjalizacja: 'Ninja' };
Możesz zauważyć, że: Q Zmienna, która przechowuje obiekt, nazywa się bohater. Q Inaczej niż w przypadku tablic, do definiowania obiektów używa się nawiasów klamrowych { i }, a nie kwadratowych [ i ]. Q Elementy obiektu (nazywane polami lub własnościami) oddziela się za pomocą
przecinków. Q Pary klucz – wartość rozdziela się dwukropkiem. Klucze (nazwy pól) można umieszczać w cudzysłowach. Poniższe instrukcje są równoważne: var o = {prop: 1}; var o = {"prop": 1}; var o = {'prop': 1};
Nie zaleca się stosowania cudzysłowów (chociażby ze względu na oszczędność znaków), jednak w niektórych sytuacjach nie da się ich uniknąć: Q jeśli nazwa pola jest jednym z zarezerwowanych słów języka JavaScript (pełna lista w Dodatku A); Q jeśli nazwa zawiera znaki specjalne (czyli znaki inne niż litery, liczby i podkreślnik); Q jeśli pierwszym znakiem nazwy jest cyfra.
W skrócie: jeśli zdecydujesz się nadać polu nazwę, która nie jest poprawną nazwą zmiennej, to musisz umieścić ją w cudzysłowie.
104
Rozdział 4. • Obiekty
Pokazany poniżej dziwaczny twór: var o = { pole: 1, 'tak lub nie': 'tak', '!@#$%^&*': true };
jest w pełni poprawnym obiektem. W przypadku drugiego i trzeciego pola cudzysłów1 jest obowiązkowy — pominięcie go doprowadzi do błędu. W dalszej części rozdziału poznasz inne niż []i {} sposoby definiowania obiektów i tablic. Tablice zdefiniowane za pomocą [] określa się mianem literałów tablicowych, a obiekty zdefiniowane za pomocą {} to literały obiektowe.
Elementy, pola, metody Mówimy, że tablice zawierają elementy. Obiekty, dla odmiany, mają pola. Dla JavaScriptu to rozróżnienie nie ma znaczenia — jest ono czysto terminologiczne i pochodzi z innych języków programowania. Pole obiektu może zawierać funkcję, ponieważ funkcje również są danymi. Takie pola nazywamy metodami. var pies = { imie: 'Burek', mow: function(){ alert('Hau, hau!'); } };
Możliwe jest także przechowywanie funkcji w tablicy, jednak taki kod jest rzadkością. >>> var a = []; >>> a[0] = function(co){alert(co);}; >>> a[0]('Uuu!');
Tablice asocjacyjne W niektórych językach programowania istnieje rozróżnienie na: Q zwykłe tablice indeksowane, których kluczami są liczby; Q tablice asocjacyjne, których kluczami są łańcuchy znaków2. 1
W języku angielskim cudzysłowu i apostrofów można używać zamiennie — tak samo jest w języku JavaScript — przyp. tłum.
2
Lub dowolne obiekty — przyp. tłum.
105
JavaScript. Programowanie obiektowe
W języku JavaScript tablicom indeksowanym odpowiadają tablice, a tablicom asocjacyjnym — obiekty.
Dostęp do własności obiektu Dostęp do własności obiektu można uzyskać na dwa sposoby: Q przy użyciu nawiasów kwadratowych, na przykład bohater['specjalizacja']; Q przy użyciu kropki, na przykład bohater.specjalizacja. Notacja z kropką jest wygodniejsza, ale nie zawsze można ją zastosować. Zasady są takie same jak w przypadku nazw pól: jeśli nazwa nie jest poprawną nazwą zmiennej, nie można skorzystać z notacji z kropką. Weźmy następujący obiekt: var bohater = { gatunek: 'Żółw', specjalizacja: 'Ninja' };
Dostęp do własności obiektu za pomocą notacji z kropką: >>> bohater.gatunek;
."Żółw" Dostęp do własności obiektu za pomocą notacji nawiasowej: >>> bohater['specjalizacja'];
"Ninja" Próba dostępu do nieistniejącego pola kończy się zwróceniem wartości undefined: >>> 'Kolor włosów bohatera to ' + bohater.kolor_wlosow;
"Kolor włosów bohatera to undefined" Obiekty mogą zawierać dane, w tym także inne obiekty. var ksiazka = { tytul: 'Paragraf 22', wydana: 1961, autor: { imie: 'Joseph', nazwisko: 'Heller' } };
106
Rozdział 4. • Obiekty
W celu pobrania wartości pola imie obiektu będącego wartością pola autor obiektu ksiazka, należy napisać: >>> ksiazka.autor.imie
"Joseph" lub, przy użyciu składni z nawiasami: >>> ksiazka['autor']['nazwisko']
"Heller" Można nawet łączyć notacje: >>> ksiazka.autor['nazwisko']
"Heller" >>>ksiazka['autor'].nazwisko
"Heller" Istnieje jeszcze jedna sytuacja, w której konieczne jest użycie notacji nawiasowej. Jeśli nazwa pola, do którego chcemy sięgnąć, nie jest znana w czasie pisania kodu, można przypisać jej wartość zmiennej: >>> var klucz = 'imie' >>> ksiazka.autor[klucz];
Joseph
Wywoływanie metod obiektu Skoro metoda jest po prostu polem klasy, które przypadkiem jest także funkcją, dostęp do metod odbywa się tak samo jak dostęp do zwykłych pól: przy użyciu notacji z kropką lub notacji nawiasowej. Metody wywołuje się jak wszystkie inne funkcje: należy po nazwie metody dodać nawiasy, które wydadzą rozkaz „Wykonać!”. var bohater = { gatunek: 'Żółw', specjalizacja: 'Ninja' mow: function() { return 'Moja specjalizacja to ' + bohater.specjalizacja; } } >>> bohater.mow();
"Moja specjalizacja to Ninja"
107
JavaScript. Programowanie obiektowe
Jeśli metoda pobiera parametry, przekazujemy je dokładnie tak samo jak w przypadku zwykłych funkcji: >>> bohater.mow('a', 'b', 'c');
To, że dostęp do pól może odbywać się za pomocą nawiasów kwadratowych, oznacza, że w ten sam sposób można wywoływać funkcje. W praktyce jednak rzadko stosuje się tę składnię: >>> bohater['mow'](); Dobra rada: żadnych cudzysłowów! 1. Podczas sięgania do pól i metod stosuj notację z kropką. 2. Nie używaj cudzysłowów w nazwach pól literałów obiektowych.
Modyfikacja pól i metod JavaScript jest językiem dynamicznym: pozwala modyfikować składowe (czyli pola i metody) istniejących obiektów w czasie wykonania. Można dodawać nowe składowe i usuwać stare. Można utworzyć pusty obiekt, a pola i metody dodać do niego później. Zobaczmy, jak to zrobić. Pusty obiekt: >>> var bohater = {};
Dostęp do nieistniejącego pola: >>> typeof bohater.gatunek
"undefined" Dodanie pól i metod: >>> bohater.gatunek = 'Żółw'; >>> bohater.imie = 'Leonardo'; >>> bohater.mowImie = function() {return bohater.imie;};
Wywołanie metody: >>> bohater.mowImie();
"Leonardo" Usuwanie własności: >>> delete bohater.imie;
true
108
Rozdział 4. • Obiekty
Próba ponownego wywołania metody zwracającej imię zakończy się niepowodzeniem: >>> bohater.mowImie();
reference to undefined property bohater.imie
Wartość this W poprzednim przykładzie widzieliśmy metodę mowImie(), która do pola imie obiektu bohater odwoływała się za pomocą składni bohater.imie. Istnieje jednak inny, bardziej ogólny sposób dostępu z wnętrza metody do aktualnego obiektu (to znaczy do obiektu, do którego należy metoda): poprzez specjalną wartość this. var bohater = { imie: 'Rafael', mowImie: function() { return this.imie; } } >>> bohater.mowImie();
"Rafael" Jak widać, this (z ang. „ten”) oznacza bieżący obiekt.
Konstruktory Obiekty można tworzyć także przy użyciu funkcji nazywanych konstruktorami. Przykład: function Bohater() { this.specjalizacja = 'Ninja'; }
Konstruktor wywołujemy przy użyciu operatora new: >>> var bohater = new Bohater(); >>> bohater.specjalizacja;
"Ninja" Przewagą tego sposobu tworzenia obiektów jest to, że konstruktory mogą przyjmować parametry. Zmieńmy kod konstruktora tak, by pobierał jeden parametr i przypisywał jego wartość zmiennej imie. function Bohater(imie) { this.imie = imie; this.specjalizacja = 'Ninja';
109
JavaScript. Programowanie obiektowe
this.kimJestes = function() { return "Jestem " + this.imie + ", a moja specjalizacja to " + this.specjalizacja; } }
Przy użyciu jednego konstruktora można utworzyć wiele różnych obiektów: >>> var h1 = new Bohater('Michał Anioł'); >>> var h2 = new Bohater('Donatello'); >>> h1.kimJestes();
"Jestem Michał Anioł, a moja specjalizacja to Ninja" >>> h2.kimJestes();
"Jestem Donatello, a moja specjalizacja to Ninja" Konwencja nakazuje zaczynać nazwy konstruktorów wielką literą, dzięki czemu od razu można zorientować się, że nie mamy do czynienia z normalną funkcją. Wywołanie konstruktora bez operatora new nie zostanie uznane za błąd, ale może prowadzić do nieoczekiwanych wyników: >>> var h = Bohater('Leonardo'); >>> typeof h
"undefined" Co tu zaszło? Ponieważ nie został użyty operator new, nie powstał nowy obiekt. Funkcja została wywołana jako zwykła funkcja, a nie jako konstruktor, zatem h zawiera wartość zwracaną przez funkcję. Ponieważ jednak funkcja nie zawiera instrukcji return, w rzeczywistości zwraca wartość undefined, która zostaje przypisana zmiennej h. W takim razie do czego odnosi się wskaźnik this? Otóż odnosi się on do obiektu globalnego.
Obiekt globalny Omawialiśmy już zmienne globalne (i potrzebę ich unikania). Mówiłem także o tym, że programy napisane w języku JavaScript są uruchamiane wewnątrz środowiska (na przykład przeglądarki). Skoro wiesz już o istnieniu obiektów, musisz poznać całą prawdę: środowisko zapewnia obiekt globalny, a wszystkie zmienne globalne są jego polami. Jeśli uruchamiasz programy w środowisku przeglądarki, Twoim obiektem globalnym jest window („okno”). Możesz przekonać się o istnieniu obiektu globalnego, deklarując zmienną globalną poza wszelkimi funkcjami: >>> var a = 1;
110
Rozdział 4. • Obiekty
Dostęp do niej uzyskasz na różne sposoby: Q odwołując się do zmiennej a; Q odwołując się do pola obiektu globalnego, na przykład window['a'] lub window.a. Wróćmy do przykładu, w którym definiowaliśmy konstruktor i wywoływaliśmy go bez użycia operatora new. Jak już mówiłem, w takim wypadku this odwołuje się do obiektu globalnego, a wszystkie własności ustawione za pomocą this stają się własnościami obiektu globalnego (w przypadku przeglądarki będzie to window). Zadeklarowanie konstruktora i wywołanie go bez new zwróci undefined. >>> function Bohater(imie) {this.imie = imie;} >>> var h = Bohater('Leonardo'); >>> typeof h
"undefined" >>> typeof h.imie
h has no properties Ponieważ wewnątrz funkcji Bohater pojawiło się this, utworzona została zmienna globalna (pole obiektu globalnego) o nazwie imie. Próba odwołania się do pola imie zmiennej h kończy się niepowodzeniem (i komunikatem, że h nie posiada żadnych własności). >>> imie
"Leonardo" >>> window.imie
"Leonardo" Jeśli konstruktor zostanie wywołany z użyciem new, zwrócony zostanie nowy obiekt, do którego będzie się odnosiło słowo this. >>> var h2 = new Bohater('Michał Anioł'); >>> typeof h2
"object" >>> h2.imie
"Michał Anioł" Także funkcje globalne z rozdziału 3. można wywołać jako metody obiektu window. Poniższe dwa fragmenty kodu są równoważne: >>> parseInt('101 dalmatyńczyków')
101 >>> window.parseInt('101 dalmatyńczyków')
101 111
JavaScript. Programowanie obiektowe
Pole constructor W czasie gdy obiekt jest tworzony, otrzymuje on specjalne pole o nazwie constructor. Zawiera ono referencję do konstruktora, który został użyty do utworzenia obiektu. Kontynuując przykład z bohaterami: >>> h2.constructor
Bohater(imie) Jako że własność constructor zawiera referencję do funkcji, można wywołać tę funkcję w celu utworzenia nowego obiektu. Poniższy kod oznacza mniej więcej: „Nie interesuje mnie, jak powstał obiekt h2, ale chcę dostać jeszcze jeden taki sam”. >>> var h3 = new h2.constructor('Rafael'); >>> h3.imie;
"Rafael" Konstruktorem obiektów literałowych jest wbudowana funkcja Object() (więcej na jej temat w dalszej części rozdziału). >>> var o = {}; >>> o.constructor;
Object() >>> typeof o.constructor;
"function"
Operator instanceof Przy użyciu operatora instanceof można sprawdzić, czy obiekt został utworzony za pomocą określonego konstruktora: >>> >>> >>> >>>
function Bohater(){} var h = new Bohater(); var o = {}; h instanceof Bohater;
true >>> h instanceof Object;
false >>> o instanceof Object;
true
112
Rozdział 4. • Obiekty
Zwróć uwagę, że podczas sprawdzania po nazwie konstruktora nie podaje się nawiasów (zatem nie piszemy h instanceof Bohater()). Jest tak dlatego, że nie wywołujemy funkcji, tylko odwołujemy się do niej za pomocą nazwy, jak do każdej innej zmiennej.
Funkcje zwracające obiekty Obiekty można tworzyć nie tylko za pomocą konstruktorów i operatora new, ale także za pomocą zwykłych funkcji. Możliwe jest napisanie funkcji, która wykona pewne zadania przygotowawcze i na koniec zwróci wartość będącą obiektem. Poniższy przykład przedstawia prostą funkcję o nazwie factory() („fabryka”), która produkuje obiekty: function factory(name) { return { name: name }; }
Korzysta się z niej w następujący sposób: >>> var o = factory('jeden'); >>> o.name
"jeden" >>> o.constructor
Object() Można także korzystać z konstruktorów i zwracać obiekty inne niż this, czyli zmieniać standardowe zachowanie konstruktora. Już tłumaczę, jak to zrobić. Oto najczęstszy scenariusz wykorzystania konstruktora: >>> function C() {this.a = 1;} >>> var c = new C(); >>> c.a
1 A jednak można zrobić coś takiego: >>> function C2() {this.a = 1; return {b: 2};} >>> var c2 = new C2(); >>> typeof c2.a
"undefined" >>> c2.b
2 113
JavaScript. Programowanie obiektowe
Co się stało? Zamiast obiektu this, który posiada pole a, konstruktor zwrócił inny obiekt, który posiada pole b. Jest to możliwe tylko wtedy, gdy zwracana wartość jest obiektem. W przeciwnym wypadku (jeśli zwrócona zostanie dowolna wartość niebędąca obiektem), konstruktor zachowa się zgodnie ze standardowym scenariuszem i zwróci this.
Przekazywanie obiektów Podczas kopiowania obiektu lub przekazywania go funkcji w rzeczywistości przekazuje się jedynie referencję do tego obiektu. Modyfikacja tej referencji pociąga za sobą modyfikację oryginalnego obiektu. W poniższym fragmencie obiekt jest przypisywany nowej zmiennej, a następnie uzyskana w ten sposób kopia obiektu jest zmieniana. W wyniku tego zmienia się także pierwotny obiekt: >>> var oryginal = {ile: 1}; >>> var kopia = oryginal; >>> kopia.ile
1 >>> kopia.ile = 100;
100 >>> oryginal.ile
100 Tak samo ma się sprawa z przekazywaniem obiektów funkcjom: >>> >>> >>> >>>
var oryginal = {ile: 100}; var zeruj = function(o) {o.ile = 0;} zeruj(oryginal); oryginal.ile
0
Porównywanie obiektów Wynikiem porównania dwóch obiektów będzie true tylko wtedy, gdy porównywane będą dwie referencje do tego samego obiektu. Jeśli porównamy dwa oddzielne obiekty, które akurat mają ten sam zestaw pól i metod, to mimo wszystko otrzymamy false. Utwórzmy dwa obiekty, które wyglądają tak samo: >>> var azor = {gatunek: 'pies'}; >>> var burek = {gatunek: 'pies'};
114
Rozdział 4. • Obiekty
Wynikiem porównania będzie false: >>> azor === burek
false >>> azor == burek
false Utwórzmy teraz nową zmienną mojPies i przypiszmy jej jeden z obiektów. W ten sposób otrzymamy dwie zmienne wskazujące ten sam obiekt. >>> var mojPies = burek;
Teraz mojPies i burek są referencjami do tego samego obiektu. Zmiana własności mojPies pociągnie za sobą zmianę własności obiektu burek. Wynikiem porównania będzie true. >>> mojPies === burek
true Ponieważ azor jest innym obiektem, nie zostanie dopasowany do mojPies: >>> mojPies === azor
false
Obiekty w konsoli Firebug Zanim na poważnie zajmiemy się obiektami wbudowanymi, chcę powiedzieć kilka słów na temat pracy z obiektami w konsoli Firebug. Prawdopodobnie udało się już, na podstawie przykładów przedstawionych w tym rozdziale, wyciągnąć pewne wnioski na temat sposobu wyświetlania obiektów w konsoli. Jeśli utworzysz obiekt, a następnie wpiszesz jego nazwę, obiekt zostanie przedstawiony w postaci łańcucha znaków, który uwzględni także własności obiektu. W przypadku gdy własności jest zbyt wiele, pokazane zostanie tylko kilka pierwszych.
115
JavaScript. Programowanie obiektowe
Jeśli klikniesz na reprezentacji obiektu, Firebug przeniesie Cię do zakładki DOM, w której pokazane są wszystkie własności obiektu. Jeśli dana własność sama jest obiektem, obok jej nazwy pojawi się znak plus (+), za pomocą którego można wyświetlić szczegóły zagnieżdżonego obiektu.
Konsola daje nam dostęp do obiektu o nazwie console, którego metody, takie jak console.log(), console.error() i console.info(), pozwalają wypisać w konsoli dowolną wartość.
console.log() przydaje się, gdy trzeba szybko coś przetestować lub gdy skrypt ma wypisywać
informacje ułatwiające debugowanie. Poniższy przykład pokazuje zastosowanie tej metody w pętli: >>> for(var i = 0; i < 5; i++) { console.log(i); }
0 1 2 3 4
116
Rozdział 4. • Obiekty
Obiekty wbudowane Wcześniej w tym rozdziale zetknęliśmy się już z konstruktorem Object(). Jest on zwracany przez obiekty literałowe, gdy sięgnie się do ich pola constructor. Funkcja ta jest jednym z konstruktorów wbudowanych. Takich konstruktorów jest więcej — z wszystkimi spośród nich spotkasz się zaraz na kartach tego rozdziału. Obiekty wbudowane można podzielić na trzy kategorie: Q Obiekty opakowujące: Object, Array, Function, Boolean, Number i String. Odpowiadają one różnym typom danych JavaScriptu. Zasadniczo każda wartość zwracana przez operator typeof (omówiony w rozdziale 2.) posiada swój obiekt opakowujący. Wyjątkami są "undefined" i "null". Q Obiekty użytkowe. Są to Math, Date i RegExp — warto jest je poznać, ponieważ często
okazują się przydatne. Q Obiekty błędów, czyli obiekt Error oraz inne, bardziej szczegółowe obiekty, za pomocą których można przywrócić działanie programu po wystąpieniu nieoczekiwanej sytuacji. W rozdziale omawiam jedynie wybrane metody obiektów wbudowanych. Pełna lista znajduje się w dodatku C. Jeśli nie widzisz różnicy pomiędzy wbudowanym obiektem a wbudowanym konstruktorem — nie martw się, w zasadzie są tym samym. Za chwilę wytłumaczę, że funkcje, wśród nich również konstruktory, także są obiektami.
Object Object jest rodzicem wszystkich obiektów w języku JavaScript — wszystkie inne obiekty z niego dziedziczą. W celu utworzenia nowego obiektu możesz skorzystać z notacji literałowej albo z konstruktora Object(). Następujące dwie linie są równoważne: >>> var o = {}; >>> var o = new Object();
Pusty obiekt nie jest zupełnie bezużyteczny, ponieważ już na starcie jest wyposażony w kilka pól i metod: Q Własność o.constructor zwróci konstruktor. Q o.toString() to metoda, która zwraca tekstową reprezentację obiektu. Q o.valueOf() zwraca jednowartościową reprezentację obiektu, najczęściej sam
obiekt.
117
JavaScript. Programowanie obiektowe
Zobaczmy te metody w akcji: >>> var o = new Object();
Wywołanie toString()zwróci tekstową reprezentację obiektu: >>> o.toString()
"[object Object]" Metoda toString() zostanie wewnętrznie wywołana przez JavaScript, jeśli obiekt zostanie użyty w kontekście łańcucha znaków. Przykładowo alert() działa jedynie na łańcuchach, dlatego jeśli zostanie jej przekazany obiekt, w tle zostanie wywołana metoda toString(). Poniższe dwie linie przyniosą ten sam efekt: >>> alert(o) >>> alert(o.toString())
Innym typem kontekstu tekstowego jest konkatenacja (złączanie) łańcuchów znaków. Jeśli podjęta zostanie próba połączenia obiektu z łańcuchem, obiekt od razu zostanie zamieniony na odpowiadający mu tekst: >>> "An object: " + o
"An object: [object Object]" valueOf() to kolejna metoda, w którą wyposażone są wszystkie obiekty. W przypadku obiektów prostych, których konstruktorem jest Object(), valueOf() zwróci dany obiekt: >>> o.valueOf() === o
true Podsumujmy: Q Obiekty można tworzyć za pomocą var o = {}; (preferowana notacja literałowa) lub za pomocą var o = new Object();. Q Każdy, nawet najbardziej złożony obiekt dziedziczy z obiektu Object i dzięki temu posiada metody takie jak toString() i pola takie jak constructor.
Array Array() to funkcja wbudowana, której można używać jako konstruktora do tworzenia tablic: >>> var a = new Array();
Powyższy fragment kodu odpowiada następującemu zapisowi literałowemu: >>> var a = [];
118
Rozdział 4. • Obiekty
Niezależnie od tego, w jaki sposób została utworzona tablica, można dodawać do niej elementy w ten sam, znany nam sposób: >>> a[0] = 1; a[1] = 2; a;
[1, 2] Konstruktorowi Array() można przekazać wartości, które zostaną wstawione do tablicy jako jej elementy. >>> var a = new Array(1,2,3,'cztery'); >>> a; [1, 2, 3, "cztery"]
Wyjątkiem jest zachowanie konstruktora, gdy jako argument przekażemy pojedynczą liczbę. Wówczas zostanie ona uznana za długość tablicy. >>> var a2 = new Array(5); >>> a2;
[undefined, undefined, undefined, undefined, undefined] Skoro tablice można tworzyć przy użyciu konstruktora, czy są one obiektami? Tak — można upewnić się za pomocą operatora typeof: >>> typeof a;
"object" Każdy obiekt dziedziczy pola i metody pochodzące od Object: >>> a.toString();
"1,2,3,cztery" >>> a.valueOf()
[1, 2, 3, "cztery"] >>> a.constructor
Array() Tablice są obiektami obdarzonymi pewnymi wyjątkowymi cechami: Q Ich pola są nazywane automatycznie za pomocą liczb od zera w górę. Q Posiadają pole length, które zawiera liczbę elementów tablicy. Q Poza metodami odziedziczonymi z Object posiadają własne metody wbudowane.
Przyjrzymy się różnicom pomiędzy tablicą a obiektem. Na początek utwórzmy pusty obiekt o i pustą tablicę a: >>> var a = [], o = {};
119
JavaScript. Programowanie obiektowe
Tablice zawsze posiadają pole length określające ich długość, podczas gdy zwykłe obiekty nie: >>> a.length
0 >>> typeof o.length
"undefined" Zarówno do tablic, jak i do obiektów można dodawać pola liczbowe i nieliczbowe: >>> a[0] = 1; o[0] = 1; >>> a.prop = 2; o.prop = 2;
Pole length zawsze przechowuje liczbę pól numerycznych, ignorując pozostałe. >>> a.length
1 Wartość pola length można zmieniać. Zwiększenie jego wartości powoduje dodanie do tablicy pustych elementów (o wartości undefined). >>> a.length = 5
5 >>> a
[1, undefined, undefined, undefined, undefined] Zmniejszenie wartości pola length spowoduje usunięcie końcowych elementów. >>> a.length = 2;
2 >>> a
[1, undefined]
Ciekawe metody obiektu Array Poza metodami odziedziczonymi z Object obiekty tablicowe posiadają własne przydatne metody, takie jak sort(), join() i slice() (pełna lista w dodatku C). Poeksperymentujmy sobie z metodami tablic: >>> var a = [3, 5, 1, 7, 'test'];
Metoda push() dodaje element na koniec tablicy, zaś pop() usuwa ostatni element. Wywołanie a.push('new') zadziała tak samo jak a[a.length] = 'new', a a.pop() odpowiada a.length--.
120
Rozdział 4. • Obiekty
>>> a.push('new')
6 >>> a
[3, 5, 1, 7, "test", "new"] >>> a.pop()
"new" >>> a
[3, 5, 1, 7, "test"] Metoda sort() sortuje elementy tablicy, a także zwraca wynik sortowania. W poniższym przykładzie, po wywołaniu sort(), zmienne a i b wskazują tę samą tablicę: >>> var b = a.sort(); >>> b
[1, 3, 5, 7, "test"] >>> a
[1, 3, 5, 7, "test"] Metoda join() zwraca łańcuch składający się z wartości elementów tablicy rozdzielonych łańcuchem przekazanym jako parametr: >>> a.join(' to nie ');
"1 to nie 3 to nie 5 to nie 7 to nie test" slice()zwraca fragment tablicy bez wprowadzania modyfikacji do oryginalnego obiektu. Pierwszym parametrem jest indeks początkowy (indeks pierwszego elementu, który ma zostać zwrócony) , drugim indeks końcowy — oba liczone od zera. >>> b = a.slice(1, 3);
[3, 5] >>> b = a.slice(0, 1);
[1] >>> b = a.slice(0, 2);
[1, 3] Oryginalna tablica nie uległa zmianie: >>> a
[1, 3, 5, 7, "test"]
121
JavaScript. Programowanie obiektowe
Metoda splice()dla odmiany zmienia tablicę, na której jest wywoływana. Usuwa ona fragment tablicy, zwraca go oraz, opcjonalnie, wypełnia powstałą lukę nowymi elementami. Pierwsze dwa parametry to indeksy początkowy i końcowy, pozostałe to nowe wartości. >>> b = a.splice(1, 2, 100, 101, 102);
[3, 5] >>> a
[1, 100, 101, 102, 7, "test"] Wypełnianie luki nowymi elementami nie jest obowiązkowe — można z niego zrezygnować: >>> a.splice(1, 3)
[100, 101, 102] >>> a
[1, 7, "test"]
Function Wiesz już, że funkcje są pewnym specjalnym typem danych. Okazuje się jednak, że są czymś więcej — są obiektami. Istnieje wbudowany konstruktor Function, który pozwala tworzyć funkcje w odmienny od pokazanego wcześniej (aczkolwiek niezalecany) sposób. Istnieją trzy równoważne sposoby definiowania funkcji: >>> function suma(a, b) {return a + b;}; >>> suma(1, 2)
3 >>> var suma = function(a, b) {return a + b;}; >>> suma(1, 2)
3 >>> var suma = new Function('a', 'b', 'return a + b;'); >>> suma(1, 2)
3 Konstruktorowi Function() przekazuje się najpierw nazwy parametrów, a potem kod źródłowy ciała funkcji (wszystko jako łańcuch znaków). Do utworzenia funkcji konieczne jest przetworzenie kodu podanego w postaci tekstowej. Rozwiązanie to posiada wszystkie wady funkcji eval(), dlatego należy ograniczać stosowanie konstruktora Function(). Jeśli funkcja tworzona za pomocą Function() ma wiele argumentów, można zapisać je wewnątrz jednego łańcucha znaków, oddzielając poszczególne parametry przecinkami. Następujące definicje są równoważne: 122
Rozdział 4. • Obiekty
>>> var pierwsza = new Function('a, b, c, d', 'return arguments;'); >>> pierwsza(1,2,3,4);
[1, 2, 3, 4] >>> var druga = new Function('a, b, c', 'd', 'return arguments;'); >>> druga(1,2,3,4);
[1, 2, 3, 4] >>> var trzecia = new Function('a', 'b', 'c', 'd', 'return arguments;'); >>> trzecia(1,2,3,4);
[1, 2, 3, 4] Dobra rada Nie używaj konstruktora Function(). Należy unikać wszelkich funkcji, które jako argument pobierają kod w postaci łańcucha znaków. Do tej samej grupy należą funkcje setTimeout() (która jeszcze nie pojawiła się w tej książce) oraz eval().
Własności obiektu Function Jak wszystkie inne obiekty, funkcje posiadają pole constructor, które zawiera referencję do konstruktora Function(). >>> function myfunc(a){return a;} >>> myfunc.constructor
Function() Funkcje posiadają także pole length, które określa liczbę parametrów przyjmowanych przez funkcję. >>> function myfunc(a, b, c){return true;} >>> myfunc.length
3 Jest jeszcze jedno interesujące pole, które nie należy do standardu ECMA, ale które istnieje w większości przeglądarek — pole caller. Zawiera ono referencję do funkcji, która wywołała naszą funkcję. Powiedzmy, że funkcja A() jest wywoływana przez funkcję B(). Jeśli wewnątrz A() wywołamy A.caller, zwrócona zostanie funkcja B(). >>> function A(){return A.caller;} >>> function B(){return A();} >>> B()
B()
123
JavaScript. Programowanie obiektowe
Jest to przydatne, jeśli funkcja ma zachowywać się odmiennie w zależności od tego, jaka inna funkcja ją wywołała. Jeśli wywołasz A() w przestrzeni globalnej (poza jakąkolwiek funkcją), A.caller będzie miało wartość null. >>> A()
null Najważniejszym polem funkcji jest pole prototype. Omówię je dokładnie w następnym rozdziale, chwilowo wystarczy następujący zestaw faktów: Q Pole prototype funkcji zawiera obiekt. Q Ma ono znaczenie tylko, jeśli funkcja jest wywoływana jako konstruktor. Q Wszystkie obiekty utworzone za pomocą funkcji przechowują referencję do pola prototype i mogą korzystać z jego własności jak z własnych.
Krótka demonstracja pola prototype. Zacznijmy od prostego obiektu, który posiada pole imie i metodę mow(). var obiekt = { imie: 'Ninja', mow: function(){ return 'Jestem ' + this.imie; } }
Możesz sprawdzić, że pusta funkcja posiada pole prototype zawierające pusty obiekt. >>> function F(){} >>> typeof F.prototype
"object" Jeśli zmienisz pole prototype, zacznie się robić ciekawie. Domyślny pusty obiekt można zamienić na dowolny pusty obiekt. Przypiszmy tam zatem nasz obiekt. >>> F.prototype = obiekt;
Po tej zmianie, przy użyciu funkcji F() w roli konstruktora możesz utworzyć nowy obiekt ob, który będzie miał dostęp do pól F.prototype jak do własnych. >>> var ob = new F(); >>> ob.imie
"Ninja" >>> ob.mow()
"Jestem Ninja" Więcej o polu prototype dowiesz się z następnego rozdziału.
124
Rozdział 4. • Obiekty
Metody obiektu Function Obiekty będące funkcjami również są potomkami obiektu Object, dlatego posiadają metody domyślne, takie jak toString(). toString() wywołana na obiekcie będącym funkcją zwróci jej kod źródłowy. >>> function myfunc(a, b, c) {return a + b + c;} >>> myfunc.toString()
"function myfunc(a, b, c) { return a + b + c; }" Jeśli spróbujesz zajrzeć w kod funkcji wbudowanych, otrzymasz niezbyt przydatny łańcuch znaków [native code]: >>> eval.toString()
"function eval() { [native code] }" Ważnymi metodami obiektów funkcyjnych są call() i apply(). Dzięki nim obiekty mogą wypożyczać metody od innych obiektów i wywoływać je jak własne. Jest to prosty i skuteczny sposób wielokrotnego wykorzystania kodu. Załóżmy, że mamy obiekt obiekt, który posiada metodę mow(): var obiekt= { imie: 'Ninja', mow: function(kto){ return 'Siema ' + kto + ', jestem ' + this.imie; } }
Możesz wywołać metodę mow(), która sięga do this.name w celu pobrania wartości własnego pola. >>> some_obj.say('stary');
"Siema stary, jestem Ninja" Utwórzmy teraz prosty obiekt moj_obiekt, który posiada jedynie pole imie: >>> mój_obiekt = {imie: 'Programistyczny guru'};
Powiedzmy, że moj_obiekt tak bardzo lubi mow(), że chce wywołać ją jako swoją własną metodę. Jest to możliwe przy użyciu metody call() obiektu funkcyjnego mow():
125
JavaScript. Programowanie obiektowe
>>> obiekt.mow.call(mój_obiekt, 'stary');
"Siema stary, jestem Programistyczny guru" Działa! Co dokładnie zaszło? Wywołaliśmy metodę call() obiektu mow(), przekazując jej dwa parametry: obiekt moj_obiekt oraz łańcuch 'stary'. W wyniku tego podczas wywołania mow() wszystkie referencje do wartości this wskazywały mój_obiekt. Dzięki temu this.name nie zwróciło 'Ninja', tylko 'Guru programistyczny'. Jeśli dana funkcja pobiera więcej argumentów, po prostu je wymieniamy: obiekt.metoda.call(mój_obiekt, 'a', 'b', 'c');
Jeśli jako pierwszy parametr call() nie zostanie przekazany obiekt lub jeśli przekaże się null, funkcja zostanie wywołana na rzecz obiektu globalnego. Metoda apply() działa tak jak call(), z tą różnicą, że wszystkie parametry przekazywane metodzie innego obiektu umieszcza się w tablicy. Poniższe dwie linie są równoważne: obiekt.metoda.apply(moj_obiekt, ['a', 'b', 'c']); obiekt.metoda.call(mój_obiekt, 'a', 'b', 'c');
Kontynuując powyższy przykład, możesz spróbować wykonać następujący kod: >>> obiekt.metoda.apply(mój obiekt, [stary']);
"Siema stary, jestem Programistyczny guru"
Nowe spojrzenie na obiekt arguments W poprzednim rozdziale pokazałem, jak z wnętrza funkcji uzyskać dostęp do zmiennej o nazwie arguments, która przechowuje wartości wszystkich parametrów użytych podczas wywołania funkcji: >>> function f() {return arguments;} >>> f(1,2,3)
[1, 2, 3] arguments wygląda jak tablica, jednak w rzeczywistości jest to obiekt tablicopodobny. Przypomina tablicę, ponieważ posiada indeksowane elementy oraz pole length. Na tym jednak podobieństwa się kończą — arguments nie posiada metod tablicowych, takich jak sort() czy slice().
Obiekt ten posiada jednak inną ciekawą własność: pole callee. Zawiera ono referencję do aktualnie wywoływanej funkcji. Jeśli utworzysz i wywołasz funkcję zwracającą arguments.callee, zwracaną wartością będzie referencja do tej samej funkcji. >>> function f(){return arguments.callee;} >>> f()
f() 126
Rozdział 4. • Obiekty
Dzięki arguments.callee funkcje anonimowe mogą rekurencyjnie wywoływać same siebie. Oto przykład: ( function(count){ if (count < 5) { alert(count); arguments.callee(++count); } } )(1)
Widać tu funkcję anonimową, która pobiera parametr count, wyświetla go, a następnie wywołuje samą siebie ze zwiększoną wartością count. Cała funkcja została umieszczona w nawiasie, po którym następuje pusty nawias powodujący, że funkcja od razu jest wykonywana z wartością początkową 1. Uruchomienie kodu spowoduje wyświetlenie czterech okienek dialogowych, prezentujących liczby 1, 2, 3 i 4.
Boolean Kontynuując naszą podróż przez wbudowane obiekty JavaScriptu, dochodzimy do mało skomplikowanej grupy, w której skład wchodzą obiekty opakowujące proste typy danych (typ logiczny boolean, liczba, łańcuch znaków). Typ boolean po raz pierwszy pojawił się w rozdziale 2. Teraz przyszła pora na spotkanie z konstruktorem Boolean(): >>> var b = new Boolean();
Zmiennej b zostanie przypisany nowy obiekt, a nie prosta wartość typu boolean. Do właściwej wartości przechowywanej wewnątrz obiektu można dostać się za pomocą metody valueOf(), odziedziczonej z Object. >>> var b = new Boolean(); >>> typeof b
"object" >>> typeof b.valueOf()
"boolean" >>> b.valueOf()
false Obiekty utworzone za pomocą konstruktora Boolean() nie są specjalnie przydatne, jako że obiekt ten zawiera jedynie odziedziczone metody.
127
JavaScript. Programowanie obiektowe
Inaczej ma się sprawa z Boolean() wywoływanym jako normalna funkcja, a nie konstruktor — czyli bez użycia new. W ten sposób można zamienić na typ logiczny wartość należącą do innego typu danych (odpowiada to zastosowaniu na zmiennej podwójnej negacji, jak w przypadku !!wartosc). >>> Boolean("test")
true >>> Boolean("")
false >>> Boolean({})
true Poza sześcioma fałszywymi wartościami wszystko, w tym obiekty puste, zostanie uznane za prawdziwe. Oznacza to także, że wszystkie obiekty utworzone za pomocą konstruktora Boolean() zwrócą wartość true, ponieważ są obiektami. Utwórzmy dwa obiekty Boolean, jeden przechowujący wartość true, a drugi false: >>> var b1 = new Boolean(true) >>> b1.valueOf()
true >>> var b2 = new Boolean(false) >>> b2.valueOf()
false Teraz zamieńmy je na prosty typ boolean. W obu przypadkach otrzymamy wartość true, ponieważ obiekty te nie należą do zbioru sześciu fałszywych wartości. >>> Boolean(b1)
true >>> Boolean(b2)
true
Number Funkcji Number() używa się podobnie jak Boolean(): Q jako normalnej funkcji, zamieniającej dowolną wartość na liczbę (podobnej do parseInt() i parseFloat()); Q jako konstruktora, za pomocą którego (i przy użyciu operatora new) tworzymy nowe
obiekty. 128
Rozdział 4. • Obiekty
>>> var n = Number('12.12'); >>> n
12.12 >>> typeof n
"number" >>> var n = new Number('12.12'); >>> typeof n
"object" Skoro funkcje są obiektami, mogą posiadać pola i metody. Funkcja Number() ma kilka ciekawych pól wbudowanych (których wartości nie można zmieniać): >>> Number.MAX_VALUE
1.7976931348623157e+308 >>> Number.MIN_VALUE
5e-324 >>> Number.POSITIVE_INFINITY
Infinity >>> Number.NEGATIVE_INFINITY
-Infinity >>> Number.NaN
NaN Obiekt liczbowy posiada trzy metody: toFixed(), toPrecision() i toExponential() (szczegóły w dodatku C). >>> var n = new Number(123.456) >>> n.toFixed(1)
"123.5" Warto wiedzieć, że do korzystania z tych metod nie jest konieczne jawne utworzenie obiektu liczbowego. Można wywołać je na rzecz wartości liczbowej, która zostanie automatycznie zamieniona na obiekt (usuwany po zakończeniu obliczeń). >>> (12345).toExponential()
"1.2345e+4" Jak wszystkie inne obiekty, obiekty liczbowe również posiadają metodę toString(). W ich wypadku można jednak podać opcjonalny drugi parametr, określający podstawę (domyślnie 10).
129
JavaScript. Programowanie obiektowe
>>> var n = new Number(255); >>> n.toString();
"255" >>> n.toString(10);
"255" >>> n.toString(16);
"ff" >>> (3).toString(2);
"11" >>> (3).toString(10);
"3"
String Konstruktor String() służy do tworzenia obiektów przechowujących łańcuchy znaków. Oferują one szereg metod ułatwiających przetwarzanie tekstu. Jeśli jednak nie planujesz z nich korzystać, wygodniej Ci będzie korzystać z prostego typu danych. Poniższy przykład ilustruje różnicę pomiędzy obiektem a prostym typem danych przechowującym tekst. >>> var primitive = 'Halo!'; >>> typeof primitive;
"string" >>> var obj = new String('world'); >>> typeof obj;
"object" Obiekt przechowujący łańcuch jest bardzo podobny do tablicy znaków: umożliwia odwoływanie się do poszczególnych pól za pomocą indeksów i posiada pole length, określające długość łańcucha: >>> obj[0]
"w" >>> obj[4]
"d" >>> obj.length
5 130
Rozdział 4. • Obiekty
Łańcuch znaków w postaci prostego typu danych można wydobyć z obiektu za pomocą metod valueOf() lub toString(), odziedziczonych z Object. Prawdopodobnie nigdy nie skorzystasz z tego sposobu, ponieważ metoda toString() jest automatycznie wywoływana za każdym razem, gdy obiekt zostanie użyty w kontekście tekstowym. >>> obj.valueOf()
"world" >>> obj.toString()
"world" >>> obj + ""
"world" Sam łańcuch znaków nie jest obiektem i nie posiada żadnych pól ani metod. JavaScript pozwala jednak traktować proste łańcuchy jak obiekty. W poniższym przykładzie widzimy automatyczną zamianę łańcucha na odpowiedni obiekt: >>> "ziemniak".length
8 >>> "pomidor"[0]
"p" >>> "ziemniak"["ziemniak".length - 1]
"k" Ostatni przykład prezentujący różnicę pomiędzy obiektem przechowującym łańcuch znaków a łańcuchem będącym prostym typem danych: zamieńmy oba na boolean. Pusty łańcuch jest wartością fałszywą, natomiast wszystkie obiekty zostaną zamienione na true. >>> Boolean("")
false >>> Boolean(new String(""))
true Podobnie jak w przypadku Number() i Boolean(), funkcja String() użyta bez operatora new zamieni parametr na typ prosty. Jeśli wartość wejściowa będzie obiektem, zostanie wywołana funkcja toString(). >>> String(1)
"1"
131
JavaScript. Programowanie obiektowe
>>> String({p: 1})
"[object Object]" >>> String([1,2,3])
"1,2,3"
Ciekawe metody obiektu String Poeksperymentujmy sobie z metodami, które można wywołać na obiektach reprezentujących łańcuchy znaków (pełna lista tych metod znajduje się w dodatku C). Zacznijmy od utworzenia obiektu: >>> var s = new String("Guru programowania");
Metody toUpperCase() i toLowerCase() pozwalają zmienić wielkość liter: >>> s.toUpperCase()
"GURU PROGRAMOWANIA" >>> s.toLowerCase()
"guru programowania" Metoda charAt() zwraca znak znajdujący się na określonej pozycji (ten sam efekt da użycie nawiasu kwadratowego, ponieważ obiekt przechowujący łańcuch znaków przypomina tablicę znaków). >>> s.charAt(0);
"G" >>> s[0]
"G" Odwołanie się do nieistniejącej pozycji zwróci pusty łańcuch: >>> s.charAt(101)
"" Metoda indexof() pozwala na przeszukiwanie łańcuchów. Jeśli istnieje chociaż jedno dopasowanie, zwracana jest pozycja pierwszego z nich. Pozycje znaków liczone są od 0. Trzecim znakiem w wyrazie "Guru" (czyli znakiem na pozycji 2.) jest "r". >>> s.indexOf('r')
2
132
Rozdział 4. • Obiekty
Można również określić pozycję, od której ma się rozpocząć wyszukiwanie. Poniższy fragment kodu znajdzie drugie wystąpienie litery "r", ponieważ rozpocznie sprawdzanie łańcucha od pozycji 5.: >>> s.indexOf('r', 5)
6 Metoda lastIndexOf() rozpoczyna wyszukiwanie od końca łańcucha (co nie zmienia faktu, że pozycja dopasowania jest liczona od początku łańcucha): >>> s.lastIndexOf('r')
9 Podczas wyszukiwania rozróżniane są wielkie i małe litery. Możliwe jest wyszukiwanie całych łańcuchów, a nie tylko znaków: >>> s.indexOf('Guru')
0 Jeśli fragment nie zostanie znaleziony, zwracana jest wartość -1: >>> s.indexOf('guru')
-1 Jeśli chcesz pominąć kwestię wielkości liter, możesz przed rozpoczęciem wyszukiwania zamienić wszystkie litery na małe: >>> s.toLowerCase().indexOf('guru')
0 Wartość 0 oznacza, że wynik wyszukiwania znajduje się na początku łańcucha. Może to prowadzić do nieporozumień podczas korzystania z if, ponieważ wartości 0 odpowiada wartość logiczna false. Prowadzi to do zachowań nieco sprzecznych z logiką: if (s.indexOf('Guru')) {...}
Bezpiecznym sposobem sprawdzenia, czy tekst zawiera inny tekst, jest porównanie wyniku zwracanego przez indexOf() z liczbą -1. if (s.indexOf('Guru') !== -1) {...}
Metody slice() i substring() zwracają fragment łańcucha ograniczony za pomocą argumentów rozumianych jako pozycje początkowa i końcowa. >>> s.slice(5, 12)
"program"
133
JavaScript. Programowanie obiektowe
>>> s.substring(5, 12)
"program" Należy pamiętać, że drugi parametr to pozycja końcowa, a nie długość fragment łańcucha. Pokazane powyżej dwie metody różnią się sposobem interpretacji argumentów ujemnych. substring() potraktuje je jak zera, podczas gdy slice() doda je do długości łańcucha. Zatem przekazanie wartości (1, -1) zostanie zrozumiane jako substring(1, 0) i slice(1, s.length-1): >>> s.slice(1, -1)
"uru programowani" >>> s.substring(1, -1)
"G" Metoda split() zamienia obiekt na tablicę, traktując parametr jako wartość rozdzielającą: >>> s.split(" ")
["Guru", "programowania"] Przeciwieństwem split() jest metoda join(), która zamienia tablicę w obiekt typu String: >>> s.split(' ').join(' ');
"Guru programowania" Metoda concat() skleja łańcuchy, podobnie jak operator + dla typu prostego: >>> s.concat(" w języku JavaScript")
"Guru programowania w języku JavaScript" Zwróć uwagę na to, że wszystkie omawiane powyżej metody związane z łańcuchami zwracają wartości proste i nie modyfikują obiektu źródłowego. Po wywołaniu wszystkich tych metod łańcuch znaków przechowywany przez obiekt nie uległ zmianie: >>> s.valueOf()
"Guru programowania" Do przeszukiwania łańcuchów używaliśmy indexOf() oraz lastIndexOf(). Istnieją bardziej zaawansowane metody (search(), match() i replace()), które jako parametry pobierają wyrażenia regularne. Opowiem o nich później, gdy dojdziemy do konstruktora RegExp(). To już wszystkie obiekty opakowujące dane. Zajmiemy się teraz użytkowymi obiektami Math, Date i RegExp.
134
Rozdział 4. • Obiekty
Math Math różni się nieco od innych wbudowanych obiektów globalnych. Jest zwykłą funkcją i w związku z tym nie może być używana do tworzenia nowych obiektów. Math to wbudowany obiekt
globalny, który dostarcza szeregu metod i pól ułatwiających wykonywanie operacji matematycznych. Pola i metody Math są stałymi — nie można zmieniać ich wartości. Ich nazwy pisane są wielkimi literami w celu odróżnienia ich od zwykłych pól będących zmiennymi. Przyjrzyjmy się niektórym spośród tych stałych: Liczba π: >>> Math.PI
3.141592653589793 Pierwiastek kwadratowy z 2: >>> Math.SQRT2
1.4142135623730951 Liczba Eulera e: >>> Math.E
2.718281828459045 Logarytm naturalny z 2: >>> Math.LN2
0.6931471805599453 Logarytm naturalny z 10: >>> Math.LN10
2.302585092994046 Nareszcie wiesz, jak zaimponować przyjaciołom, gdy któryś z nich (nieważne z jakiego dziwnego powodu) zacznie się zastanawiać, jaka jest wartość liczby e — wystarczy, że wpiszesz w konsoli Math.E, i od razu otrzymasz odpowiedź. Popatrzmy teraz na metody obiektu Math (ich pełna lista znajduje się w dodatku C). Losowanie liczb: >>> Math.random()
0.3649461670235814
135
JavaScript. Programowanie obiektowe
Metoda random() zwraca liczbę pomiędzy 0 a 1. Jeśli potrzebna jest Ci liczba z przedziału od 0 do 100, możesz wykonać następującą operację: >>> 100 * Math.random()
W celu otrzymania liczb z przedziału od wartości min do wartości max najlepiej skorzystać z formuły ((max - min) * Math.random()) + min. Przykładowo liczbę z przedziału od 2 do 10 losujemy w następujący sposób: >>> 8 * Math.random() + 2
9.175650496668485 Jeśli potrzebna jest Ci liczba całkowita, możesz skorzystać z jednej z metod zaokrąglających: floor() zaokrągla w dół, ceil() w górę, a round() do najbliższej wartości. Jeśli wynikiem ma
być 0 albo 1, stosujemy: >>> Math.round(Math.random())
Najwyższą lub najniższą liczbę ze zbioru wyznaczamy za pomocą metod min() i max(). Jeśli na stronie internetowej umieścimy formularz, w którym użytkownik powinien podać liczbę odpowiadającą miesiącowi, możemy w następujący sposób upewnić się, że wartość jest poprawna: >>> Math.min(Math.max(1, input), 12)
Obiekt Math umożliwia wykonywanie działań matematycznych, które nie posiadają własnych operatorów. Liczby można podnosić do dowolnej potęgi za pomocą metody pow(), do wyznaczania pierwiastka kwadratowego służy metoda sqrt(), wartości trygonometryczne wyznaczają sin(), cos(), atan() itp. 2 do potęgi 8: >>> Math.pow(2, 8)
256 Pierwiastek kwadratowy z 9: >>> Math.sqrt(9)
3
Date Konstruktor Date() tworzy obiekty reprezentujące daty. Jako argument funkcja ta może pobierać: Q nic (domyślnie zostanie ustawiona aktualna data); Q tekst, który da się przetłumaczyć na datę; Q osobne wartości określające dzień, miesiąc, godzinę itd.; Q znacznik czasu.
136
Rozdział 4. • Obiekty
Obiekt, któremu przypisana zostanie aktualna data i godzina: >>> new Date()
Tue Jan 08 2008 01:10:42 GMT+0100 Jak zwykle w przypadku obiektów konsola Firebug wyświetla wynik wywołania metody toString().
Poniżej przedstawiam kilka przykładów użycia łańcuchów znaków do inicjalizacji obiektu przechowującego datę. Można wybierać spomiędzy wielu różnych formatów: >>> new Date('2009 11 12')
Thu Nov 12 2009 00:00:00 GMT+0100 >>> new Date('1 1 2012')
Sun Jan 01 2012 00:00:00 GMT+0100 >>> new Date('1 mar 2012 5:30')
Thu Mar 01 2012 05:30:00 GMT+0100 JavaScript potrafi odczytać datę z łańcuchów znaków w różnym formacie, jednak nie jest to najskuteczniejszy sposób precyzyjnego definiowania daty. Lepiej jest przekazać konstruktorowi wartości liczbowe określające: Q rok; Q miesiąc: od 0 (styczeń) do 11 (grudzień); Q dzień: od 1 do 31; Q godzina: od 0 do 23; Q minuty: od 0 do 59; Q sekundy: od 0 do 59; Q milisekundy: od 0 do 999.
Spójrzmy na przykłady. Podanie wszystkich wspomnianych parametrów: >>> new Date(2008, 0, 1, 17, 05, 03, 120)
Tue Jan 01 2008 17:05:03 GMT+0100 Podanie tylko daty i godziny: >>> new Date(2008, 0, 1, 17)
Tue Jan 01 2008 17:00:00 GMT+0100
137
JavaScript. Programowanie obiektowe
Postaraj się zapamiętać, że miesiące liczone są od 0, zatem 1 oznacza luty: >>> new Date(2008, 1, 28)
Thu Feb 28 2008 00:00:00 GMT+0100 Jeśli podasz zbyt dużą wartość, zostanie ona przetłumaczona na odpowiednią datę w przyszłości. Przykładowo ponieważ w roku 2008 nie było dnia 30 lutego, taka wartość zostanie przetłumaczona na 1 marca (2008 był rokiem przestępnym). >>> new Date(2008, 1, 29)
Fri Feb 29 2008 00:00:00 GMT+0100 >>> new Date(2008, 1, 30)
Sat Mar 01 2008 00:00:00 GMT+0100 W analogiczny sposób 32 grudnia zostanie zamieniony na 1 stycznia następnego roku: >>> new Date(2008, 11, 31)
Wed Dec 31 2008 00:00:00 GMT+0100 >>> new Date(2008, 11, 32)
Thu Jan 01 2009 00:00:00 GMT+0100 Obiekt reprezentujący datę można jeszcze zainicjalizować za pomocą znacznika czasu, czyli liczby milisekund od początku ery Uniksa (gdzie 0 milisekund oznacza 1 stycznia 1970). >>> new Date(1199865795109)
Wed Jan 09 2008 00:03:15 GMT+0100 Jeśli funkcja Date() zostanie wywołana bez operatora new, zwróci łańcuch znaków reprezentujący bieżącą datę, niezależnie do tego, czy podano jakiekolwiek parametry. Poniższe wywołania zwracają aktualną (w chwili uruchomienia kodu) datę. >>> Date()
"Thu Jan 17 2008 23:11:32 GMT+0100" >>> Date(1, 2, 3, "bez znaczenia");
"Thu Jan 17 2008 23:11:35 GMT+0100"
Metody działające na obiektach Date Istnieje wiele metod, które można wywołać na obiektach Date. Większość z nich to metody dostępowe get*() (ustawiające wartość atrybutu) lub set*() (pobierające wartość). Mamy na przykład getMonth() (pobierz miesiąc), setMonth() (ustaw miesiąc), getHours() (pobierz godzinę), setHours() (ustaw godzinę) itd.
138
Rozdział 4. • Obiekty
Utwórzmy obiekt: >>> var d = new Date(); >>> d.toString();
"Wed Jan 09 2008 00:26:39 GMT+0100" Ustawienie miesiąca na marzec (miesiące liczymy od 0): >>> d.setMonth(2);
1205051199562 >>> d.toString();
"Sun Mar 09 2008 00:26:39 GMT+0100" Pobranie miesiąca: >>> d.getMonth();
2 Poza metodami należącymi do instancji obiektu Date istnieją jeszcze dwie metody będące polami funkcji/obiektu Date(). Do ich funkcjonowania nie jest potrzebna instancja danych — działają tak jak metody Math. W językach, w których istnieje pojęcie klasy, tego typu metody nazywa się statycznymi. Metoda Date.parse() zamienia tekst na znacznik czasu: >>> Date.parse('Jan 1, 2008')
1199174400000 Date.UTC() pobiera parametry określające rok, miesiąc, dzień itd. i zamienia je na znacznik czasu uniwersalnego: >>> Date.UTC(2008, 0, 1)
1199145600000 Skoro konstruktor new Date() przyjmuje znaczniki czasu, można przekazać mu wynik wywołania Date.UTC. Poniższy przykład dowodzi, że UTC() podaje czas uniwersalny, a Date() czas lokalny: >>> new Date(Date.UTC(2008, 0, 1));
Mon Dec 31 2007 16:00:00 GMT+0100 >>> new Date(2008, 0, 1);
Tue Jan 01 2008 00:00:00 GMT+0100
139
JavaScript. Programowanie obiektowe
Spójrzmy jeszcze na ostatni przykład działań na obiekcie typu Date. Ciekawiło mnie, w jaki dzień tygodnia wypadną moje urodziny w roku 2012: >>> var d = new Date(2012, 5, 20); >>> d.getDay();
3 Dni tygodnia liczymy od 0 (niedziela), zatem 3 powinno oznaczać środę: >>> d.toDateString();
"Wed Jun 20 2012" Zgadza się, będzie to środa (ang. Wednesday) — nie jest to najlepszy dzień na imprezę. W takim razie napiszę sobie pętlę, która obliczy, ile razy na przestrzeni lat od 2012 do 3012 dzień 20 czerwca wypadnie w piątek. Albo, jeszcze lepiej, sprawdzę, jak rozkładają się dni tygodnia. Zakładając obecne tempo rozwoju medycyny, w roku 3012 będziemy mogli wspólnie napić się szampana. Najpierw wypełnijmy tablicę siedmioma elementami, po jednym dla każdego dnia tygodnia. Wykorzystamy elementy jako liczniki, które będziemy zwiększać w miarę zbliżania się pętli do roku 3012. var stats = [0,0,0,0,0,0,0];
Pętla: for (var i = 2012; i < 3012; i++) { stats[new Date(i, 5, 20).getDay()]++; }
Wynik: >>> stats;
[139, 145, 139, 146, 143, 143, 145] 143 piątki i 145 sobót. Tak jest!
RegExp Wyrażenia regularne (ang. regular expressions) są niezwykle potężnym mechanizmem przeszukiwania i edycji tekstu. Jeśli znasz język SQL, możesz wyobrazić je sobie jako coś podobnego. SQL służy do wyszukiwania i aktualizacji danych w bazie, natomiast wyrażenia regularne pozwalają przeszukiwać i zmieniać fragmenty tekstu.
140
Rozdział 4. • Obiekty
Różne języki stosują różne implementacje wyrażeń regularnych (możesz o nich myśleć jako o dialektach). JavaScript stosuje składnię odpowiadającą językowi Perl 5. Wyrażenia regularne w skrócie określa się mianem „regex” lub „regexp”. Wyrażenie regularne składa się z: Q wzorca, do którego ma zostać dopasowany tekst; Q nieobowiązkowych modyfikatorów (nazywanych także flagami), które wpływają na
sposób stosowania wzorca. Wzorzec może być zwykłym fragmentem tekstu, który ma zostać dokładnie dopasowany, ale takie zastosowania wyrażeń regularnych spotyka się rzadko, tym bardziej że do osiągnięcia tego celu wystarczy zastosować indexOf(). W większości przypadków wzorzec jest dość złożony i czasem trudny do zrozumienia. Opanowanie wzorców wyrażeń regularnych nie jest proste, dlatego nie będę omawiał ich szczegółowo. Zamiast tego pokażę, jak składnia, obiekty i metody JavaScriptu ułatwiają korzystanie z wyrażeń regularnych. Dodatkowe informacje na temat wzorców można znaleźć w dodatku D. Konstruktor RegExp() pozwala tworzyć obiekty reprezentujące wyrażenia regularne. >>> var re = new RegExp("j.*t");
Obiekty te można w nieco wygodniejszy sposób tworzyć za pomocą literałów: >>> var re = /j.*t/; j.*t w powyższym przykładzie jest wzorcem wyrażenia regularnego. Oznacza „znajdź takie łańcuchy, które zaczynają się od j i kończą się na t, a pomiędzy nimi występuje 0 lub więcej dowolnych znaków”. Kropka (.) oznacza dowolny znak. Jeśli wzorzec jest przekazywany konstruktorowi RegExp(), należy umieścić go w cudzysłowie.
Pola obiektów RegExp Obiekty reprezentujące wyrażenia regularne posiadają następujące pola: Q global: jeśli to pole ma wartość false (domyślną), w wyniku wyszukiwania zwrócony zostanie tylko pierwszy odnaleziony wynik. Jeśli chcesz otrzymać wszystkie dopasowania, zmień wartość pola na true. Q ignoreCase: true oznacza, że nie są rozróżniane wielkie i małe litery. Wartość domyślna tego pola to false. Q multiline: wartość true pozwala na wyszukiwanie dopasowań, które zajmują więcej niż jedną linię. Wartością domyślną jest false. Q lastIndex: pozycja, od której ma się rozpocząć wyszukiwanie — domyślnie 0. Q source: przechowuje wzorzec wyrażenia regularnego.
141
JavaScript. Programowanie obiektowe
Pole lastIndex jest jedynym, którego wartość można zmieniać po utworzeniu obiektu. Pierwsze trzy parametry to modyfikatory wyrażenia regularnego. Podczas tworzenia obiektu wyrażenia za pomocą konstruktora można jako drugi parametr przekazać dowolną kombinację poniższych znaków: Q "g" dla global, Q "i" dla ignoreCase, Q "m" dla multiline.
Kolejność liter nie ma znaczenia. Przekazanie danej litery powoduje ustawienie wartości powiązanego z nią modyfikatora na true. W poniższym przykładzie wszystkie modyfikatory otrzymują wartość true: >>> var re = new RegExp('j.*t', 'gmi');
Sprawdźmy: >>> re.global;
true Po utworzeniu obiektu nie można już zmienić wartości modyfikatora: >>> re.global = false; >>> re.global
true Jeśli obiekt jest tworzony za pomocą literału, modyfikatory podaje się po końcowym ukośniku. >>> var re = /j.*t/ig; >>> re.global
true
Metody obiektów RegExp Obiekty RegExp oferują dwie metody służące do znajdowania fragmentów tekstu: test() i exec(). Obie przyjmują parametr tekstowy. test() zwraca wartość logiczną (true, jeśli znaleziono dopasowanie, i false w przeciwnym przypadku), natomiast exec() — tablicę dopasowanych łańcuchów znaków. Oczywiście exec() wykonuje bardziej skomplikowane obliczenia, dlatego o ile nie są Ci potrzebne konkretne dopasowania, korzystaj z test(). Najczęstsze zastosowanie wyrażeń regularnych to walidacja formularzy — do tego test w zupełności wystarczy. Brak dopasowania z powodu różnicy w wielkości liter: >>> /j.*t/.test("Javascript")
false
142
Rozdział 4. • Obiekty
Ustawienie wartości ignoreCase na true powoduje, że uda się odnaleźć pasujący tekst: >>> /j.*t/i.test("Javascript")
true To samo zapytanie zadane za pomocą exec() zwraca tablicę. Sprawdźmy wartość jej pierwszego elementu: >>> /j.*t/i.exec("Javascript")[0]
"Javascript"
Metody obiektu String, których parametrami mogą być wyrażenia regularne Wcześniej w tym rozdziale opowiadałem o obiekcie String i o wykorzystaniu jego metod indexOf() i lastIndexOf() do przeszukiwania tekstu. Przy ich użyciu można odnajdować w tekście fragmenty przekazane w postaci parametrów. Więcej możliwości daje przeszukiwanie tekstu przy użyciu wyrażeń regularnych. Obiekty String umożliwiają również to. Obiekty tekstowe posiadają następujące metody przyjmujące jako parametry wyrażenia regularne: Q match() zwraca tablicę dopasowań. Q search() zwraca pozycję pierwszego dopasowania. Q replace() pozwala zamienić dopasowany tekst na inny. Q split() potrafi dzielić tekst na tablicę elementów, także w oparciu o wyrażenie
regularne.
search() i match() Poeksperymentujmy trochę z metodami search() i match(). Zacznijmy od utworzenia nowego obiektu tekstowego. >>> var s = new String('HelloJavaScriptWorld'); match() zwróci tablicę zawierającą tylko pierwsze dopasowanie: >>> s.match(/a/);
["a"] Jeśli skorzystamy z modyfikatora g, wyszukiwanie będzie miało zasięg globalny i otrzymamy tablicę zawierającą dwa elementy: >>> s.match(/a/g);
["a", "a"]
143
JavaScript. Programowanie obiektowe
Pominięcie różnic w wielkości liter: >>> s.match(/j.*a/i);
["Java"] Metoda search() zwraca pozycję dopasowanego łańcucha: >>> s.search(/j.*a/i);
5
replace() replace() umożliwia zamianę dopasowanego tekstu na inny. W poniższym przykładzie przy użyciu tej metody usuwam z tekstu wszystkie wielkie litery (zamieniając je na pusty łańcuch): >>> s.replace(/[A-Z]/g, '');
"elloavacriptorld" Jeśli nie zostanie użyty modyfikator g, zmieni się tylko pierwszy znaleziony fragment: >>> s.replace(/[A-Z]/, '');
"elloJavaScriptWorld" Jeśli chcesz wykorzystać odnaleziony tekst jako fragment podstawienia, dostęp do niego uzyskasz za pomocą sekwencji $&. Poniższy fragment kodu poprzedza dopasowany tekst znakiem podkreślnika: >>> s.replace(/[A-Z]/g, "_$&");
"_Hello_Java_Script_World" Jeśli wyrażenie regularne zawiera grupy (oznaczone nawiasami), do poszczególnych grup można się dostać dzięki sekwencjom: $1 dla pierwszej grupy, $2 dla drugiej itd. >>> s.replace(/([A-Z])/g, "_$1");
"_Hello_Java_Script_World" Wyobraź sobie, że na Twojej stronie znajduje się formularz, w który użytkownik powinien wpisać swój adres e-mail, nazwę użytkownika i hasło. Gdy tylko wpisze e-mail, nasz skrypt zasugeruje nazwę użytkownika w oparciu o ten adres: >>> var email = "
[email protected]"; >>> var nazwa_uzytkownika = email.replace(/(.*)@.*/, "$1"); >>> nazwa_uzytkownika;
"stoyan"
144
Rozdział 4. • Obiekty
Wywołania zwrotne replace Podczas zamiany fragmentów tekstu na inne można zamiast konkretnego tekstu podać funkcję zwracającą łańcuch, która w odpowiedni sposób przetworzy odnaleziony tekst. >>> function replaceCallback(match){return "_" + match.toLowerCase();} >>> s.replace(/[A-Z]/g, replaceCallback);
"_hello_java_script_world" W rzeczywistości funkcja otrzymuje kilka parametrów (w powyższym przykładzie zignorowaliśmy wszystkie oprócz pierwszego): Q Pierwszy parametr to dopasowany tekst. Q Drugi to przeszukiwany łańcuch. Q Przedostatni informuje o pozycji dopasowania. Q Pozostałe parametry (jeśli jest ich więcej niż jeden, część z nich pojawi się w tablicy
przed informacją o pozycji dopasowania) zawierają fragmenty dopasowane do poszczególnych grup z wzorca. Przetestujmy to. Po pierwsze, utwórzmy zmienną, która będzie przechowywała tablicę argumentów przekazanych podczas wywołania funkcji: >>> var glob;
Następnie zdefiniujmy wyrażenie regularne z trzema grupami, które ma pasować do adresów e-mail postaci
[email protected]: >>> var re = /(.*)@(.*)\.(.*)/;
Teraz napiszmy funkcję, która przechowa wartości i zwróci postać adresu nieczytelną dla botów: var callback = function(){ glob = arguments; return arguments[1] + ' na serwerze: ' + arguments[2] + ' kropka ' + arguments[3]; }
Działanie funkcji jest następujące: >>> "
[email protected]".replace(re, callback);
"stoyan na serwerze phpied kropka com" Oto argumenty, które odebrała funkcja: >>> glob
["
[email protected]", "stoyan", "phpied", "com", 0, "
[email protected]"]
145
JavaScript. Programowanie obiektowe
split() Zapoznałem Cię już z metodą split(), która tworzy tablicę w oparciu o tekst wejściowy i łańcuch znaków pełniący funkcję separatora. Rozetnijmy łańcuch składający się z wartości oddzielonych przecinkami: >>> var csv = 'raz, dwa,trzy ,cztery'; >>> csv.split(',');
["raz", " dwa", "trzy ", "cztery"] Ponieważ w wejściowym tekście spacje nie są stosowane konsekwentnie, w wyniku podziału otrzymaliśmy tablicę, która także zawiera spacje. Można to naprawić, stosując wyrażenie \s*, które oznacza „zero lub więcej spacji”: >>> csv.split(/\s*,\s*/)
["raz", "dwa", "trzy", "cztery"]
Przekazanie zwykłego tekstu zamiast wyrażenia regularnego Warto zapamiętać, że omówione przed chwilą cztery metody (split(), match(), search() i replace()) mogą zamiast wyrażeń regularnych pobierać zwykły tekst. W takim wypadku argument zostanie wykorzystany do utworzenia nowego obiektu wyrażenia regularnego, tak jakby został przekazany do konstruktora RegExp(). Przykład: >>> "test".replace('e', 'o')
"tost" Powyższe wywołanie jest równoważne: >>> "test".replace(new RegExp('e'), 'o')
"tost" Jeśli parametr jest łańcuchem, nie można ustawić wartości modyfikatorów. Na określenie ich wartości pozwala konstruktor RegExp() oraz notacja literałowa.
Obsługa błędów za pomocą obiektów Error Nie da się całkowicie wyeliminować błędów, dlatego potrzebny jest mechanizm ich wykrywania, dzięki któremu program będzie mógł wykryć, że coś poszło nie tak, i w elegancki sposób odzyskać sprawność. Do obsługi błędów w języku JavaScript służą instrukcje try, catch i finally. Kiedy pojawia się błąd, „rzucany” jest obiekt błędu. Obiekty te tworzy się za pomocą wbudowanych
146
Rozdział 4. • Obiekty
konstruktorów: EvalError, RangeError, ReferenceError, SyntaxError, TypeError i URIError3. Wszystkie konstruktory dziedziczą z obiektu Error. Spowodujmy błąd i zobaczmy, co się stanie. Nasz przykład będzie próbował wywołać nieistniejącą funkcję. Wpisz w konsoli Firebug następujący kod: >>> nieMaMnie ();
Wynik będzie mniej więcej taki:
Jeśli strona zawiera błąd, w prawym dolnym rogu przeglądarki zamiast normalnej ikonki Firebug pojawi się:
Informacje o błędach można przeglądać w konsoli błędów (Narzędzia/Konsola błędów):
Sposób wyświetlania informacji o błędach jest różny w różnych przeglądarkach. Internet Explorer w lewym dolnym rogu wyświetla następujący komunikat:
Po dwukrotnym kliknięciu otrzymamy więcej informacji (patrz rysunek na następnej stronie). W zależności od konfiguracji przeglądarki możesz nawet nie zauważyć, że wystąpił błąd. Nigdy jednak nie będziesz mieć pewności, że wszyscy użytkownicy wyłączyli informowanie o błędach. Uwolnienie ich od konieczności oglądania komunikatów o błędach na Twojej stronie należy tylko do Ciebie. Błąd z naszego przykładu został wyświetlony użytkownikowi, ponieważ kod nie próbował go „przechwycić” i nie był przygotowany na jego obsługę. Na szczęście łapanie błędów jest naprawdę proste. Potrzeba do tego instrukcji try („spróbuj”), po której nastąpi instrukcja catch („przechwyć”). 3
Odpowiednio: błąd wykonania, błąd zakresu, błąd referencji, błąd składniowy, błąd typu, błąd adresu URI — przyp. tłum.
147
JavaScript. Programowanie obiektowe
Poniższy kod nie spowoduje wystąpienia błędów: try { nieMaMnie(); } catch (e){ // nic nie rób }
Mamy tu: Q Instrukcję try, po której następuje blok kodu. Q Instrukcję catch, po której następuje nazwa zmiennej w nawiasie i kolejny blok kodu.
Istnieje jeszcze nieobowiązkowa instrukcja finally. Towarzyszący jej blok kodu jest wykonywany niezależnie od tego, czy wystąpił błąd. W powyższym przykładzie w żaden sposób nie naprawiamy błędu. Blok następujący po catch jest miejscem, w którym możemy wprowadzić konieczne poprawki lub poinformować użytkownika, że zaszły nieoczekiwane okoliczności. Zmienna e w nawiasie po słowie catch przechowuje obiekt błędu. Jak wszystkie inne obiekty, zawiera on pewne przydatne pola i metody. Niestety różne przeglądarki implementują je na różne sposoby, ale istnieją dwa pola, które występują w każdej wersji. Są to e.name (nazwa) i e.message (komunikat). Uruchom teraz następujący kod: try { nieMaMnie(); } catch (e){ alert(e.name + ': ' + e.message); } finally { alert('Wreszcie!'); }
148
Rozdział 4. • Obiekty
Pojawi się okienko alert() pokazujące nazwę błędu i komunikat, a potem drugie okienko o treści „Wreszcie!”. W Firefoksie pierwsze okienko wyświetli tekst ReferenceError: nieMaMnie is not defined. W Internet Explorerze będzie to TypeError: Oczekiwano obiektu. Na tej podstawie możemy wywnioskować dwa fakty: Q e.name przechowuje nazwę konstruktora, który został użyty podczas tworzenia obiektu błędu. Q Skoro w różnych przeglądarkach ten sam błąd w kodzie jest wiązany z różnymi obiektami błędów, nie jest dobrym pomysłem podejmowanie decyzji na temat zachowania kodu na podstawie typu błędu (tzn. wartości e.name). Nowe obiekty błędów można tworzyć samodzielnie za pomocą konstruktora new Error() lub dowolnego z dziedziczących z niego konstruktorów. Wystąpienie nowego błędu w kodzie sygnalizujemy za pomocą instrukcji throw („rzuć”). Niech nasz kod wywołuje funkcję mozeIstnieje(), a następnie wykonuje pewne obliczenia. Chcemy w konsekwentny sposób przechwycić wszystkie błędy, niezależnie od tego, czy zostały one spowodowane tym, że nie istnieje funkcja mozeIstnieje(), czy niedozwoloną operacją podczas obliczeń. Oto kod: try { var total = mozeIstnieje(); if (total === 0) { throw new Error('Dzielenie przez zero!'); } else { alert(50 / total); } } catch (e){ alert(e.name + ': ' + e.message); } finally { alert('Wreszcie!'); }
Kod zachowa się inaczej w zależności od tego, czy istnieje funkcja mozeIstnieje(), i od zwracanych przez nią wartości: Q Jeśli mozeIstnieje() nie istnieje, wyświetlony zostanie komunikat ReferenceError:
mozeIstnieje is not defined (w Firefoksie) lub TypeError: Oczekiwano obiektu (w IE). Q Jeśli mozeIstnieje() zwraca 0, wystąpi błąd Dzielenie przez zero!. Q Jeśli mozeIstnieje() zwraca 2, w okienku pojawi się tekst 25.
Niezależnie od istnienia funkcji mozeIstnieje() i od zwracanej przez nią wartości, na końcu pojawi się okno dialogowe z komunikatem Wreszcie!.
149
JavaScript. Programowanie obiektowe
Zamiast rzucania ogólnego błędu throw new Error('Dzielenie przez zero! ') możesz zdecydować się na większą drobiazgowość i rzucić na przykład błąd zakresu throw new RangeError ´('Dzielenie przez zero! '). Możesz także zrezygnować z konstruktora i rzucić zwykły obiekt: throw { name: "MójBłąd", message: "O rany! Stało się coś strasznego" }
Podsumowanie W rozdziale 2. przedstawiłem pięć prostych typów danych (liczba, łańcuch znaków, boolean, null i undefined). Napisałem także, że wszystko, co nie należy do typu prostego, jest obiektem. Teraz wiesz również, że: Q Obiekty są podobne do tablic, ale sami określamy klucze. Q Obiekty posiadają pola. Q Niektóre spośród pól są funkcjami (funkcje to dane, var f = function(){};).
Takie pola nazywa się metodami. Q Tablice to obiekty, które posiadają predefiniowane pola o nazwach będących liczbami oraz pole length. Q Obiekty tablicowe posiadają wiele użytecznych metod, takich jak sort() czy slice(). Q Funkcje także są obiektami posiadającymi pola (takie jak długość i prototyp) i metody (takie jak call() i apply()).
Spośród pięciu prostych typów danych wszystkim oprócz undefined (który reprezentuje wartość pustą) i null (który także jest obiektem) odpowiadają konstruktory: Number(), String() i Boolean(). Przy ich użyciu tworzy się tak zwane obiekty opakowujące, posiadające dodatkowe funkcje ułatwiające pracę z prostymi typami danych. Number(), String() i Boolean() można wywołać: Q z operatorem new — w celu utworzenia nowego obiektu; Q bez new — w celu przekształcenia dowolnej wartości na odpowiadającą jej wartość
prostą. Inne omówione w tym rozdziale wbudowane konstruktory to: Object(), Array(), Function(), Date(), RegExp()i Error(). Opisałem także funkcję Math, która jednak nie jest konstruktorem. Wiesz już, że obiekty odgrywają w języku JavaScript podstawową rolę. Prawie wszystko albo jest obiektem, albo może zostać opakowane w obiekt.
150
Rozdział 4. • Obiekty
Podsumujmy jeszcze sposoby tworzenia obiektów za pomocą notacji literałowej: Nazwa
Literał
Konstruktor
Przykład
Object
{}
new Object()
{prop:1}
Array
[]
new Array()
[1,2,3,'test']
wyrażenie regularne
/wzorzec/modyfikatory
new RegExp('wzorzec', 'modyfikatory') /java.*/img
Ćwiczenia 1. Do którego z obiektów (globalnego czy obiektu o) odnosi się wartość this w poniższym kodzie? function F() { function C() { return this; } return C(); } var o = new F();
2. Jaki będzie wynik wykonania poniższego fragmentu kodu? function C(){ this.a = 1; return false; } console.log(typeof new C());
3. A jaki będzie wynik wykonania tego fragmentu? >>> >>> >>> >>>
c = [1,. 2, [1, 2]]; c.sort(); c.join('--'); console.log(c);
4. Wyobraź sobie, że nie istnieje konstruktor String(). Utwórz konstruktor MojString(), którego działanie będzie tak bliskie działaniu String(), jak to tylko możliwe. Nie wolno Ci używać wbudowanych pól i metod obiektu String i pamiętaj, że nie istnieje String(). Sprawdź działanie kodu przy użyciu następującego testu: >>> var s = new MojString('hello'); >>> s.length;
5 >>> s[0];
"h"
151
JavaScript. Programowanie obiektowe
>>> s.toString();
"hello" >>> s.valueOf();
"hello" >>> s.charAt(1);
"e" >>> s.charAt('2');
"l" >>> s.charAt('e');
"h" >>> s.concat(' world!');
"hello world!" >>> s.slice(1,3);
"el" >>> s.slice(0,-1);
"hell" >>> s.split('e');
["h", "llo"] >>> s.split('l');
["he", "", "o"] Możesz przejść przez wszystkie znaki łańcucha wejściowego za pomocą pętli for…in, traktując go jak tablicę.
5. Dodaj do konstruktora MojString() metodę reverse() („odwróć”). Wykorzystaj fakt, że tablice posiadają metodę reverse().
6. Wyobraź sobie, że nie istnieje ani Array(), ani literałowy sposób tworzenia tablic. Napisz konstruktor MojArray(), który zachowuje się w prawie taki sam sposób jak Array(). Przeprowadź następujące testy: >>> var a = new MojArray(1,2,3,"test"); >>> a.toString();
"1,2,3,test"
152
Rozdział 4. • Obiekty
>>> a.length;
4 >>> a[a.length - 1]
"test" >>> a.push('boo');
5 >>> a.toString();
"1,2,3,test,boo" >>> a.pop();
[1, 2, 3, "test"] >>> a.toString();
"1,2,3,test" >>> a.join(',')
"1,2,3,test" >>> a.join(' to nie ')
"1 to nie 2 to nie 3 to nie test" Jeśli podoba Ci się to ćwiczenie, nie poprzestawaj na join(), ale zaimplementuj również inne metody. 7. Wyobraź sobie, że nie istnieje Math. Utwórz obiekt MojMath, który posiada następujące metody: Q MojMath.rand(min, max, wlacznie) — losuje liczbę z przedziału od min do max, włącznie, jeśli wlacznie ma wartość true. Q
MojMath.min(tablica) — zwraca najmniejszy element tablicy.
Q
MojMath.max(tablica) — zwraca największy element tablicy.
153
JavaScript. Programowanie obiektowe
154
5 Prototypy W tym rozdziale omówię pole prototype obiektów funkcyjnych. JavaScript jest prototypowym językiem obiektowym, więc zrozumienie idei prototypów jest bardzo ważne. Sam prototyp nie jest niczym specjalnie skomplikowanym, jednak jest to nowe pojęcie, z którym po prostu trzeba się oswoić. Prototypy to kolejny (po domknięciach) element JavaScriptu, który — kiedy już uda się z nim zapoznać — wydaje się oczywisty i niezastąpiony. Jak zwykle zachęcam Cię do samodzielnego wpisywania kodu w konsoli i eksperymentowania z przykładami. W rozdziale poruszone zostały następujące tematy: Q pole prototype i przechowywany w nim obiekt; Q dodawanie pól do obiektu prototype; Q korzystanie z pól dodanych do prototypu; Q różnica pomiędzy własnymi polami obiektu a polami prototypu; Q __proto__, czyli ukryte powiązanie obiektu z jego prototypem; Q metody isPrototypeOf(), hasOwnProperty() oraz propertyIsEnumerable(); Q rozszerzanie obiektów wbudowanych, takich jak tablice i łańcuchy znaków.
Pole prototype Funkcje w języku JavaScript są obiektami posiadającymi pola i metody. Niektóre z nich już znasz, na przykład metody apply() i call()oraz pola length i constructor. prototype jest kolejnym polem obiektu Function.
JavaScript. Programowanie obiektowe
Po zdefiniowaniu prostej funkcji foo() możesz traktować ją jak obiekt i uzyskać dostęp do jej pól: >>> function foo(a, b){return a * b;} >>> foo.length
2 >>> foo.constructor
Function() Pole prototype tworzone jest w chwili definicji funkcji. Jego wartością początkową jest pusty obiekt. >>> typeof foo.prototype
"object" Jawne ustawienie wartości pola da ten sam efekt: >>> foo.prototype = {}
Możesz rozszerzać obiekt przechowywany w prototype o funkcje i metody. Nie będą one miały żadnego wpływu na działanie funkcji foo() — zostaną użyte tylko podczas wywołania foo() jako konstruktora.
Dodawanie pól i metod przy użyciu prototypu W poprzednim rozdziale pokazałem, jak tworzyć funkcje będące konstruktorami nowych obiektów. Wewnątrz takiej funkcji, wywoływanej z operatorem new, programista ma dostęp do wartości this zawierającej obiekt, który zostanie zwrócony przez konstruktor. Rozszerzając ten obiekt (czyli dodając do niego pola i metody), dodajemy funkcjonalności do nowo tworzonego obiektu. Spójrzmy na konstruktor Gadget(), który przy użyciu this dodaje dwa pola i jedną metodę do tworzonego obiektu. function Gadget(name, color) { this.nazwa = nazwa; this.kolor = kolor; this.ktosTy = funkcja(){ return 'Jam ' + this.kolor + ' ' + this.nazwa; } }
Innym sposobem rozszerzania nowo tworzonych obiektów jest dodawanie pól i metod do pola prototype konstruktora. Dołóżmy do obiektu jeszcze dwa pola, cena i ocena_uzytkownikow, oraz metodę informuj(). Ponieważ prototype zawiera obiekt, można dodać własności klasy w następujący sposób:
156
Rozdział 5. • Prototypy
Gadget.prototype.cena = 100; Gadget.prototype.ocena_uzytkownikow = 3; Gadget.prototype.informuj = function() { return 'Ocena_uzytkownikow: ' + this.ocena_uzytkownikow + ', cena: ' + this.cena; };
Można także zrezygnować z dodawania własności do obiektu prototype i zamiast tego całkowicie go nadpisać innym obiektem: Gadget.prototype = { cena: 100, ocena_uzytkownikow: 3, informuj: function() { return 'Ocena_uzytkownikow: ' + this.ocena_uzytkownikow + ', cena: ' + this.cena; } };
Korzystanie z pól i metod obiektu prototype Pola i metody dodane do prototypu stają się dostępne zaraz po utworzeniu nowego obiektu przy użyciu danego konstruktora. Jeśli przy użyciu konstruktora Gadget() stworzysz obiekt nowaZabawka, możliwe będzie sięgnięcie do wszystkich zdefiniowanych wcześniej pól i metod. >>> var nowaZabawka = new Gadget('kamera', 'czarna'); >>> nowaZabawka.nazwa;
"kamera" >>> nowaZabawka.kolor;
"czarna" >>> nowaZabawka.ktosTy();
"Jam czarna kamera" >>> nowaZabawka.cena;
100 >>> nowaZabawka.ocena_uzytkownikow;
3 >>> nowaZabawka.informuj();
"Ocena użytkownków: 3, cena: 100"
157
JavaScript. Programowanie obiektowe
Nie wolno zapominać o tym, że obiekty są przekazywane przez referencję, zatem zmiana prototypu pociąga za sobą zmiany we wszystkich dziedziczących z niego obiektach, nawet tych utworzonych wcześniej. Dodajmy do prototypu nową metodę: Gadget.prototype.pobierz = function(co) { return this[co]; };
Pomimo tego, że nowaZabawka została utworzona przed zdefiniowaniem metody pobierz(), obiekt ma do niej dostęp:: >>> nowaZabawka.pobierz('cena');
100 >>> nowaZabawka.pobierz('kolor');
"czarna"
Własne pola obiektu a pola prototypu W poprzednim przykładzie metoda informuj() korzystała z this w celu uzyskania dostępu do obiektu. Ten sam wynik dałoby odwołanie się do Gadget.prototype: Gadget.prototype.informuj = function() { return 'Ocena użytkowników: ' + Gadget.prototype.ocena_uzytkownikow + ', cena: ' + Gadget.prototype.cena; };
Czy to rozwiązanie różni się czymś od poprzedniego? By móc poprawnie odpowiedzieć na to pytanie, przyjrzymy się, jak dokładnie działa prototyp. Jeszcze raz utwórzmy obiekt nowaZabawka: >>> var nowaZabawka = new Gadget('kamera', 'czarna');
Jeśli spróbujesz pobrać wartość któregoś z pól obiektu nowaZabawka, na przykład nowaZabawka. ´nazwa, JavaScript przejdzie przez wszystkie pola obiektu w poszukiwaniu pola o nazwie nazwa. Jeśli znajdzie takie pole, zwróci jego wartość. >>> nowaZabawka.nazwa
"kamera" Co stanie się, jeśli zapragniesz sięgnąć do pola ocena_uzytkownikow? Pola nie uda się odnaleźć. Wówczas odszukany zostanie prototyp konstruktora, który został użyty podczas tworzenia obiektu (będzie to prototyp nowaZabawka.constructor.prototype). Jeśli prototyp posiada to pole, zostanie pobrana jego wartość.
158
Rozdział 5. • Prototypy
>>> nowaZabawka. ocena_uzytkownikow
3 Ten sam efekt dałoby bezpośrednie sięgnięcie do prototypu. Każdy obiekt posiada pole constructor będące referencją do funkcji, która utworzyła obiekt. W naszym przypadku: >>> nowaZabawka.constructor
Gadget(nazwa, kolor) >>> nowaZabawka.constructor.prototype.ocena_uzytkownikow
3 Przejdźmy teraz na wyższy poziom. Każdy obiekt posiada konstruktor. Prototyp jest obiektem, zatem również musi mieć konstruktor, który z kolei ma swój prototyp. Innymi słowy, możliwe jest następujące polecenie: >>> nowaZabawka.constructor.prototype.constructor
Gadget(nazwa, kolor) >>> nowaZabawka.constructor.prototype.constructor.prototype
Object cena=100 ocena_uzytkownikow=3 W zależności od długości łańcucha prototypów można wywołać różną liczbę sekwencji constructor.prototype, jednak w końcu zawsze dojdzie się do wbudowanego Object(), będącego prototypem najwyższego rzędu. Jeśli więc po wywołaniu nowaZabawka.toString() okaże się, że nowaZabawka nie posiada metody toString() ani nie ma jej też jej prototyp, oznacza to, że doszło się do metody toString prototypu Object. >>> nowaZabawka.toString()
"[object Object]"
Nadpisywanie pól prototypu własnymi polami obiektu Jednym z wniosków płynących z powyższego wykładu jest to, że jeśli obiekt nie posiada pewnego pola, może skorzystać z pola o tej samej nazwie pochodzącego z łańcucha prototypów (o ile istnieje). Co dzieje się w sytuacji, gdy i obiekt, i prototyp posiadają pole o danej nazwie? Pierwszeństwo będzie miało własne pole obiektu. Utwórzmy obiekt pasujący do opisanego powyżej scenariusza: function Gadget(nazwa) { this.nazwa = nazwa; } Gadget.prototype.nazwa = 'foo';
"foo"
159
JavaScript. Programowanie obiektowe
Po utworzeniu nowego obiektu i sięgnięciu do pola nazwa otrzymasz wartość jego własnego pola: >>> var zabawka = new Gadget('aparat fotograficzny'); >>> zabawka.nazwa;
"aparat fotograficzny" Jeśli usuniesz to pole, do głosu dojdzie analogiczne pole prototypu: >>> delete zabawka.nazwa;
true >>> zabawka.nazwa;
"foo" Oczywiście zawsze możesz odtworzyć własne pole obiektu: >>> zabawka.nazwa = 'aparat fotograficzny'; >>> zabawka.nazwa;
"aparat fotograficzny"
Pobieranie listy pól Jeśli chcesz wypisać wszystkie pola danego obiektu, możesz skorzystać z pętli for…in. W rozdziale 2. pokazałem, jak wykorzystać tę pętlę do iteracji po elementach tablicy: var a = [1, 2, 3]; for (var i in a) { console.log(a[i]); }
Tablice są obiektami, zatem można podejrzewać, że pętla for…in działa także na obiektach: var o = {p1: 1, p2: 2}; for (var i in o) { console.log(i + '=' + o[i]); }
Wynikiem będzie: p1=1 p2=2 Warto pamiętać, że: Q Pętla for…in pominie niektóre pola. Nie pojawią się na przykład pole length (dla tablic) ani constructor. Niepominięte pola określa się mianem wyliczalnych (ang. enumerable). To, czy pole jest wyliczalne, można sprawdzić za pomocą metody propertyIsEnumerable(), oferowanej przez wszystkie obiekty.
160
Rozdział 5. • Prototypy
Q Uwzględnione zostaną pola pochodzące z łańcucha prototypów, o ile są wyliczalne.
Można sprawdzić, czy dane pole jest własnym polem obiektu, czy polem pochodzącym z prototypu, za pomocą metody hasOwnProperty(). Q propertyIsEnumerable() zwróci false dla wszystkich pól prototypu, nawet jeśli są wyliczalne i pojawią się w pętli for…in.
Sprawdźmy, jak działają te metody. Zacznijmy od uproszczonej wersji konstruktora Gadget(): function Gadget(nazwa, kolor) { this.nazwa = nazwa; this.kolor = kolor; this.metoda = function(){return 1;} } Gadget.prototype.cena = 100; Gadget.prototype.ocena_uzytkownikow = 3;
Nowy obiekt: var nowaZabawka = new Gadget('kamera', 'czarna');
Za pomocą pętli for…in możesz wypisać wszystkie pola obiektu, także te pochodzące z prototypu: for (var pole in nowaZabawka) { console.log(pole + ' = ' + nowaZabawka [pole]); }
Wśród nich mogą znaleźć się również funkcje (skoro metody to pola, które przypadkiem są funkcjami): nazwa = kamera kolor = czarna metoda = function () { return 1; } cena = 100 ocena_uzytkownikow = 3 Jeśli chcesz odróżnić własne pola obiektu od pól prototypu, skorzystaj z hasOwnProperty(): >>> nowaZabawka.hasOwnProperty('nazwa')
true >>> nowaZabawka.hasOwnProperty('cena')
false
161
JavaScript. Programowanie obiektowe
Tym razem pętla wypisze tylko własne pola obiektu: for (var pole in nowaZabawka) { if (nowaZabawka.hasOwnProperty(pole)) { console.log(pole + '=' + nowaZabawka [pole]); } }
Wynik: nazwa=kamera kolor=czarna metoda=function () { return 1; } Przejdźmy teraz do propertyIsEnumerable(). Dla własnych pól metoda zwraca true: >>> nowaZabawka.propertyIsEnumerable('nazwa')
true Większość pól i metod wbudowanych nie jest wyliczalna: >>> nowaZabawka.propertyIsEnumerable('constructor')
false Nie są wyliczalne pola pochodzące z łańcucha prototypów: >>> nowaZabawka.propertyIsEnumerable('cena')
false Takie pola staną się wyliczalne, jeśli dojdziemy do obiektu zawartego w polu prototype i to na nim wywołamy metodę: >>> nowaZabawka.constructor.prototype.propertyIsEnumerable('cena')
true
isPrototypeOf() Każdy obiekt posiada metodę isPrototypeOf(). Zwraca on informację o tym, czy konkretny obiekt jest prototypem innego. Weźmy prosty obiekt małpa. var małpa = { owłosiona: true, je: 'banany', oddycha: 'powietrzem' };
162
Rozdział 5. • Prototypy
Utwórzmy teraz konstruktor Człowiek(), którego pole prototype ustawimy na małpa. function Człowiek(nazwisko) { this.nazwisko = nazwisko; } Człowiek.prototype = małpa;
Jeśli stworzysz teraz nowy obiekt typu Człowiek i nazwiesz go jurek, a następnie spytasz „Czy małpa jest prototypem Jurka?”, otrzymasz wartość true. >>> var jurek = new Człowiek('Jurek'); >>> małpa.isPrototypeOf(jurek)
true
Ukryte powiązanie __proto__ Wiesz już, że jeśli obiekt nie posiada pola o podanej nazwie, sprawdzone zostanie pole prototype. Jeszcze raz utwórzmy obiekt małpa i wykorzystajmy go jako prototyp podczas tworzenia obiektów za pomocą konstruktora Człowiek(). var małpa = { je: 'banany', oddycha: 'powietrzem' }; function Człowiek() {} Człowiek.prototype = małpa;
Utwórzmy teraz obiekt programista i przypiszmy mu kilka pól: var programista = new Człowiek(); programista.je = 'pizzę'; programista.wymiata_w = 'JavaScript';
Pobierzmy teraz wartości pól. wymiata_w jest polem obiektu programista. >>> programista.wymiata_w
"JavaScript" Innym polem jest je: >>> programista.je
"pizzę" oddycha nie jest własnym polem obiektu programista, dlatego sprawdzony zostanie prototyp,
jak gdyby istniało tajemne powiązanie pomiędzy obiektem a prototypem.
163
JavaScript. Programowanie obiektowe
>>> programista.oddycha
"powietrzem" Czy z obiektu programista możesz dostać się do jego prototypu? Cóż, w zasadzie tak — jeśli wykorzystasz konstruktor jako pośrednika, dostaniesz się do obiektu małpa, pisząc programista. ´constructor.prototype. Rozwiązanie to nie jest jednak godne polecenia. Pole constructor pełni przede wszystkim funkcję informacyjną, a jego wartość może zostać zmieniona w dowolnej chwili. Możesz nawet podstawić tam dane niebędące obiektem — nie zaburzy to działania łańcucha prototypów. Przypiszmy polu constructor prosty łańcuch znaków: >>> programista.constructor = 'śmieć'
"śmieć" Wydaje się, że zepsuliśmy prototyp: >>> typeof programista.constructor.prototype
"undefined" …a jednak tak nie jest, skoro programista nadal oddycha "powietrzem": >>> programista.oddycha
"powietrzem" Dowodzi to, że nadal istnieje sekretne powiązanie pomiędzy obiektem a jego prototypem. Firefox widzi to powiązanie jako pole __proto__ (na początku i na końcu słowa proto znajdują się po dwa podkreślniki). >>> programista.__proto__
Object je=banany oddycha=powietrzem Możesz pobawić się tym polem dla celów nauki, ale nie warto korzystać z niego w skryptach. __proto__ nie istnieje w przeglądarce IE, więc Twoje skrypty nie byłyby przenośne. Spróbuj na przykład utworzyć kilka obiektów, dla których małpa będzie prototypem, a potem wprowadzić
zmianę w prototypie, która wpłynie na istniejące już obiekty: >>> małpa.test = 1
1 >>> programista.test
1 __proto__ i prototype to nie to samo. Różnica polega na tym, że __proto__ jest polem instancji, podczas gdy prototype jest polem konstruktorów.
164
Rozdział 5. • Prototypy
>>> typeof programista.__proto__
"object" >>> typeof programista.prototype
"undefined" Powiem to jeszcze raz: nie odwołuj się bezpośrednio do __proto__, chyba że robisz to dla celów nauki, lub szukając błędów.
Rozszerzanie obiektów wbudowanych Obiekty wbudowane, takie jak konstruktory Array, String, Object czy Function, mogą zostać rozszerzone za pomocą prototypów. Oznacza to, że możesz dodać, na przykład do prototypu Array, nowe metody, które staną się dostępne dla wszystkich tablic. Przećwiczmy to. Język PHP posiada funkcję o nazwie in_array(), która sprawdza, czy podana wartość jest elementem tablicy. JavaScript nie ma takiej metody. Zaimplementujmy ją pod nazwą inArray() i dodajmy do Array.prototype. Array.prototype.inArray = function(needle) { for (var i = 0, len = this.length; i < len; i++) { if (this[i] === needle) { return true; } } return false; }
Każda tablica będzie miała tę metodę. Sprawdźmy: >>> var a = [czerwony', 'zielony', 'niebieski']; >>> a.inArray('czerwony');
true >>> a.inArray('żółty');
false Działa! Zatem zróbmy to jeszcze raz. Wyobraź sobie, że Twoja aplikacja często musi odwracać kolejność liter w łańcuchach znaków, w związku z czym czujesz, że przydałaby się wbudowana metoda reverse() („odwróć”) działająca na napisach. Zaraz, zaraz, przecież tablice posiadają metodę reverse()! Możesz łatwo dodać ją (w postaci Array.prototype.reverse()) do prototypu String (podobne zadanie znalazło się w ćwiczeniach do rozdziału 4.).
165
JavaScript. Programowanie obiektowe
String.prototype.reverse = function() { return Array.prototype.reverse.apply(this.split('')).join(''); }
Metoda split() została wykorzystana do zamiany łańcucha na tablicę, na której następnie wywołujemy metodę reverse(). Wynik jest z powrotem zamieniany na napis przy użyciu join(). Przetestujmy nową metodę: >>> "Stoyan".reverse();
"nayotS"
Rozszerzanie obiektów wbudowanych — kontrowersje Rozszerzając obiekty wbudowane za pomocą prototypów, możesz w dowolny sposób kształtować kod. Możliwości tego mechanizmu są niemalże nieograniczone, dlatego przed jego zastosowaniem należy zastanowić się, czy nie istnieją inne, mniej inwazyjne rozwiązania. Przykładem biblioteki, w którym prototypy stosowane są na każdym kroku, jest Prototype. Jej twórca tak bardzo lubił prototypy, że nazwał swoje dzieło na ich cześć! Biblioteka ta pozwala używać metod JavaScriptu w sposób zbliżony do języka Ruby. Biblioteka YUI (Yahoo! User Interface) jest całkowitym przeciwieństwem Prototype. Jej twórcy w żaden sposób nie modyfikują obiektów wbudowanych, ponieważ wychodzą z założenia, że osoba znająca JavaScript spodziewa się, że obiekty będą działały tak samo niezależnie od wykorzystywanej biblioteki. Modyfikacja najważniejszych obiektów mogłaby zdezorientować użytkownika i doprowadzić do powstania nieoczekiwanych błędów. Prawda jest taka, że JavaScript ewoluuje, a kolejne wersje przeglądarek oferują coraz lepsze wsparcie dla funkcjonalności języka. Funkcja, której brakuje Ci w tej chwili (i którą możesz chcieć dodać do prototypu), może jutro okazać się funkcją wbudowaną. W takim wypadku stworzona przez Ciebie metoda przestanie być potrzebna. Możesz obudzić się z dużą ilością kodu, który nie jest już potrzebny, a na dodatek działa troszkę inaczej niż nowa metoda wbudowana. Jeśli już decydujesz się na rozszerzanie obiektów wbudowanych, zawsze upewniaj się, że metoda faktycznie nie istnieje. Nasz ostatni przykład mógłby wyglądać tak: if (!String.prototype.reverse) { String.prototype.reverse = function() { return Array.prototype.reverse.apply(this.split('')).join(''); } } Dobra rada Jeśli decydujesz się na dodanie nowego pola do obiektu wbudowanego, koniecznie sprawdź, czy dane pole nie zostało już zaimplementowane.
166
Rozdział 5. • Prototypy
Pułapki związane z prototypami Istnieją dwa nie do końca intuicyjne zachowania programów związane z prototypami: Q Zmiana prototypu pociąga za sobą jego zmianę we wszystkich utworzonych przy jego pomocy obiektach, z wyjątkiem sytuacji, gdy obiekt prototype zostaje
całkowicie zastąpiony innym obiektem. Q Nie można ufać zawartości prototype.constructor.
Dwa proste konstruktory i dwa obiekty: >>> function Pies(){this.ogon = true;} >>> var azor = new Pies(); >>> var burek = new Pies();
Nawet po utworzeniu obiektów możliwe jest dodawanie do prototypu nowych własności, do których obiekty będą miały dostęp. Dodajmy metodę szczekaj(): >>> Pies.prototype.szczekaj = function(){return 'Hau!';}
Oba obiekty mają do niej dostęp: >>> azor.szczekaj();
"Hau!" >>> burek.szczekaj();
"Hau!" Jeśli teraz spytasz obiekty o konstruktor, za pomocą którego zostały utworzone, odpowiedzą poprawnie. >>> azor.constructor;
Pies() >>> burek.constructor;
Pies() Ciekawostką jest to, że jeśli spytasz o konstruktor prototypu, w odpowiedzi również otrzymasz Pies(), co nie jest do końca zgodne z prawdą. Prototyp to w rzeczywistości zwykły obiekt utworzony za pomocą Object(). Nie posiada on żadnych własności obiektu utworzonego za pomocą konstruktora Pies(). >>> azor.constructor.prototype.constructor
Pies() >>> typeof azor.constructor.prototype.ogon
"undefined" 167
JavaScript. Programowanie obiektowe
Nadpiszmy teraz obiekt prototypu zupełnie innym obiektem: >>> Pies.prototype = {łapy: 4, owłosiony: true};
Okazuje się, że stare obiekty nie mają dostępu do nowych pól. Nadal są one powiązane ze starym obiektem prototypu: >>> typeof azor.łapy
"undefined" >>> azor.szczekaj()
"Hau!" >>> typeof azor.__proto__.szczekaj
"function" >>> typeof azor.__proto__.łapy
"undefined" Nowe obiekty będą korzystały z poprawionej wersji prototypu: >>> var lessi = new Pies(); >>> lessi.szczekaj()
TypeError: lessi.szczekaj is not a function >>> lessi.łapy
4 __proto__ wskazuje nowy obiekt prototypu: >>> typeof lessi.__proto__.szczekaj
"undefined" >>> typeof lessi.__proto__.łapy
"number" W tej chwili pole constructor przestaje zwracać poprawną informację. Powinno wskazywać na Pies(), ale zamiast niego jest to Object(): >>> lessi.constructor
Object() >>> azor.constructor
Dog()
168
Rozdział 5. • Prototypy
Najbardziej dezorientującym elementem jest informacja o prototypie konstruktora: >>> typeof lessi.constructor.prototype.łapy
"undefined" >>> typeof azor.constructor.prototype.łapy
"number" Poniższe linie powinny przywrócić oczekiwane zachowanie kodu: >>> Pies.prototype = {łapy: 4, owłosiony: true}; >>> Pies.prototype.constructor = Pies; Dobra rada Jeśli nadpisujesz prototyp, nadaj odpowiednią wartość polu constructor.
Podsumowanie Podsumujmy krótko treść rozdziału: Q Wszystkie funkcje posiadają pole o nazwie prototype. Początkowo zawiera ono pusty obiekt. Q Do obiektu prototype można dodawać pola i metody. Można także zastąpić go innym obiektem. Q Obiekt utworzony za pomocą konstruktora (z operatorem new) przechowuje ukryte powiązanie z prototypem oraz może odwoływać się do pól prototypu jak do swoich własnych. Q Własne pola obiektu nadpisują pola prototypu o tej samej nazwie. Q Pola własne od pól prototypu można odróżnić za pomocą metody hasOwnProperty(). Q Istnieje łańcuch prototypów: jeśli obiekt foo nie posiada pola bar, w wyniku odwołania się do foo.bar zostanie przeszukany prototyp. Jeśli prototyp nie ma tego pola, sprawdzony zostanie jego prototyp — aż do najwyższego poziomu, czyli do Object. Q Możesz rozszerzać konstruktory wbudowane. Wprowadzone w nich zmiany będą widoczne dla wszystkich obiektów. Jeśli przypiszesz funkcję do Array.prototype.flip, wszystkie tablice będą miały metodę flip (np. [1,2,3].flip). Sprawdź (w kodzie),
czy metoda na pewno nie istnieje — uchroni Cię to przed błędami w przyszłości.
169
JavaScript. Programowanie obiektowe
Ćwiczenia 1. Utwórz obiekt kształt, który posiada pole typ oraz metodę dostępową pobierzTyp() (lub, zgodnie z angielskojęzyczną konwencją, pole type oraz metodę getType()). 2. Zdefiniuj konstruktor Trójkąt(), którego prototypem jest Figura. Obiekty tworzone za pomocą Trójkąt() powinny mieć trzy własne pola: a, b i c, przechowujące długość boków trójkąta. 3. Dodaj do prototypu nową metodę o nazwie pobierzObwód(). 4. Przetestuj poprawność implementacji za pomocą następującego kodu: >>> var t = new Trójkąt(1, 2, 3); >>> t.constructor
Trójkąt(a, b, c) >>> kształt.isPrototypeOf(t)
true >>> t.pobierzObwód()
6 >>> t.pobierzTyp()
"trójkąt" 5. Napisz pętlę, która wypisze wszystkie własne pola i metody obiektu t, pomijając pola i metody prototypu. 6. Spraw, by działał poniższy kod: >>> [1,2,3,4,5,6,7,8,9].potasuj()
[2, 4, 1, 8, 9, 6, 5, 3, 7]
170
6 Dziedziczenie W rozdziale 1. przedstawiłem różne zagadnienia i terminy związane z programowaniem obiektowym. W kolejnym rozdziałach pokazywałem, jakie jest ich zastosowanie w języku JavaScript. W tej chwili wiesz już, czym są obiekty, pola i metody. Wiesz, że JavaScript nie ma klas, ale ich funkcjonalności są dostępne dzięki konstruktorom. Kapsułkowanie? Jak najbardziej: obiekty posiadają zarówno dane, jak i sposoby działania na tych danych (czyli metody). Agregacja? Tak, obiekty mogą zawierać inne obiekty, a nawet zawierają je prawie zawsze — skoro metody to funkcje, a funkcje to obiekty. Skoro posiadasz już cały ten ogrom wiedzy, przyszła pora, by skoncentrować się na dziedziczeniu. Jest to jedna z najciekawszych cech programowania obiektowego. Dzięki dziedziczeniu ten sam kod można wykorzystywać wielokrotnie. Sprzyja to lenistwu, ale to właśnie lenistwo doprowadziło do powstania języków programowania, czyż nie? JavaScript jest językiem dynamicznym, przez co większość zadań programistycznych można wykonać na więcej niż jeden sposób. Dziedziczenie nie jest tu wyjątkiem. Zamierzam przedstawić kilka popularnych wzorców implementacji dziedziczenia, zaczynając od sposobu opisanego w standardzie ECMAScript. Poświęć chwilę na zrozumienie tych wzorców i świadomy wybór tego, który najlepiej pasuje do Twojego projektu i stylu pracy. W rozdziale kilka razy pojawi się nazwisko Douglasa Crockforda. Nie sposób mówić o dziedziczeniu w języku JavaScript bez powoływania się na jego dokonania. Poza filmikami wspomnianymi w rozdziale 1. (http://developer.yahoo.com/yui/theater/) polecam także artykuły na jego stronie http://crockford.com/javascript.
JavaScript. Programowanie obiektowe
Łańcuchy prototypów Zacznijmy od podstawowego sposobu implementacji dziedziczenia, czyli od łańcuchów prototypów. Wiesz już, że każda funkcja posiada pole prototype, które zawiera obiekt. Jeśli funkcja zostanie wywołana z operatorem new, zostanie utworzony nowy obiekt, niejawnie połączony z prototypem. Ukryte połączenie pomiędzy obiektem a jego prototypem (w niektórych środowiskach dostępne jako __proto__) umożliwia obiektowi korzystanie z pól i metod prototypu jak ze swoich własnych. Prototyp sam jest obiektem i jako taki posiada połączenie ze swoim prototypem, który także posiada własny prototyp. Taką hierarchię określa się mianem łańcucha prototypów.
Widoczny na rysunku obiekt A posiada wiele pól. Jednym z nich jest ukryte pole __proto__, przechowujące wskaźnik do obiektu o nazwie B. Z kolei pole __proto__ obiektu B wskazuje obiekt C. Łańcuch kończy się obiektem Object, który jest rodzicem najwyższego rzędu: wszystkie obiekty dziedziczą z niego. W jaki sposób można wykorzystać te właściwości obiektów? Wiesz już, że jeśli obiekt A nie posiada pewnego pola, które ma obiekt B, A może uzyskać dostęp do tego pola jak do własnego. Tak samo B może sięgać do składowych obiektu C. W ten właśnie sposób działa dziedziczenie: obiekt ma dostęp do wszystkich pól i metod znajdujących się wyżej w łańcuchu dziedziczenia. W dalszej części rozdziału zobaczysz różne przykłady oparte na następującej hierarchii: ogólny obiekt-rodzic Figura jest dziedziczony przez obiekt Figura2D, z którego dalej dziedziczy dowolna liczba dwuwymiarowych figur, takich jak Trójkąt, Prostokąt itd.
Przykładowy łańcuch prototypów Tworzenie łańcuchów prototypów jest standardowym sposobem implementacji dziedziczenia, opisanym w standardzie ECMAScript. Stwórzmy teraz przykładową hierarchię, zaczynając od trzech konstruktorów.
172
Rozdział 6. • Dziedziczenie
function Figura(){ this.nazwa = 'figura'; this.toString = function() {return this.nazwa;}; } function Figura2D(){ this.nazwa = 'figura 2D'; } function Trójkąt(bok, wysokość) { this.nazwa = 'trójkąt'; this.bok = bok; this.wysokość = wysokość; this.pobierzPole = function(){return this.bok * this.wysokość / 2;}; }
Dziedziczenie odbywa się w następujący sposób: Figura2D.prototype = new Figura(); Trójkąt.prototype = new Figura2D();
Co się dzieje? Zamiast dodawać pola do obiektu przechowywanego w polu prototype obiektu Figura2D, nadpisaliśmy obiekt czymś zupełnie nowym — obiektem powstałym w wyniku wywołania konstruktora new Figura(). Tak samo w przypadku obiektu Trójkąt, którego prototyp został zastąpiony nowym obiektem Figura2D. Należy pamiętać (dotyczy to zwłaszcza osób przyzwyczajonych do języków takich jak Java, C++ czy PHP), że JavaScript działa na obiektach, a nie na klasach. Do realizacji dziedziczenia konieczne jest utworzenie instancji Figura przy użyciu operatora new — nie dziedziczy się bezpośrednio z Figura. Ponadto gdy obiekty zostaną utworzone w ten sposób, można do woli zmieniać konstruktor Figura(), nawet nadpisać go czy skasować. Nie będzie to miało wpływu na obiekt Figura2D, ponieważ Figura2D dziedziczy tylko z jednej konkretnej instancji. Pewnie pamiętasz z poprzedniego rozdziału, że całkowite nadpisanie prototypu (w odróżnieniu od jego rozszerzenia o nowe pola) ma pewne efekty uboczne związane z polem constructor. Dlatego po implementacji dziedziczenia warto na nowo ustawić wartość constructor: Figura2D.prototype.constructor = Figura2D; Trójkąt.prototype.constructor = Trójkąt;
Przetestujmy kod. Stwórzmy obiekt Trójkąt i wywołajmy jego metodę pobierzPole(): >>> var my = new Trójkąt(5, 10); >>> my.pobierzPole();
25 Chociaż obiekt my nie ma własnej metody toString(), dziedziczy ją z innego obiektu i dzięki temu może ją wywołać. Zwróć uwagę, że this wewnątrz metody toString odnosi się do obiektu my.
173
JavaScript. Programowanie obiektowe
>>> my.toString()
"trójkąt" Oto, co dzieje się po wywołaniu my.toString(): Q Interpreter JavaScriptu sprawdza pola obiektu my i nie znajduje metody o nazwie toString(). Q Sprawdza obiekt wskazywany przez my.__proto__. Obiekt ten jest instancją Figura2D utworzoną w procesie dziedziczenia. Q Interpreter próbuje znaleźć metodę toString() w instancji Figura2D. Ponieważ przeszukiwanie kończy się niepowodzeniem, sprawdza wskaźnik __proto__, który tym razem prowadzi do instancji utworzonej wcześniej jako new Shape(). Q Udaje się odnaleźć metodę toString() wewnątrz instancji Shape(). Q Metoda zostaje wywołana w kontekście obiektu my, co oznacza, że this wskazuje my.
Jeśli spytamy my „kto cię stworzył?”, odpowie poprawnie, ponieważ podczas dziedziczenia jawnie zmieniliśmy wartość constructor. >>> my.constructor
Trójkąt(bok, wysokość) Przy pomocy operatora instanceof możesz upewnić się, że my jest instancją wszystkich trzech konstruktorów. >>> my instanceof Figura
true >>> my instanceof Figura2D
true >>> my instanceof Trójkąt
true >>> my instanceof Array
false Ten sam efekt da wywołanie metody isPropertyOf() („czy jest polem obiektu”) konstruktorów z parametrem my: >>> Figura.prototype.isPrototypeOf(my)
true >>> Figura2D.prototype.isPrototypeOf(my)
true
174
Rozdział 6. • Dziedziczenie
>>> Trójkąt.prototype.isPrototypeOf(my)
true >>> String.prototype.isPrototypeOf(my)
false Możesz również tworzyć obiekty przy użyciu dwóch pozostałych konstruktorów. Obiekty utworzone za pomocą new Figura2D() także mają metodę toString(), odziedziczoną z Figura(). >>> var td = new Figura2D(); >>> td.constructor
Figura2D() >>> td.toString()
"figura 2D" >>> var s = new Figura(); >>> s.constructor
Figura()
Przenoszenie wspólnych pól do prototypu Kiedy tworzysz obiekty przy użyciu konstruktora, możesz dodawać nowe pola przy użyciu this. Nie jest to najlepsze rozwiązanie, jeśli wszystkie instancje mają te same pola. W powyższym przykładzie konstruktor Figura() został zdefiniowany w następujący sposób: function Figura(){ this.nazwa= 'Figura'; }
Oznacza to, że za każdym razem gdy nowy obiekt powstaje w wyniku wywołania new Figura(), tworzone jest nowe pole nazwa, które musi zostać przechowane w pamięci. Zamiast tego można dodać pole nazwa do prototypu — wówczas będzie ono dzielone przez wszystkie instancje. function Figura(){} Figura.prototype.nazwa = 'Figura';
Od tej pory nowe obiekty tworzone za pomocą Figura() nie będą posiadały własnego pola nazwa, tylko będą korzystały z pola dodanego do prototypu. To rozwiązanie jest bardziej efektywne, jednak można je stosować tylko w przypadku pól, których wartości nie różnią się pomiędzy instancjami. Idealnymi kandydatami do tego typu współdzielenia są metody. Poprawmy powyższy przykład poprzez dodanie do prototypu wszystkich pól i metod, które się do tego nadają. W przypadku Figura() i Figura2D() można współdzielić wszystko:
175
JavaScript. Programowanie obiektowe
function Figura(){} // rozszerzenie prototypu Figura.prototype.nazwa = 'figura'; Shape.prototype.toString = function() {return this.nazwa;}; function Figura2D(){} // obsługa dziedziczenia Figura2D.prototype = new Figura(); Figura2D.prototype.constructor = Figura2D; // rozszerzenie prototypu Figura2D.prototype.nazwa = 'figura 2D';
Jak widać, najpierw należy zadbać o dziedziczenie, a dopiero potem rozszerzać prototyp — inaczej wszystko, co zostanie dodane do Figura2D.prototype, zniknie podczas dziedziczenia. Konstruktor Trójkąt() jest trochę inny, ponieważ każdy tworzony przez niego obiekt jest nowym trójkątem, który może mieć inne wymiary. Dlatego też bok i wysokość powinny być polami instancji. Inne pola można dzielić — na przykład pobierzPole() jest zawsze takie samo, niezależnie od wymiarów trójkąta. Tak jak poprzednio, najpierw należy opisać dziedziczenie, a dopiero potem rozszerzać prototyp. function Trójkąt(bok, wysokość) { this.bok = bok; this.wysokość = wysokość; } // obsługa dziedziczenia Trójkąt.prototype = new Figura2D(); Trójkąt.prototype.constructor = Trójkąt; // rozszerzenie prototypu Trójkąt.prototype.nazwa = 'Trójkąt'; Trójkąt.prototype.getArea = function(){return this.bok * this.wysokość / 2;};
Kod można testować w taki sam sposób jak wcześniej: >>> var my = new Trójkąt(5, 10); >>> my.getArea()
25 >>> my.toString()
"Trójkąt" Działanie kodu jest takie same. Różnią się tylko operacje wykonywane w tle podczas wywołania my.toString(). Konieczne jest jedno sprawdzenie więcej, zanim metoda zostanie odnaleziona w Figura.prototype, a nie w instancji new Figura(), co miało miejsce w poprzednim przykładzie. Możesz także poeksperymentować z metodą hasOwnProperty(), która pozwoli Ci sprawdzić, czy dane pole jest własnym polem obiektu, czy polem pochodzącym z łańcucha prototypów.
176
Rozdział 6. • Dziedziczenie
>>> my.hasOwnProperty('bok')
true >>> my.hasOwnProperty('nazwa')
false Wywołania isPrototypeOf() oraz operator instanceof zadziałają dokładnie tak samo jak we wcześniejszym przykładzie: >>> Figura2D.prototype.isPrototypeOf(my)
true >>> my instanceof Figura
true
Dziedziczenie samego prototypu Wyjaśniłem już, że dla zwiększenia wydajności warto rozważyć dodanie współdzielonych pól i metod do prototypu. Jeśli zdecydujesz się na to rozwiązanie, dobrym pomysłem może okazać się dziedziczenie samego prototypu, skoro to w nim znajduje się interesujący Cię kod wielokrotnego użytku. Innymi słowy, lepsze będzie dziedziczenie obiektu osadzonego w Figura.prototype niż całego obiektu utworzonego za pomocą new Figura() — przecież i tak nie skorzystasz z własnych pól obiektu Figura (inaczej trafiłyby one do prototypu). Rozwiązanie to pociąga za sobą zwiększenie efektywności, ponieważ: Q Nie jest tworzony nowy obiekt potrzebny tylko podczas dziedziczenia. Q Przeszukiwanie łańcucha prototypów (na przykład w celu odnalezienia toString()) jest krótsze. Oto kod w poprawionej wersji (zmiany zostały wytłuszczone): function Figura(){} // rozszerzenie prototypu Figura.prototype.nazwa = 'figura'; Figura.prototype.toString = function() {return this.nazwa;}; function Figura2D(){} // obsługa dziedziczenia Figura2D.prototype = Figura.prototype; Figura2D.prototype.constructor = Figura2D; // rozszerzenie prototypu Figura2D.prototype.nazwa = 'figura 2D';
177
JavaScript. Programowanie obiektowe
function Trójkąt(bok, wysokość) { this.bok = bok; this.wysokość = wysokość; } // obsługa dziedziczenia Trójkąt.prototype = Figura2D.prototype; Trójkąt.prototype.constructor = Trójkąt; // rozszerzenie prototypu Trójkąt.prototype.nazwa = 'trójkąt'; Trójkąt.prototype.pobierzPole = function(){return this.bok * this.wysokość / 2;}
Kod można przetestować w taki sam sposób jak wcześniej: >>> var my = new Trójkąt(5, 10); >>> my.pobierzPole()
25 >>> my.toString()
"trójkąt" Na czym polega różnica w wyszukiwaniu podczas wywołania my.toString()? Po pierwsze, interpreter jak zwykle szuka metody toString() wewnątrz obiektu. Nie znajduje go, dlatego sprawdza prototyp. Prototyp zawiera wskaźnik na ten sam obiekt, który wskazują pola prototype obiektu Figura2D oraz obiektu Figura. Pamiętaj, że obiekty nie są kopiowane przez wartość, tylko przez referencję. Dlatego sprawdzenie odbywa się w dwóch krokach, a nie w czterech (jak w poprzednim przykładzie) ani trzech (jak na początku). Kopiowanie samego prototypu zwiększa efektywność, ale ma pewien efekt uboczny: ponieważ wszystkie dzieci i wszyscy rodzice wskazują ten sam obiekt, jeśli dziecko zmieni prototyp, zmiana będzie widoczna dla wszystkich rodziców i całego rodzeństwa. Przeanalizuj następującą linię: Trójkąt.prototype.nazwa = 'trójkąt;
Zmieniane jest pole nazwa. Zmiana wartości jest widziana także na ścieżce Figura.prototype.name. Jeśli utworzysz egzemplarz obiektu przy użyciu new Figura(), pole nazwa będzie zawierało wartość "trójkąt": >>> var f = new Figura() >>> f.nazwa
"trójkąt"
Konstruktor tymczasowy — new F() Rozwiązaniem opisanego powyżej problemu, kiedy to wszystkie pola prototype wskazują ten obiekt, przez co obiekty-rodzice odczytują zmiany wprowadzone przez obiekty-dzieci, jest zastosowanie pośrednika, który przerwie łańcuch. Pośrednik ma formę tymczasowego konstruktora. 178
Rozdział 6. • Dziedziczenie
Tworząc pustą funkcję F() i ustawiając jej wartość prototype na prototype konstruktora-rodzica, możesz wywoływać new f() i tworzyć obiekty, które nie mają własnych pól, ale dziedziczą wszystko z prototype rodzica. Zmodyfikowany kod wygląda tak: function Figura(){} // rozszerzenie prototypu Figura.prototype.nazwa = 'figura'; Figura.prototype.toString = function() {return this.nazwa;}; function Figura2D(){} // obsługa dziedziczenia var F = function(){}; F.prototype = Figura.prototype; Figura2D.prototype = new F(); Figura2D.prototype.constructor = Figura2D; // rozszerzenie prototypu Figura2D.prototype.name = '2D Figura'; function Trójkąt(bok, wysokość) { this.bok = bok; this.wysokość = wysokość; } // obsługa dziedziczenia var F = function(){}; F.prototype = Figura2D.prototype; Trójkąt.prototype = new F(); Trójkąt.prototype.constructor = Trójkąt; // rozszerzenie prototypu Trójkąt.prototype.name = 'Trójkąt'; Trójkąt.prototype.getArea = function(){return this.bok * this.wysokość / 2;};
Testy: >>> var my = new Trójkąt(5, 10); >>> my.pobierzPole()
25 >>> my.toString()
"trójkąt" W ten sposób zachowywany jest łańcuch prototypów, ale pola rodziców nie są nadpisywane polami dzieci: >> my.__proto__.__proto__.__proto__.constructor
Figura()
179
JavaScript. Programowanie obiektowe
>>> var s = new Figura(); >>> s.name
"figura" Podejście opisane w tym podrozdziale jest zgodne z poglądem, że dziedziczone powinny być tylko pola i metody dodane do prototypu, zaś własne pola obiektów powinny być pomijane, ponieważ z reguły są zbyt szczegółowe i zbyt silnie związane z instancją, by można je było ponownie wykorzystać.
Uber: dostęp do obiektu-rodzica Większość klasycznych języków obiektowych posiada specjalną składnię umożliwiającą dostęp do klasy-rodzica, określanej mianem nadklasy (ang. superclass) lub klasy bazowej. Przydaje się to, gdy dziecko chce implementować metodę, która wykonuje wszystkie czynności wykonywane przez metodę rodzica, a na koniec dodaje coś od siebie. W takich sytuacjach dziecko wywołuje metodę rodzica o tej samej nazwie, a następnie przetwarza zwracany przez nią wynik. JavaScript nie posiada takiej składni, ale osiągnięcie opisanej funkcjonalności i tak jest możliwe. Jeszcze raz przepiszmy poprzedni przykład, rozszerzając go o pole uber, które będzie przechowywało wskaźnik na prototyp rodzica. function Figura(){} // rozszerzenie prototypu Figura.prototype.nazwa = 'figura'; Figura.prototype.toString = function(){ var wynik = []; if (this.constructor.uber) { wynik[wynik.length] = this.constructor.uber.toString(); } wynik[wynik.length] = this.name; return wynik.join(', '); }; function Figura2D(){} // obsługa dziedziczenia var F = function(){}; F.prototype = Figura.prototype; Figura2D.prototype = new F(); Figura2D.prototype.constructor = Figura2D; Figura2D.uber = Figura.prototype; // rozszerzenie prototypu Figura2D.prototype.name = '2D Figura';
180
Rozdział 6. • Dziedziczenie
function Trójkąt(bok, wysokość) { this.bok = bok; this.wysokość = wysokość; } // obsługa dziedziczenia var F = function(){}; F.prototype = Figura2D.prototype; Trójkąt.prototype = new F(); Trójkąt.prototype.constructor = Trójkąt; Trójkąt.uber = Figura2D.prototype; // rozszerzenie prototypu Trójkąt.prototype.name = 'Trójkąt'; Trójkąt.prototype.getArea = function(){return this.bok * this.wysokość / 2;};
Pojawiły się następujące nowości: Q Nowe pole uber wskazuje prototyp rodzica. Q Zmieniła się definicja toString().
Wcześniej metoda toString() zwracała jedynie wartość this.nazwa. Teraz metoda dodatkowo sprawdza, czy istnieje this.constructor.uber. Jeśli tak, najpierw wywołuje metodę this. ´constructor.uber.toString(). this.constructor to funkcja (konstruktor), a this.constructor. ´uber to wskaźnik na prototype rodzica. Efekt jest taki, że wywołanie toString() na instancji Trójkąt zwraca połączone wyniki wywołania tej metody na elementach łańcucha prototypów: >>> var my = new Trójkąt(5, 10); >>> my.toString()
"figura, figura 2D, Trójkąt" Mógłbym zamiast uber nazwać pole „superclass”, ale sugerowałoby to, że JavaScript ma klasy. Chciałbym nazwać je po prostu „super” (jak w Javie), ale niestety super jest słowem zarezerwowanym i nie mogę z niego skorzystać. Niemieckie słowo „über” zostało zasugerowane przez Douglasa Crockforda. Oznacza ono mniej więcej to samo co angielskie słowo „super” (ponad).
Zamknięcie dziedziczenia wewnątrz funkcji Umieśćmy kod obsługujący dziedziczenie wewnątrz funkcji o nazwie extend() („rozszerz”): function extend(Dziecko, Rodzic) { var F = function(){}; F.prototype = Rodzic.prototype; Dziecko.prototype = new F(); Dziecko.prototype.constructor = Dziecko; Dziecko.uber = Rodzic.prototype; }
181
JavaScript. Programowanie obiektowe
Dzięki tej funkcji (lub podobnej, lepiej dopasowanej do konkretnego programu) kod będzie krótszy i bardziej czytelny. Obiekty mogą dziedziczyć z innych w następujący sposób: extend(Figura2D, Figura); extend(Trójkąt, Figura);
W podobny sposób realizowane jest dziedziczenie w bibliotece YUI (Yahoo! User Interface). Odpowiedni fragment kodu korzystający z tej biblioteki wyglądałby tak: YAHOO.lang.extend(Trójkąt, Figura)
Kopiowanie pól Inny sposób realizacji dziedziczenia to kopiowanie pól. Można zmniejszyć ilość potrzebnego kodu (a przecież o to właśnie chodzi w dziedziczeniu), kopiując pola obiektu-rodzica do obiektudziecka. Napiszmy teraz funkcję extend2(). extend2() ma taki sam interfejs jak extend(): pobiera dwa konstruktory i kopiuje wszystkie pola (w tym metody) z prototypu rodzica do prototypu dziecka. function extend2(Dziecko, Rodzic) { var p = Rodzic.prototype; var c = Dziecko.prototype; for (var i in p) { c[i] = p[i]; } c.uber = p; }
Jak widać, wystarczy pętla przechodząca przez wszystkie pola prototypu. Tak samo jak wcześniej, można dodać pole uber, umożliwiające dziecku dostęp do metod rodzica. W tym wypadku nie jest konieczne przestawianie wartości Dziecko.prototype.constructor, ponieważ prototyp dziecka jest rozszerzany, a nie całkowicie zamieniany. Ten sposób dziedziczenia jest mniej wydajny niż poprzedni, ponieważ podczas wykonania programu pola obiektu dziecko są duplikowane, a nie tylko sprawdzane wewnątrz łańcucha prototypów. Pamiętaj jednak, że wspomniany problem zachodzi tylko w sytuacji, gdy pola są typu prostego. Żadne obiekty (w tym również funkcje i tablice) nie będą duplikowane, ponieważ są przekazywane przez referencję. Przeanalizujmy to na przykładzie konstruktorów Figura() i Figura2D(). Prototyp konstruktora Figura() zawiera jedno pole typu prostego (nazwa) i jedno będące obiektem (metoda toString()): var Figura = function(){}; var Figura2D = function(){}; Figura.prototype.nazwa = 'figura'; Figura.prototype.toString = function(){return this.nazwa;};
182
Rozdział 6. • Dziedziczenie
Jeśli do realizacji dziedziczenia zostanie użyta funkcja extend(), ani instancja Figura2D(), ani jej prototyp nie będą miały pola nazwa. Będą za to miały dostęp do pola nazwa odziedziczonego z Figura(). >>> extend(Figura2D, Figura); >>> var f2 = new Figura2D(); >>> f2.nazwa
"figura" >>> Figura2D.prototype.name
"figura" >>> f2.__proto__.name
"figura" >>> f2.hasOwnProperty('nazwa')
false >>> f2.__proto__.hasOwnProperty('nazwa')
false Jeśli wykorzystana zostanie funkcja extend2(), prototyp konstruktora Figura2D() otrzyma własną kopię pola nazwa. Otrzyma także własną kopię toString(), jednak kopia ta jest referencją, zatem funkcja nie zostanie utworzona po raz drugi. >>> extend2(Figura2D, Figura); >>> var td = new Figura2D(); >>> f2.__proto__.hasOwnProperty('nazwa')
true >>> f2.__proto__.hasOwnProperty('toString')
true >>> f2.__proto__.toString === Shape.prototype.toString
true Wyraźnie widać, że metody toString() w praktyce są tą samą funkcją. To rozwiązanie jest bardzo korzystne, ponieważ nie ma potrzeby tworzenia duplikatów metod. Podsumowując: extend2() jest mniej wydajna niż extend(), ponieważ od nowa tworzy pola prototypu. Różnica w wydajności nie jest jednak aż tak straszna, ponieważ duplikowane są jedynie typy proste. Co więcej, w wielu sytuacjach rozwiązanie zastosowane w extend2() może okazać się bardziej korzystne, jako że zmniejszy się liczba kroków koniecznych do odnalezienia danego pola w łańcuchu prototypów.
183
JavaScript. Programowanie obiektowe
Uwaga na kopiowanie przez referencję! To, że obiekty (w tym funkcje i tablice) są kopiowane przez referencję, może niekiedy prowadzić do nieoczekiwanych rezultatów. Stwórzmy dwa konstruktory i dodajmy kilka pól do prototypu pierwszego z nich. >>> var A = function(){}, B = function(){}; >>> A.prototype.costam = [1,2,3];
[1, 2, 3] >>> A.prototype.nazwa = 'a';
"a" Niech B dziedziczy z A (nie ma znaczenia, czy za pomocą extend(), czy extend2()): >>> extend2(B, A);
Jeśli wykorzystane zostało extend2(), prototyp B odziedziczył pola prototypu A jako własne. >>> B.prototype.hasOwnProperty('nazwa')
true >>> B.prototype.hasOwnProperty('costam')
true Pole nazwa jest typu prostego, dlatego tworzona jest jego kopia. Pole costam jest tablicą (obiektem), dlatego jest kopiowane przez referencję: >>> B.prototype.costam
[1, 2, 3] >>> B.prototype.costam === A.prototype.costam
true Zmiana pola nazwa w B nie będzie miała żadnego wpływu na A: >>> B.prototype.nazwa += 'b'
"ab" >>> A.prototype.nazwa
"a" Jeśli jednak w B zostanie zmienione pole costam, zmiana będzie widoczna w A, ponieważ oba prototypy posiadają wskaźnik na tę samą tablicę.
184
Rozdział 6. • Dziedziczenie
>>> B.prototype.costam.push(4,5,6);
6 >>> A.prototype,costam
[1, 2, 3, 4, 5, 6] Co innego, jeśli kopia costam w B zostanie całkowicie nadpisana innym obiektem. W takim wypadku A zachowa wskaźnik na stary obiekt. >>> B.prototype.costam = ['a', 'b', 'c'];
["a", "b", "c"] >>> A.prototype.costam
[1, 2, 3, 4, 5, 6] Wyobraź sobie obiekt jako rzecz tworzoną i przechowywaną w pewnym miejscu w pamięci. Zmienne i pola jedynie wskazują dane miejsce. Przypisanie polu B.prototype.costam nowego obiektu jest jak wydanie rozkazu: „Zapomnij o starym obiekcie i przesuń wskaźnik w miejsce nowego”.
185
JavaScript. Programowanie obiektowe
Obiekty dziedziczą z obiektów Wszystkie przedstawione do tej pory przykłady dziedziczenia dotyczyły sytuacji, w których obiekty są tworzone za pomocą konstruktorów, a obiekty tworzone za pomocą jednego konstruktora mają dziedziczyć pola pochodzącego z innego. Prawdopodobnie zastanawiasz się, co z obiektami, które tworzone są bez użycia konstruktorów, za pomocą notacji literałowej (która, przy okazji, zajmuje mniej miejsca). W Javie czy PHP definiuje się klasy, które mogą dziedziczyć z innych klas (łatwo zapamiętać, że klasyczne programowanie obiektowe oparte jest na klasach). JavaScript nie ma klas. Programiści, którzy wcześniej pracowali w którymś z języków z klasami, uciekają się do konstruktorów, ponieważ to rozwiązanie najbardziej przypomina znane im mechanizmy. Na dodatek JavaScript posiada operator new, który wielu osobom może kojarzyć się z Javą. Prawda jest jednak taka, że JavaScript jest całkowicie oparty na obiektach i wszystkie opisane mechanizmy dziedziczenia na pewnym poziomie sprowadzają się do obiektów. Wróćmy do pierwszego z przykładów dziedziczenia pokazanych w tym rozdziale: Dziecko.prototype = new Rodzic();
Konstruktor Dziecko (lub, jeśli komuś wygodniej, klasa) dziedziczy z Rodzic. Odbywa się to jednak poprzez utworzenie obiektu (new Rodzic) i dziedziczenie z niego. Dlatego można tu mówić o pseudoklasowym wzorcu dziedziczenia: mechanizm przypomina dziedziczenie za pomocą klas, jednak w praktyce wszystko jest obiektem. Dlaczego zatem nie pozbyć się pośrednika (konstruktora/klasy) i nie dziedziczyć bezpośrednio z obiektów? W extend2() kopiowaliśmy pola prototypu obiektu-rodzica do prototypu obiektudziecka. Prototypy same są obiektami. Można zapomnieć o prototypach i konstruktorach i skopiować wszystkie pola jednego obiektu do drugiego. Można zacząć od utworzenia pustego obiektu (var o = {}) i stopniowo dodawać do niego pola. Alternatywnym sposobem jest rozpoczęcie tworzenia obiektu od skopiowania wszystkich pól obiektu już istniejącego. Właśnie to robi poniższa funkcja: pobiera obiekt i zwraca jego kopię. function extendCopy(p) { var c = {}; for (var i in p) { c[i] = p[i]; } c.uber = p; return c; }
Kopiowanie pól to prosty, ale często stosowany wzorzec. Funkcja extend() w kodzie Firebug jest zaimplementowana w ten właśnie sposób. Z tego samego wzorca korzystały wczesne wersje popularnych bibliotek, takich jak JQuery czy Prototype.
186
Rozdział 6. • Dziedziczenie
Zobaczmy, jak działa funkcja. Zaczniemy od utworzenia obiektu bazowego: var figura = { nazwa: 'figura', toString: function() {return this.nazwa;} }
W celu utworzenia obiektu dziedziczącego z figura wywołamy funkcję extendCopy(). Zwróci ona nowy obiekt, do którego będzie można dodawać nowe funkcjonalności. var dwaDe = extendCopy(figura); dwaDe.name = 'figura 2D; dwaDe.toString = function(){return this.uber.toString() + ', ' + this.nazwa;};
Obiekt trójkąt, dziedziczący z dwaDe: var trójkąt = extendCopy(dwaDe); trójkąt.name = 'trójkąt '; trójkąt.pobierzPole = function(){return this.bok * this.wysokość / 2;}
Wykorzystanie obiektu: >>> trójkąt.bok = 5; trójkąt.wysokość = 10; trójkąt.pobierzPole();
25 >>> trójkąt.toString();
"figura, figura 2D, trójkąt" Pewnym minusem tej metody dziedziczenia jest to, że trzeba jawnie przypisać wartości polom nowego obiektu trójkąt, co zajmuje więcej miejsca niż wywołanie konstruktora z odpowiednimi argumentami. Jednak można uniknąć tej niedogodności, implementując funkcję (można nazwać na przykład init() albo, jeśli ktoś lubi PHP, __construct()), która działa jak konstruktor pod tym względem, że przyjmuje i ustawia wartości początkowe.
Głębokie kopiowanie Pokazana powyżej funkcja extendCopy() tworzy tak zwaną płytką kopię obiektu. Jej przeciwieństwem jest oczywiście głęboka kopia. Jak już wspomniałem (w podrozdziale o dramatycznym tytule „Uwaga na kopiowanie przez referencję!”), podczas kopiowania obiektów kopiowane są jedynie wskaźniki do miejsc w pamięci, w których znajduje się dany obiekt — przynajmniej w przypadku płytkiej kopii. Zmiana wprowadzona w kopii jest widoczna w oryginalnym obiekcie. Głębokie kopie pozwalają uniknąć tego efektu. Głębokie kopiowanie, które zaimplementujemy w postaci funkcji deepCopy(), przebiega podobnie jak płytkie, z tą różnicą, że jeśli pętla przechodząca po polach obiektu napotka pole,
187
JavaScript. Programowanie obiektowe
które samo jest obiektem, zostanie na nim rekurencyjnie wywołana funkcja głębokiego kopiowania (czyli właśnie deepCopy()): function deepCopy(p, c) { var c = c || {}; for (var i in p) { if (typeof p[i] === 'object') { c[i] = (p[i].constructor === Array) ? .[] : {}; deepCopy(p[i], c[i]); } else { c[i] = p[i]; } } return c; }
Stwórzmy obiekt, którego polami będą tablice i inne obiekty. var rodzic = { liczby: [1, 2, 3], litery: ['a', 'b', 'c'], obj: { pole: 1 }, bool: true };
Sprawdźmy, czy wszystko działa jak należy. Zmiana pola liczby w głębokiej kopii nie powinna mieć wpływu na pole liczby oryginalnego obiektu. >>> var głęboka = deepCopy(rodzic); >>> var płytka = extendCopy(rodzic); >>> głęboka.liczby.push(4,5,6);
6 >>> głęboka.liczby
[1, 2, 3, 4, 5, 6] >>> rodzic.liczby
[1, 2, 3] >>> płytka.liczby.push(10)
4 >>> płytka.liczby
[1, 2, 3, 10]
188
Rozdział 6. • Dziedziczenie
>>> rodzic.liczby
[1, 2, 3, 10] >>> głęboka.liczby
[1, 2, 3, 4, 5, 6] Dziedziczenie za pomocą głębokiej kopii zostało zaimplementowane w nowszych wersjach jQuery.
object() W związku z tym, że obiekty dziedziczą z innych obiektów, Douglas Crockford zaproponował wykorzystanie funkcji object(), która jako parametr przyjmuje pewien obiekt i zwraca nowy obiekt, którego prototypem jest obiekt przekazany jako parametr. function object(o) { function F() {} F.prototype = o; return new F(); }
Jeśli potrzebny jest Ci dostęp do pola uber, możesz nieco zmienić funkcję object(): function object(o) { var n; function F() {} F.prototype = o; n = new F(); n.uber = o; return n; }
Zasada jest taka sama jak w przypadku extendCopy(): pobierany jest obiekt (jak dwaDe we wcześniejszym przykładzie), po czym tworzona jest jego kopia, którą następnie można rozszerzać. var trójkąt = object(dwaDe); trójkąt.nazwa = 'trójkąt'; trójkąt.pobierzPole = function(){return this.bok * this.wysokość / 2;};
Zachowanie obiektu nie zmieniło się: >>> trójkąt.toString()
"figura, figura 2D, trójkąt" Ten wzorzec dziedziczenia nazywa się prototypowym, ponieważ obiekt-rodzic staje się prototypem obiektu-dziecka.
189
JavaScript. Programowanie obiektowe
Połączenie dziedziczenia prototypowego z kopiowaniem pól Z dziedziczenia najczęściej korzysta się po to, by wykorzystać pewne istniejące funkcjonalności i, być może, rozszerzyć je. Tworzony jest nowy obiekt, który dziedziczy z już istniejącego i do którego dodaje się nowe pola i metody. Wszystkie te operacje można wykonać za pomocą pojedynczego wywołania funkcji, która łączy dwa omówione wcześniej podejścia. Możesz: Q wykorzystać dziedziczenie prototypowe w celu sklonowania istniejącego obiektu, Q skopiować wszystkie pola innego obiektu. function objectPlus(o, dodatki) { var n; function F() {} F.prototype = o; n = new F(); n.uber = o; for (var i in dodatki) { n[i] = dodatki[i]; } return n; }
Funkcja pobiera obiekt o (z którego będzie dziedziczył nowy obiekt) oraz obiekt dodatki (zawierający dodatkowe pola i metody, które mają być skopiowane). Zobaczmy, jak to działa. Zaczniemy od bazowego obiektu figura: var figura = { nazwa: 'figura', toString: function() {return this.nazwa;} };
Pora na utworzenie obiektu dwaDe, który będzie dziedziczył z figura, ale oprócz tego będzie posiadał pewne dodatkowe pola, które utworzymy za pomocą anonimowego literału obiektowego. var dwaDe = objectPlus(figura, { nazwa: 'figura 2D', toString: function(){return this.uber.toString() + ', ' + this.nazwa} });
Teraz obiekt trójkąt, który dziedziczy z dwaDe oraz posiada pewne dodatkowe pola. var trójkąt = objectPlus(dwaDe, { name: ' trójkąt ', pobierzPole: function(){return this.bok * this.wysokość / 2;},
190
Rozdział 6. • Dziedziczenie
bok: 0, wysokość: 0 });
Przetestujmy działanie programu na przykładzie trójkąta my o zdefiniowanej długości boku i wysokości. >>> var my = objectPlus(triangle, {bok: 4, wysokość: 4}); >>> my.pobierzPole()
8 >>> my.toString()
"figura, figura 2D, trójkąt, trójkąt" Gołym okiem widać jedną różnicę: toString() zwraca ciąg znaków, w którym wyraz "trójkąt" pojawia się dwa razy. Jest tak dlatego, że nasza konkretna instancja dziedziczy z trójkąt — mamy o jeden poziom dziedziczenia więcej. Nowa instancja mogłaby otrzymać własną nazwę: >>> var my = objectPlus(triangle, {bok: 4, wysokość: 4, nazwa: 'trójkąciątko'}); >>> my.toString()
"figura, figura 2D, trójkąt, trójkąciątko"
Dziedziczenie wielokrotne Jeśli dziecko dziedziczy z więcej niż jednego rodzica, mamy do czynienia z dziedziczeniem wielokrotnym. Niektóre języki obiektowe wspierają dziedziczenie wielokrotne, inne nie. Zwolennicy obu rozwiązań mają w zanadrzu mocne argumenty: że dziedziczenie wielokrotne jest bardzo wygodne, albo że to niepotrzebne komplikowanie architektury aplikacji, które spokojnie można zastąpić odpowiednim łańcuchem pojedynczego dziedziczenia. Tak czy inaczej, w językach dynamicznych (do których należy JavaScript) dziedziczenie wielokrotne bardzo łatwo zaimplementować, nawet jeśli język nie został wyposażony w specjalną składnię do obsługi tego mechanizmu. Przełóżmy dyskusję na temat wad i zalet dziedziczenia wielokrotnego na któryś z długich, zimowych wieczorów i zobaczmy, jak to naprawdę działa. Jak już wspomniałem, implementacja jest prosta. Przypomnij sobie dziedziczenie za pomocą kopiowania wartości i rozszerz je do postaci, w której źródłem kopiowania może być dowolna liczba obiektów. Napiszmy funkcję multi(), która akceptuje dowolną liczbę obiektów wejściowych. Pętlę kopiującą pola można umieścić w innej pętli, która przejdzie przez wszystkie obiekty przekazane jako argumenty funkcji.
191
JavaScript. Programowanie obiektowe
function multi() { var n = {}, stuff, j = 0, len = arguments.length; for (j = 0; j < len; j++) { dodatki = arguments[j]; for (var i in dodatki) { n[i] = dodatki [i]; } } return n; }
Przetestujmy dziedziczenie wielokrotne na przykładzie trzech obiektów: figura, dwaDe oraz trzeciego, nienazwanego obiektu. Utworzenie obiektu trójkąt będzie wymagało wywołania multi() i przekazania wszystkich trzech obiektów jako argumentów. var figura = { name: 'figura', toString: function() {return this.nazwa;} }; var dwaDe = { nazwa: 'figura 2D', wymiary: 2 }; var trójkąt = multi(figura, dwaDe, { nazwa: 'trójkąt', pobierzPole: function(){return this.bok * this.wysokość / 2;}, bok: 5, wysokość: 10 });
Sprawdźmy, czy kod działa zgodnie z oczekiwaniami: >>> trójkąt.pobierzPole()
25 >>> trójkąt.wymiary
2 >>> trójkąt.toString()
"trójkąt" Zwróć uwagę, że multi() przechodzi przez parametry zgodnie z kolejnością, w jakiej zostały podane. Jeśli dwa obiekty przekazane jako parametry będą miały pole o tej samej nazwie, przeważy pole obiektu, który pojawi się później.
192
Rozdział 6. • Dziedziczenie
Miksiny Być może znasz już termin miksin, dość popularny w językach takich jak Ruby. Miksin to obiekt, który dostarcza pewnych istotnych funkcjonalności, ale który nie powinien być dziedziczony ani rozszerzany przez inne obiekty. Przedstawione powyżej podejście do dziedziczenia wielokrotnego jest w pewnym sensie implementacją konceptu miksinów. Podczas tworzenia obiektu można wskazać dowolny zestaw obiektów, które mają zostać do niego włączone („wmiksowane”). Przekazując te obiekty do multi(), otrzymujesz obiekt dostarczający wszystkich wymaganych funkcjonalności bez wstawiania ich do drzewa dziedziczenia.
Dziedziczenie pasożytnicze Jeśli masz ochotę na jeszcze więcej możliwości implementacji dziedziczenia — bardzo proszę. Wzorzec, który zaraz przedstawię, został nazwany przez Douglasa Crockforda dziedziczeniem pasożytniczym. Sprowadza się on do tego, że pewna funkcja tworzy obiekt, zabierając funkcjonalności innemu obiektowi, rozszerza go i zwraca, „udając, że samodzielnie wykonała całą pracę”. Oto zwykły obiekt zdefiniowany za pomocą notacji literałowej. Jest on zupełnie nieświadomy faktu, że za chwilę stanie się ofiarą pasożyta: var dwaDe = { nazwa: 'figura 2D', wymiary: 2 };
Funkcja tworząca obiekt trójkąt może: Q sklonować obiekt dwaDe do obiektu o nazwie that (ang. „tamten”); Q dodać do that nowe pola; Q zwrócić that. function trójkąt(b, w) { var that = object(dwaDe); that.nazwa ='trójkąt'; that.pobierzPole = function(){return this.bok * this.wysokość / 2;}; that.bok = b; that.wysokość = w; return that; }
W związku z tym, że trójkąt() jest normalną funkcją, a nie konstruktorem, nie wymaga użycia operatora new. Ponieważ jednak zwraca obiekt, przypadkowe użycie new nie zmieni jej działania.
193
JavaScript. Programowanie obiektowe
>>> var t = trójkąt(5, 10); >>> t.wymiary
2 >>> var t2 = new trójkąt(5,5); >>> t2.pobierzPole();
12.5 Oczywiście that to tylko nazwa, pozbawiona specjalnego znaczenia, jakie posiada this.
Wypożyczanie konstruktora Kolejny (ostatni w tym rozdziale, przysięgam!) sposób implementacji dziedziczenia jest związany z konstruktorami, a nie z samymi obiektami. Wzorzec polega na tym, że konstruktor obiektu-dziecka wywołuje konstruktor obiektu-rodzica za pomocą metody call() lub apply(). Można to nazwać kradzieżą konstruktora lub, eufemistycznie, wypożyczeniem. Metody call() i apply() zostały omówione w rozdziale 4. Krótkie przypomnienie: pozwalają one wywoływać metody na zewnętrznym obiekcie, przekazywanym jako parametr, w taki sposób, że wartość this danej metody zostaje ustawiona na przekazany obiekt. Dziedziczenie przez wypożyczanie konstruktora polega na tym, że konstruktor dziecka wywołuje konstruktor rodzica, przypisując do this nowo utworzony obiekt-dziecko. Oto konstruktor rodzica: function Figura(id) { this.id = id; } Shape.prototype.nazwa = 'figura'; Shape.prototype.toString = function(){return this.nazwa;};
Zdefiniujmy teraz konstruktor Trójkąt(), który wywoła konstruktor Figura() za pomocą apply(), przekazując this oraz wszelkie dodatkowe argumenty. function Trójkąt() { Figura.apply(this, arguments); } Trójkąt.prototype.nazwa = 'trójkąt';
Zwróć uwagę, że zarówno Trójkąt(), jak i Figura() dodają pewne dodatkowe pola do swoich prototypów.
194
Rozdział 6. • Dziedziczenie
Utwórzmy nowy obiekt reprezentujący trójkąt: >>> var t = new Trójkąt(101); >>> t.nazwa
"Trójkąt" Nowy obiekt dziedziczy pole id rodzica, ale nie dziedziczy niczego, co zostało dodane do prototypu rodzica: >>> t.id
101 >>> t.toString();
"[object Object]" Obiekt t nie uzyskał dostępu do pól prototypu rodzica, ponieważ nie powstała żadna instancja new Figura() i prototyp w ogóle nie został użyty. Łatwo to poprawić w sposób pokazany na początku rozdziału. Trójkąt() można zmienić w następujący sposób: function Trójkąt() { Figura.apply(this, arguments); } Trójkąt.prototype = new Figura(); Trójkąt.prototype.nazwa = 'Trójkąt';
Podczas dziedziczenia według tego wzorca własne pola rodzica są odtwarzane jako własne pola dziecka (a nie pola prototypu, jak w przypadku dziedziczenia z łańcuchem prototypów). Jest to zresztą największa korzyść z wypożyczania konstruktorów: jeśli dziecko dziedziczy tablicę lub inny obiekt, jest to nowa wartość (a nie referencja), której zmiana nie doprowadzi do modyfikacji rodzica. Minusem jest to, że konstruktor rodzica jest wywoływany dwukrotnie: raz za pomocą apply() w celu odziedziczenia własnych pól, a drugi raz z new w celu odziedziczenia prototypu. Co więcej, w rzeczywistości własne pola rodzica zostaną odziedziczone dwukrotnie. Przeanalizujmy następujący uproszczony scenariusz: function Figura(id) { this.id = id; } function Trójkąt() { Figura.apply(this, arguments); } Trójkąt.prototype = new Figura(101);
Utwórzmy nowy egzemplarz: >>> var t = new Trójkąt(202); >>> t.id
202 195
JavaScript. Programowanie obiektowe
Obiekt ma własne pole id, jednak istnieje także druga wersja tego pola pochodząca z łańcucha prototypów: >>> t.__proto__.id
101 >>> delete t.id
true >>> t.id
101
Pożycz konstruktor i skopiuj jego prototyp Problem wynikający ze zdublowania pracy wykonywanej podczas wywoływania konstruktora łatwo naprawić. Możesz wywołać apply() na konstruktorze rodzica w celu pobrania wszystkich własnych pól, a następnie skopiować pola prototypu poprzez prostą iterację (lub za pomocą omówionego wcześniej extend2()). function Figura(id) { this.id = id; } Figura.prototype.nazwa = 'figura'; Figura.prototype.toString = function(){return this.nazwa;}; function Trójkąt() { Figura.apply(this, arguments); } extend2(Trójkąt, Figura); Trójkąt.prototype.nazwa = 'trójkąt';
Sprawdzenie: >>> var t = new Trójkąt(101); >>> t.toString();
"trójkąt" >>> t.id
101 Ani śladu podwójnego dziedziczenia: >>> typeof t.__proto__.id
"undefined"
196
Rozdział 6. • Dziedziczenie
Dodatkowo extend2() umożliwia dostęp do pola uber: >>> t.uber.name
"figura"
Podsumowanie W tym rozdziale przedstawiłem kilka sposobów (wzorców) implementacji dziedziczenia. Można je z grubsza podzielić na: Q wzorce związane z konstruktorami, Q wzorce związane z obiektami.
Można także rozróżnić wzorce w zależności od tego, czy: Q korzystają z prototypu, Q kopiują pola, Q łączą obie możliwości. Nr Nazwa wzorca Przykład
Klasyfikacja
Uwagi
1.
Dziecko.prototype = new Rodzic(); Łańcuchy prototypów (wzorzec pseudoklasowy)
Związany z konstruktorami.
Mechanizm domyślny, opisany w standardzie ECMA.
Dziedziczenie samego prototypu
Związany z konstruktorami.
2.
Dziecko.prototype = ´Rodzic.prototype;
Korzysta z prototypu.
Wskazówka: pola i metody wspólne dla wszystkich instancji należy przenieść do prototypu, a pozostałe przechowywać jako własne pola obiektu.
Większa efektywność, ponieważ nie tworzy się nowych instancji Kopiuje prototyp tylko w celu (brak łańcucha prototypów, obiekty dziedziczenia. dzielą ten sam Szybkie przeszukanie prototypu, ponieważ obiekt prototypu). nie ma łańcucha. Wada: dzieci mogą zmienić funkcjonalności rodziców.
197
JavaScript. Programowanie obiektowe
Nr Nazwa wzorca Przykład 3.
Konstruktor tymczasowy
function extend(Dziecko, Rodzic) { var F = function(){}; F.prototype = Rodzic.prototype; Dziecko.prototype = new F(); Dziecko.prototype.constructor = Dziecko; Dziecko.uber = Rodzic.prototype; }
Klasyfikacja
Uwagi
Związany z konstruktorami.
Inaczej niż w 1., dziedziczone są tylko pola prototypu. Nie są dziedziczone własne pola (utworzone za pomocą this wewnątrz konstruktora).
Korzysta z łańcucha prototypów.
Wykorzystywany w bibliotekach Ext.js i YUI. 4.
Kopiowanie pól prototypu
function extend2(Dziecko, Rodzic) { var p = Rodzic.prototype; var c = Dziecko.prototype; for (var i in p) { c[i] = p[i]; } c.uber = p; }
Związany z konstruktorami. Kopiuje pola. Korzysta z łańcucha prototypów.
Wszystkie pola prototypu rodzica stają się polami prototypu dziecka. Nie tworzy się nowych instancji jedynie na potrzeby dziedziczenia. Krótsze łańcuchy prototypów.
5.
6.
Kopiowanie wszystkich pól (płytkie)
function extendCopy(p) { var c = {}; for (var i in p) { c[i] = p[i]; } c.uber = p; return c; }
Związany z obiektami.
Głębokie kopiowanie
Jak wyżej, ale rekurencyjnie dla pól będących obiektami.
Związany z obiektami.
Kopiuje pola.
Kopiuje pola.
Bardzo prosty. Wykorzystywany w Firebug oraz wczesnych wersjach jQuery i Prototype.js. Nie korzysta z prototypów. Jak 5., ale obiekty są kopiowane przez wartość, a nie przez referencję. Stosowany w nowszych wersjach jQuery.
7.
Dziedziczenie prototypowe
198
function object(o){ function F() {} F.prototype = o; return new F(); }
Związany z obiektami. Korzysta z łańcucha prototypów.
Brak pseudoklas: obiekty dziedziczą z obiektów. Wykorzystanie właściwości prototypu.
Rozdział 6. • Dziedziczenie
Nr Nazwa wzorca Przykład
Klasyfikacja
Uwagi
8.
Związany z obiektami.
Połączenie dziedziczenia prototypowego (7.) i kopiowania pól (5.).
function objectPlus(o, dodatki) { Połączenie var n; dziedziczenia function F() {} prototypowego F.prototype = o; z kopiowaniem n = new F(); pól n.uber = o; (dziedziczenie for (var i in dodatki) { i rozszerzenie) n[i] = dodatki[i];
Kopiuje pola.
Jedno wywołanie umożliwia realizację dziedziczenia i rozszerzenie obiektu.
function multi() { var n = {}, dodatki, j = 0, len = arguments.length; for (j = 0; j < len; j++) { dodatki = arguments[j]; for (var i in dodatki) { n[i] = dodatki[i]; } } return n; }
Związany z obiektami.
Implementacja zbliżona do idei miksinów.
Kopiuje pola.
Kopiowanie wszystkich pól wszystkich obiektów-rodziców w kolejności ich pojawienia się na liście parametrów.
function pasożyt(ofiara) { var that = object(ofiara); that.more = 1; return that; }
Związany z obiektami.
Funkcja podobna do konstruktora tworzy obiekty.
function Dziecko() { Rodzic.apply(this, arguments); }
Związany z konstruktorami.
}
9.
Dziedziczenie wielokrotne
10 Dziedziczenie . pasożytnicze
11 Wypożyczanie . konstruktorów
Korzysta z łańcucha prototypów.
} return n;
Korzysta z łańcucha prototypów.
Kopiuje obiekt, rozszerza go i zwraca kopię. Dziedziczy tylko własne pola. Można połączyć z 1. w celu dziedziczenia także prototypu. Pozwala łatwo obejść problem związany z dziedziczeniem przez dziecko pól będących obiektami (przekazywanych przez referencję).
12 Wypożyczenie konstruktora . i skopiowanie jego prototypu
function Dziecko() { Rodzic.apply(this, arguments); } extend2(Dziecko, Rodzic);
Związany z konstruktorami. Korzysta z łańcucha prototypów. Kopiuje pola.
Połączenie 11. i 4. Pozwala dziedziczyć zarówno własne pola, jak i pola prototypu bez podwójnego wywoływania konstruktora rodzica.
199
JavaScript. Programowanie obiektowe
Którą z opcji wybrać? Wszystko zależy od Twojego stylu programowania, a także od projektu, konkretnego zadania i specyfiki zespołu. Łatwiej Ci projektować aplikację w oparciu o klasy? W takim wypadku wybierz któryś z wzorców korzystających z konstruktorów. Twoja „klasa” będzie miała tylko jedną lub kilka instancji? Wybierz wzorzec korzystający z obiektów. Czy to już wszystkie metody implementacji dziedziczenia? Bynajmniej. Możesz wybrać wzorzec z powyższej tabeli, połączyć kilka wzorców lub zaimplementować całkowicie autorski pomysł. Najważniejsze jest zrozumienie idei obiektów, prototypów i konstruktorów — reszta to pestka.
Studium przypadku: rysujemy kształty Chciałbym zakończyć ten rozdział praktycznym przykładem dziedziczenia. Zadanie jest następujące: napisać zawierający jak najmniej kodu (i powtórzeń) program obliczający pola i obwody różnych figur i potrafiący je rysować.
Analiza Konstruktor Shape (figura) będzie zawierać wszystkie wspólne elementy. Oprócz niego zaimplementujemy jeszcze konstruktory Triangle (trójkąt), Rectangle (prostokąt) oraz Square (kwadrat), wszystkie dziedziczące z Shape. Kwadrat to prostokąt, którego wszystkie boki mają równą długość, dlatego podczas tworzenia Square skorzystamy z Rectangle. Do definicji figur będziemy używać punktów o współrzędnych x i y. Figura może posiadać dowolnie wiele punktów. Trójkąt jest określany przez trzy punkty, a prostokąt (dla ułatwienia) przez jeden punkt oraz długości boków. Obwód dowolnej figury to suma długości jej boków. Pole zależy od kształtu, dlatego będzie implementowane osobno dla różnych figur. Wspólne funkcjonalności, które trafią do Shape, to: Q metoda draw() („rysuj”) potrafiąca narysować każdą figurę w oparciu o punkty; Q metoda getPerimeter() („pobierz obwód”); Q pole o nazwie points zawierające tablicę punktów; Q potrzebne metody pomocnicze.
Do rysowania wykorzystamy znacznik