JavaScript. Programowanie Obiektowe
JavaScript. Programowanie Obiektowe
ISBN: 978-83-246-5812-1
All rights reserved. No part of this book may be reproduced or transmitted in any
form or by any means, electronic or mechanical, including photocopying, recording
or by any information storage retrieval system, without permission from the Publisher.
Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: [email protected]
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie?jascob_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Printed in Poland.
O autorze 13
O recenzentach 15
Przedmowa 19
Co znajdziesz w tej książce? 19
Konwencje 20
Rozdział 1. Wprowadzenie 23
Trochę historii 24
Zapowiedź zmian 25
Teraźniejszość 26
Przyszłość 26
Programowanie obiektowe 27
Obiekty 27
Klasy 28
Kapsułkowanie 28
Agregacja 29
Dziedziczenie 29
Polimorfizm 30
Programowanie obiektowe — podsumowanie 30
Konfiguracja środowiska rozwijania aplikacji 31
Niezbędne narzędzia 31
Korzystanie z konsoli Firebug 32
Podsumowanie 33
Rozdział 3. Funkcje 73
Czym jest funkcja? 74
Wywoływanie funkcji 74
Parametry 74
Funkcje predefiniowane 76
parseInt() 76
parseFloat() 78
isNaN() 79
isFinite() 79
Encode/Decode URIs 80
eval() 80
Bonus — funkcja alert() 81
6
Spis treści
Zasięg zmiennych 81
Funkcje są danymi 83
Funkcje anonimowe 84
Wywołania zwrotne 84
Przykłady wywołań zwrotnych 85
Funkcje samowywołujące się 87
Funkcje wewnętrzne (prywatne) 87
Funkcje, które zwracają funkcje 88
Funkcjo, przepiszże się! 89
Domknięcia 90
Łańcuch zakresów 91
Zasięg leksykalny 91
Przerwanie łańcucha za pomocą domknięcia 93
Domknięcie 1. 94
Domknięcie 2. 95
Domknięcie 3. i jedna definicja 96
Domknięcia w pętli 96
Funkcje dostępowe 98
Iterator 99
Podsumowanie 100
Ćwiczenia 100
7
Spis treści
String 130
Ciekawe metody obiektu String 132
Math 135
Date 136
Metody działające na obiektach Date 138
RegExp 140
Pola obiektów RegExp 141
Metody obiektów RegExp 142
Metody obiektu String, których parametrami mogą być wyrażenia regularne 143
search() i match() 143
replace() 144
Wywołania zwrotne replace 145
split() 146
Przekazanie zwykłego tekstu zamiast wyrażenia regularnego 146
Obsługa błędów za pomocą obiektów Error 146
Podsumowanie 150
Ćwiczenia 151
8
Spis treści
9
Spis treści
10
Spis treści
Fabryka 278
Dekorator 280
Dekorowanie choinki 280
Obserwator 282
Podsumowanie 285
Skorowidz 323
11
Spis treści
12
O autorze
Stoyan Stefanov jest programistą aplikacji sieciowych w firmie Yahoo!, posiadaczem certyfi-
katu Zend oraz autorem książek. Często występuje na konferencjach poświęconych Java-
Scriptowi, PHP i innym zagadnieniom sieciowym. Swoje przemyślenia publikuje na swoim
blogu pod adresem www.phpied.com, a także na innych prowadzonych przez siebie stronach,
takich jak JSPatterns.com (strona poświęcona wzorcom w języku JavaScript). W Yahoo! Stoyan
nadzoruje tworzenie narzędzia optymalizującego wydajność o nazwie YSlow, poza tym bierze
udział w kilku projektach open-source, między innymi Firebug i PEAR.
Stoyan jest prawdziwym obywatelem świata. Urodził się i wychował w Bułgarii, ale ma także
obywatelstwo kanadyjskie. Obecnie mieszka w Los Angeles w Kalifornii. W rzadkich chwilach
wolnych od pracy gra na gitarze lub wypoczywa na którejś z plaż Santa Monica wraz z rodziną.
JavaScript. Programowanie obiektowe
14
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 zajmu-
ją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 nig-
dy nie odkleja się od klawiatury.
Douglas Crockford jest dziełem amerykańskiego systemu edukacji publicznej. Zawsze gło-
suje i posiada własny samochód. Jest największym żyjącym autorytetem w sprawach Java-
Scriptu. Jest autorem książki JavaScript — mocne strony (wydawnictwo Helion, Gliwice 2009).
Rozwijał systemy automatyzacji pracy biurowej. Badał gry i muzykę w firmie Atari. Był kierowni-
kiem ds. technologii w Lucasfilm, a także kierownikiem ds. nowych mediów w Paramount.
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 two-
rzenie stron internetowych, doradztwo oraz szkolenie techniczne.
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 oprogramowa-
nia. 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 roz-
poczęł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. Po-
stanowił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 apli-
kacji 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 wydaj-
ność”). 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 swo-
jej 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ów-
nież 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.
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 ska-
lowalnych 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 wiel-
kich 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
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ń.
Rozdział 2. omawia podstawy języka: zmienne, typy danych, tablice, pętle i instrukcje warun-
kowe.
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 zakre-
sie 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ł 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.
Dodatek C jest bardzo szczegółową ściągą z wszystkich metod i pól wszystkich obiektów
wbudowanych.
Konwencje
Fragmenty tekstu przedstawiające informacje różnego typu zostały sformatowane w odmien-
ny 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 od-
dziela się od wartości za pomocą dwukropka, jak w przykładzie klucz:wartość”.
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: „klik-
nięcie przycisku Dalej spowoduje przejście do następnego slajdu”.
Dobre rady
Dobre rady będą wyglądały tak.
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ą wie-
lofunkcyjne, 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 zastosowa-
nia. Programiści wykorzystują jego obiektowość do tworzenia skalowalnych aplikacji składa-
jących się z elementów, których uniwersalność pozwala na ich ponowne wykorzystanie. Naj-
popularniejszy 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.
Przejdźmy zatem do rozdziału 1., który krótko przedstawia historię języka JavaScript. Wpro-
wadzę 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 wy-
konania 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.
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 pomo-
cą 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.
1
Obecnie Europejskie Stowarzyszenie na rzecz Standaryzacji Systemów Informacyjnych i Komunika-
cyjnych — przyp. tłum.
24
Rozdział 1. • Wprowadzenie
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 funkcjo-
nalności dostarczanych przez obiekt XMLHttpRequest, który kiedyś obsługiwany był jedynie
przez IE, jednak został zaimplementowany w wielu przeglądarkach. XMLHttpRequest umożli-
wia 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. Najpopularniej-
szym ś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 interne-
towych, 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 stosowa-
ny (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ę skomplikowa-
ne, 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 pro-
gramista 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 pro-
gramowanie obiektowe. Oto lista pojęć, które często pojawiają się podczas rozmów o progra-
mowaniu obiektowym:
Q obiekt, metoda, pole;
Q klasa;
Q kapsułkowanie;
Q agregacja;
Q dziedziczenie;
Q polimorfizm.
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 pro-
gramowaniu obiektowym cechy obiektu nazywamy jego polami2, a jego czynności — metodami.
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.
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).
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.
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ę nie-
któ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 ro-
dzica, 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 anali-
zować kod, a wtedy okaże się, że wszystkie te abstrakcyjne koncepcje w praktyce okazują się
proste i pożyteczne.
Opis Koncepcja
Robert jest człowiekiem (obiektem). obiekty
Robert posiada dane osobowe — data urodzenia: 1 czerwca 1980, pola
płeć: męska, włosy: czarne.
Robert potrafi wykonać następujące polecenia: jedz, śpij, pij, śnij, metody
mów i oblicz swój wiek.
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ą kapsułkowanie
na tych danych (takie jak oblicz wiek).
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 agregacja, kompozycja
obiektem typu Projektant, oraz Jackiem, obiektem typu Kierownik
Projektu.
30
Rozdział 1. • Wprowadzenie
Opis Koncepcja
Projektant, Kierownik Projektu oraz Programista to obiekty dziedziczenie
dziedziczące z obiektu Osoba.
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 pro-
gramowanie. 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 dar-
mowy i działa na wszystkich najpopularniejszych platformach — pod Windowsem, Linuksem
i Mac OS. Można go pobrać ze strony http://www.mozilla.com/firefox/.
31
JavaScript. Programowanie obiektowe
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 od-
dzieli 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 rzeczywi-
stym 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 po-
winny 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 na-
stę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 zaawan-
sowane funkcjonalności obiektowe. Więcej informacji na tematy poruszone w tym rozdziale
można znaleźć na przestawionych poniżej stronach.
33
JavaScript. Programowanie obiektowe
34
2
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 przecho-
wywane 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.
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.
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;
Możesz przyspieszyć wpisywanie kodu, w trzeciej linii wpisując jedynie wie i wtedy wciska-
ją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 na-
stę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
"male"
>>> WIELKOSC_LITER_MA_ZNACZENIE
"wielkie"
W liniach zaczynających się trzema znakami większości (>>>) widać kod wpisany przez pro-
gramistę, 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 przy-
kład użycia operatora, w celu uzgodnienia terminologii:
>>> 1 + 2
W przykładzie:
Q + jest operatorem.
Q Operacją (działaniem) jest dodawanie.
37
JavaScript. Programowanie obiektowe
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
38
Rozdział 2. • Proste typy danych, tablice, pętle i warunki
Proste przypisanie wartości, na przykład var a = 1, również jest operacją. Znak = to operator
prostego przypisania.
39
JavaScript. Programowanie obiektowe
>>> a -= 3;
a -=3; odpowiada a = a – 3;
Podobnie:
>>> a *= 2;
10
>>> a /= 5;
2
>>> a %= 2
Oprócz operatorów arytmetycznych i przypisania istnieją jeszcze inne operatory, które poja-
wią się w tym i kolejnych rozdziałach książki.
Każda zmienna, która nie należy do żadnego z wymienionych pięciu typów prostych, jest obiek-
tem. 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
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ła-
dzie widać również, że słówko var jest obowiązkowe tylko podczas pierwszego nadania war-
tości zmiennej.
>>> var n = 1;
>>> typeof n;
"number"
>>> n = 1234;
1234
>>> typeof n;
"number"
"number"
"number"
41
JavaScript. Programowanie obiektowe
"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 wykorzy-
stuje się między innymi do definiowania kolorów w arkuszach stylów CSS.
"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 ze-
rem, 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 re-
prezentacji 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, naj-
wię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
Infinity
-Infinity
>>> typeof i
"number"
NaN
>>> -Infinity + Infinity
NaN
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ła-
nie 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"
45
JavaScript. Programowanie obiektowe
"string"
"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ą licz-
bami. 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 za-
mieniony (przekonwertowany) na liczbę (dotyczy to wszystkich działań z wyjątkiem dodawa-
nia, 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
Leniwa metoda zamiany dowolnego łańcucha znaków reprezentującego liczbę na liczbę to po-
mnoż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"
NaN
Leniwy sposób zamiany dowolnej wartości na łańcuch znaków polega na jej konkatenacji z pu-
stym ł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
Znaczenie Przykład
znaków
\ \ to znak uniku. >>> var s = 'Apostrof w środku łańcucha '!';
\\ Jeśli napis ma zawierać apostrofy Powyższe przypisanie spowoduje błąd, ponieważ
\' lub cudzysłowy, trzeba poprzedzić JavaScript uzna, że utworzyliśmy napis 'Apostrof w środku
je znakiem uniku, aby nie zostały łańcucha ', i nie będzie umiał przetworzyć reszty.
\"
uznane za koniec napisu. Poprawne są następujące fragmenty kodu:
Jeśli napis ma zawierać wsteczny >>> var s = 'Apostrof w środku łańcucha \'!';
>>> var s = "Apostrof w środku łańcucha \'!";
ukośnik (\), należy wpisać \\.
>>> var s = "Apostrof w środku łańcucha '!";
>>> var s = 'Powiedział "cześć"!';
>>> var s = "Powiedział \"cześć\"!";
Zastosowanie znaku uniku ze znakiem uniku:
>>> var s = "1\\2"; s;
"1\2"
47
JavaScript. Programowanie obiektowe
Łańcuch
Znaczenie Przykład
znaków
\n Koniec linii. >>> 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"
\t Znak tabulacji. >>> var s = "1\t2"
>>> s
"1 2"
\u Łańcuch \u pozwala korzystać ze Oto moje bułgarskie imię zapisane cyrylicą:
znaków Unicode. Należy po nim >>> "\u0421\u0442\u043E\u044F\u043D"
podać kod znaku.
" "
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 boolow-
skich. 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
true
Jeśli użyjemy operatora logicznego na wartości, która nie jest typu boolean, zostanie ona za-
mieniona na ten typ.
>>> var b = "jeden";
>>> !b;
false
true
Użycie podwójnej negacji jest prostym sposobem zmiany dowolnej wartości na jej odpowied-
nik 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 za-
mieniona na true. Oto wyjątki (które zostaną zamienione na false):
49
JavaScript. Programowanie obiektowe
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").
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
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
Jest tak dlatego, że mnożenie ma priorytet wyższy niż dodawanie, zatem najpierw obliczane
jest 2 * 3, co odpowiada zapisowi:
>>> 1 + (2 * 3)
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 ko-
du, 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
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ę nieczytel-
ny. 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 zdefi-
niowana, jej wartość zostaje zachowana; w przeciwnym przypadku zmienna zostaje zainicjali-
zowana 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.
53
JavaScript. Programowanie obiektowe
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.
"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
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ń arytme-
tycznych:
>>> var i = 1 + undefined; i;
NaN
>>> var i = 1 + null; i;
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
false
>>> !!null
false
"null"
>>> "" + undefined
"undefined"
55
JavaScript. Programowanie obiektowe
Q null.
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 na-
wiasó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.
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 kwadratowy-
mi. a[0] oznacza pierwszy element tablicy a, a[1] drugi i tak dalej.
>>> a[0]
1
>>> a[1]
"trzy"
>>> a
[1, 2, "trzy"]
57
JavaScript. Programowanie obiektowe
"cztery"
>>> a
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]
Aby odwołać się do elementów zagnieżdżonej tablicy, należy użyć następnego zestawu nawia-
sów kwadratowych.
>>> a[0][0]
1
>>> a[1][2]
"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.
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 spa-
cji. Zewnętrzny blok jest przesunięty o dwie spacje względem nawiasów, następny o cztery, a naj-
bardziej 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.
Warunki if
Oto prosty przykład warunku if:
var result = '';
if (a > 2) {
result = 'a jest większe od 2';
}
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ę!';
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.
Operator typeof zawsze zwraca łańcuch, który można porównać z "undefined". Należy pa-
mię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
""
>>> 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";
}
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 po-
siada 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.
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 pro-
gramista 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ą.
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.
65
JavaScript. Programowanie obiektowe
Pętla while
Po instrukcji while następuje para nawiasów z warunkiem oraz blok kodu w nawiasach klamro-
wych. 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
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ę zmien-
nej 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, po-
nieważ 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 in-
nej 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';
}
**********
**********
**********
**********
**********
**********
**********
**********
**********
**********
"
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 tro-
chę później). Jest to jedyne zastosowanie tej pętli — nie można stosować jej w ogólnych przypad-
kach, 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żde-
go 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.
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.
Następnie pokazałem Ci, jak korzystać z tablic w celu przechowywania danych oraz jak kon-
trolować 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 po-
niżej i postawienia sobie zasłużonej piątki w dzienniku przed przejściem do następnego roz-
dział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
72
3
Funkcje
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
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;
Parametry
Podczas definiowania funkcji można określić oczekiwane parametry. Funkcja nie musi pobie-
rać 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)
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');
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);
75
JavaScript. Programowanie obiektowe
>>> sumaNaSterydach();
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 przyj-
rzeć 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()
parseInt()
parseInt() pobiera argument dowolnego typu (najczęściej łańcuch znaków) i próbuje zamie-
nić 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 po-
brania liczby dziesiętnej z łańcucha "FF", zatem wynikiem będzie NaN, jednak jeśli potrak-
tujemy "FF" jako liczbę szesnastkową, otrzymamy wynik 255.
>>> parseInt('FF', 10)
NaN
>>> parseInt('FF', 16)
255
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 for-
mularza, 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
1.23
>>> parseFloat('123e2')
12300
78
Rozdział 3. • Funkcje
>>> parseInt('1e10')
isNaN()
Przy pomocy isNaN() można sprawdzić, czy wartość wejściowa jest liczbą, której można bez-
piecznie używać w operacjach arytmetycznych. isNaN() pozwala w wygodny sposób dowie-
dzieć 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
false
>>> isNaN('a1.23')
true
Funkcja isNaN() jest potrzebna także dlatego, że liczba NaN nie jest równa samej sobie. Wyni-
kiem 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 poprzed-
nim 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 przeka-
zany jej parametr jest tylko częścią URL (na przykład zawiera parametry żądania), i odpo-
wiednio 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);
"https%3A%2F%2Ffanyv88.com%3A443%2Fhttp%2Fwww.packtpub.com%2Fscr%20ipt.php%3Fq%3Dthis%
20and%20that"
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 uni-
kać. 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.
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 aktu-
alizowaną 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 global-
nych, 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
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 ist-
nieje. Jednak podczas pierwszego wywołania funkcji zmienna jest tworzona i ma zasięg glo-
balny. 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
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 wy-
konywania 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 ope-
rator 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 przy-
kł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 zmien-
nej lub nawet wykasować.
>>> var sum = function(a, b) {return a + b;}
>>> var add = sum;
>>> delete sum
true
83
JavaScript. Programowanie obiektowe
"undefined"
>>> typeof add;
"function"
>>> add(1, 2);
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ć.
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 zwra-
ca 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 jeden() {
return 1;
}
function dwa() {
return 2;
}
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 wy-
wołaniem zwrotnym (ang. callback function). Jeśli A nie ma nazwy, to jest anonimowym wy-
woł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.
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);
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
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 wy-
woł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
[3, 5, 7]
[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]
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.
87
JavaScript. Programowanie obiektowe
function a(param) {
function b(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);
b is not defined
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 nor-
malnej funkcji.
>>> var newFunc = a();
>>> newFunc();
Pierwsza linia powyższego kodu spowoduje wyświetlenie okienka z wiadomością 'A!', a dru-
ga — 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()();
Powyższa linia przy pierwszym uruchomieniu spowoduje wyświetlenie 'A!', jednak jej dru-
gie 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!');
};
}
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 odpo-
wiedzieć 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()?
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ą ja-
sne, 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 wi-
doczna 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 swo-
jego 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ła-
sne środowisko (zakres) podczas definicji, a nie podczas wywołania. Spójrzmy na przykład:
91
JavaScript. Programowanie obiektowe
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 po-
siadać 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
92
Rozdział 3. • Funkcje
true
>>> f1()
f2 is not defined
>>> var f2 = function(){return a * 2;}
>>> var a = 5;
5
>>> f1();
10
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 znala-
złeś się w punkcie c, który należy do funkcji N, możesz sięgnąć do przestrzeni globalnej, prze-
strzeni 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ą wy-
woł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 za-
miast 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;
}
}
Wewnątrz przestrzeni f() definiowana jest nowa funkcja. Ponieważ nie została użyta instruk-
cja 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
Argument przekazany funkcji wewnątrz niej jest dostępny jako zmienna globalna. Możesz stwo-
rzyć funkcję zwracającą inną funkcję, która z kolei zwraca argument przekazany rodzicowi.
function f(arg) {
var n = function(){
return arg;
};
arg++;
return n;
}
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. Funk-
cje 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
Mamy zatem tablicę z trzema funkcjami. Wywołajmy je, podając nawiasy po każdym elemen-
cie tablicy. Oczekiwane zachowanie to wypisanie numerów iteracji: 0, 1 i 2. Spróbujmy:
>>> a[0]()
3
>>> a[1]()
3
>>> a[2]()
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;
}
0
>>> a[1]();
97
JavaScript. Programowanie obiektowe
>>> a[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 po-
lega 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 utworze-
nie 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 wykorzy-
stanie 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łu-
jesz next() za każdym razem, gdy chcesz pobrać kolejną wartość. Na potrzeby przykładu wy-
korzystamy 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, zaw-
sze wskazujący następny element w tablicy:
function setup(x) {
var i = 0;
return function(){
return x[i++];
};
}
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 progra-
mowaniu w języku JavaScript powinno być dla Ciebie proste. Do tej pory unikaliśmy funkcjo-
nalności obiektowych, ale od tej chwili nie będziemy już tego robić. Powtórzmy materiał przed-
stawiony 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 prywatne funkcje i zmienne,
Q funkcje anonimowe,
Q wywołania zwrotne,
samowywołujące się funkcje,
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
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"
>>> 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 klu-
cze. Nie musisz ograniczać się do liczbowych indeksów. Możesz korzystać z bardziej przyja-
znych nazw, takich jak nazwisko, data_urodzenia czy wiek.
Nie zaleca się stosowania cudzysłowów (chociażby ze względu na oszczędność znaków), jed-
nak 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
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. Ta-
blice zdefiniowane za pomocą [] określa się mianem literałów tablicowych, a obiekty zdefi-
niowane za pomocą {} to literały obiektowe.
Pole obiektu może zawierać funkcję, ponieważ funkcje również są danymi. Takie pola nazy-
wamy 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
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 skorzy-
stać z notacji z kropką.
."Żółw"
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"
"Heller"
"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
107
JavaScript. Programowanie obiektowe
Jeśli metoda pobiera parametry, przekazujemy je dokładnie tak samo jak w przypadku zwy-
kł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']();
Pusty obiekt:
>>> var bohater = {};
"undefined"
Wywołanie metody:
>>> bohater.mowImie();
"Leonardo"
Usuwanie własności:
>>> delete bohater.imie;
true
108
Rozdział 4. • Obiekty
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"
Konstruktory
Obiekty można tworzyć także przy użyciu funkcji nazywanych konstruktorami. Przykład:
function Bohater() {
this.specjalizacja = 'Ninja';
}
"Ninja"
Przewagą tego sposobu tworzenia obiektów jest to, że konstruktory mogą przyjmować para-
metry. 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;
}
}
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 war-
tość 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 pro-
gramy napisane w języku JavaScript są uruchamiane wewnątrz środowiska (na przykład prze-
glądarki). Skoro wiesz już o istnieniu obiektów, musisz poznać całą prawdę: środowisko za-
pewnia 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
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óre-
go 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. Zawie-
ra 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 (za-
tem 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.
Poniższy przykład przedstawia prostą funkcję o nazwie factory() („fabryka”), która produkuje
obiekty:
function factory(name) {
return {
name: name
};
}
Object()
Można także korzystać z konstruktorów i zwracać obiekty inne niż this, czyli zmieniać stan-
dardowe zachowanie konstruktora. Już tłumaczę, jak to zrobić.
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 prze-
ciwnym 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
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.
114
Rozdział 4. • Obiekty
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 po-
ciągnie za sobą zmianę własności obiektu burek. Wynikiem porównania będzie true.
>>> mojPies === burek
true
false
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.
W rozdziale omawiam jedynie wybrane metody obiektów wbudowanych. Pełna lista znajduje
się w dodatku C.
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
"[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(). Po-
niż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
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();
118
Rozdział 4. • Obiekty
Niezależnie od tego, w jaki sposób została utworzona tablica, można dodawać do niej ele-
menty 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;
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"
"1,2,3,cztery"
>>> a.valueOf()
[1, 2, 3, "cztery"]
>>> a.constructor
Array()
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"
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
2
>>> a
[1, undefined]
Metoda push() dodaje element na koniec tablicy, zaś pop() usuwa ostatni element. Wywoła-
nie 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
"new"
>>> a
[3, 5, 1, 7, "test"]
Metoda sort() sortuje elementy tablicy, a także zwraca wynik sortowania. W poniższym przy-
kł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 ');
[3, 5]
>>> b = a.slice(0, 1);
[1]
>>> b = a.slice(0, 2);
[1, 3]
[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
Wypełnianie luki nowymi elementami nie jest obowiązkowe — można z niego zrezygnować:
>>> a.splice(1, 3)
[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.
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)
Konstruktorowi Function() przekazuje się najpierw nazwy parametrów, a potem kod źródło-
wy ciała funkcji (wszystko jako łańcuch znaków). Do utworzenia funkcji konieczne jest prze-
tworzenie kodu podanego w postaci tekstowej. Rozwiązanie to posiada wszystkie wady funk-
cji 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 de-
finicje są równoważne:
122
Rozdział 4. • Obiekty
[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().
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
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 na-
szą 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 roz-
dziale, 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 za-
mienić 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"
124
Rozdział 4. • Obiekty
"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ą wy-
pożyczać metody od innych obiektów i wywoływać je jak własne. Jest to prosty i skuteczny
sposób wielokrotnego wykorzystania kodu.
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');
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
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 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');
[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 aktual-
nie 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
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 kon-
struktorem 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
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 inne-
go 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, po-
nieważ 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
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 cieka-
wych 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
"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 za-
mieniona 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 wy-
padku można jednak podać opcjonalny drugi parametr, określający podstawę (domyślnie 10).
129
JavaScript. Programowanie obiektowe
"11"
>>> (3).toString(10);
"3"
String
Konstruktor String() służy do tworzenia obiektów przechowujących łańcuchy znaków. Ofe-
rują 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 przecho-
wują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
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 ra-
zem, 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 po-
zwala jednak traktować proste łańcuchy jak obiekty.
8
>>> "pomidor"[0]
"p"
>>> "ziemniak"["ziemniak".length - 1]
"k"
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
"[object Object]"
>>> String([1,2,3])
"1,2,3"
"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"
""
Metoda indexof() pozwala na przeszukiwanie łańcuchów. Jeśli istnieje chociaż jedno dopa-
sowanie, 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')
132
Rozdział 4. • Obiekty
Można również określić pozycję, od której ma się rozpocząć wyszukiwanie. Poniższy frag-
ment kodu znajdzie drugie wystąpienie litery "r", ponieważ rozpocznie sprawdzanie łańcu-
cha od pozycji 5.:
>>> s.indexOf('r', 5)
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')
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')
-1
Jeśli chcesz pominąć kwestię wielkości liter, możesz przed rozpoczęciem wyszukiwania za-
mienić wszystkie litery na małe:
>>> s.toLowerCase().indexOf('guru')
Wartość 0 oznacza, że wynik wyszukiwania znajduje się na początku łańcucha. Może to pro-
wadzić 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) {...}
"program"
133
JavaScript. Programowanie obiektowe
"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")
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"
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ąz-
ku 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 matema-
tycznych.
Pola i metody Math są stałymi — nie można zmieniać ich wartości. Ich nazwy pisane są wiel-
kimi 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
2.302585092994046
Nareszcie wiesz, jak zaimponować przyjaciołom, gdy któryś z nich (nieważne z jakiego dziw-
nego 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ę odpo-
wiadają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 wyzna-
czania pierwiastka kwadratowego służy metoda sqrt(), wartości trygonometryczne wyzna-
czają 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
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 prze-
chowującego datę. Można wybierać spomiędzy wielu różnych formatów:
>>> new Date('2009 11 12')
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ć konstrukto-
rowi 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.
137
JavaScript. Programowanie obiektowe
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łuma-
czona na 1 marca (2008 był rokiem przestępnym).
>>> new Date(2008, 1, 29)
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)
Jeśli funkcja Date() zostanie wywołana bez operatora new, zwróci łańcuch znaków reprezen-
tujący bieżącą datę, niezależnie do tego, czy podano jakiekolwiek parametry. Poniższe wywo-
łania zwracają aktualną (w chwili uruchomienia kodu) datę.
>>> Date()
138
Rozdział 4. • Obiekty
Utwórzmy obiekt:
>>> var d = new Date();
>>> d.toString();
1205051199562
>>> d.toString();
Pobranie miesiąca:
>>> d.getMonth();
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.
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));
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();
Zgadza się, będzie to środa (ang. Wednesday) — nie jest to najlepszy dzień na imprezę. W ta-
kim 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++) {
Wynik:
>>> stats;
RegExp
Wyrażenia regularne (ang. regular expressions) są niezwykle potężnym mechanizmem przeszuki-
wania 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.
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 cza-
sem 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 Java-
Scriptu ułatwiają korzystanie z wyrażeń regularnych. Dodatkowe informacje na temat wzor-
ców można znaleźć w dodatku D.
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 kon-
struktorowi RegExp(), należy umieścić go w cudzysłowie.
141
JavaScript. Programowanie obiektowe
Pole lastIndex jest jedynym, którego wartość można zmieniać po utworzeniu obiektu.
Kolejność liter nie ma znaczenia. Przekazanie danej litery powoduje ustawienie wartości po-
wią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
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
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 pierw-
szego elementu:
>>> /j.*t/i.exec("Javascript")[0]
"Javascript"
Obiekty tekstowe posiadają następujące metody przyjmujące jako parametry wyrażenia re-
gularne:
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');
["a"]
["a", "a"]
143
JavaScript. Programowanie obiektowe
["Java"]
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 uzy-
skasz za pomocą sekwencji $&. Poniższy fragment kodu poprzedza dopasowany tekst znakiem
podkreślnika:
>>> s.replace(/[A-Z]/g, "_$&");
"_Hello_Java_Script_World"
"_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
"_hello_java_script_world"
Przetestujmy to. Po pierwsze, utwórzmy zmienną, która będzie przechowywała tablicę argu-
mentów przekazanych podczas wywołania funkcji:
>>> var glob;
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];
}
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 od-
dzielonych przecinkami:
>>> var csv = 'raz, dwa,trzy ,cztery';
>>> csv.split(',');
Przykład:
>>> "test".replace('e', 'o')
"tost"
"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.
146
Rozdział 4. • Obiekty
Spowodujmy błąd i zobaczmy, co się stanie. Nasz przykład będzie próbował wywołać nieist-
niejącą funkcję. Wpisz w konsoli Firebug następujący kod:
>>> nieMaMnie ();
Jeśli strona zawiera błąd, w prawym dolnym rogu przeglądarki zamiast normalnej ikonki Fi-
rebug pojawi się:
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
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 wykony-
wany 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żyt-
kownika, ż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).
148
Rozdział 4. • Obiekty
Pojawi się okienko alert() pokazujące nazwę błędu i komunikat, a potem drugie okienko
o treści „Wreszcie!”.
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 sy-
gnalizujemy 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 zwra-
canych 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.
149
JavaScript. Programowanie obiektowe
Zamiast rzucania ogólnego błędu throw new Error('Dzielenie przez zero! ') możesz zdecydo-
wać 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 obiek-
tem. 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.
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
Ć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');
Możesz przejść przez wszystkie znaki łańcucha wejściowego za pomocą pętli for…in, traktując go jak
tablicę.
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 ')
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 — kie-
dy 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.
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 ko-
lejnym 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"
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.
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łko-
wicie go nadpisać innym obiektem:
Gadget.prototype = {
cena: 100,
ocena_uzytkownikow: 3,
informuj: function() {
return 'Ocena_uzytkownikow: ' + this.ocena_uzytkownikow + ', cena: ' +
this.cena;
}
};
"kamera"
>>> nowaZabawka.kolor;
"czarna"
>>> nowaZabawka.ktosTy();
100
>>> nowaZabawka.ocena_uzytkownikow;
3
>>> nowaZabawka.informuj();
157
JavaScript. Programowanie obiektowe
Nie wolno zapominać o tym, że obiekty są przekazywane przez referencję, zatem zmiana prototy-
pu pociąga za sobą zmiany we wszystkich dziedziczących z niego obiektach, nawet tych utwo-
rzonych wcześniej.
100
>>> nowaZabawka.pobierz('kolor');
"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ę odna-
leźć. Wówczas odszukany zostanie prototyp konstruktora, który został użyty podczas tworze-
nia obiektu (będzie to prototyp nowaZabawka.constructor.prototype). Jeśli prototyp posiada to
pole, zostanie pobrana jego wartość.
158
Rozdział 5. • Prototypy
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 obiek-
tem, 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 con-
structor.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]"
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"
true
>>> zabawka.nazwa;
"foo"
"aparat fotograficzny"
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
160
Rozdział 5. • Prototypy
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 pro-
totypu:
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
Wynik:
nazwa=kamera
kolor=czarna
metoda=function () { return 1; }
true
false
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.
162
Rozdział 5. • Prototypy
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
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;
"JavaScript"
"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.
"śmieć"
"undefined"
"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__
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
__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
"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.
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;
}
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ę wbudowa-
na metoda reverse() („odwróć”) działająca na napisach. Zaraz, zaraz, przecież tablice posia-
dają 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('');
}
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ór-
cy 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 stwo-
rzona 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
"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
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"
"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
"undefined"
>>> typeof azor.constructor.prototype.łapy
"number"
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 kilka razy pojawi się nazwisko Douglasa Crockforda. Nie sposób mówić o dzie-
dziczeniu w języku JavaScript bez powoływania się na jego dokonania. Poza filmikami wspo-
mnianymi 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 pro-
totypów.
Wiesz już, że każda funkcja posiada pole prototype, które zawiera obiekt. Jeśli funkcja zosta-
nie wywołana z operatorem new, zostanie utworzony nowy obiekt, niejawnie połączony z prototy-
pem. 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__, prze-
chowują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 posia-
da 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ól-
ny 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.
172
Rozdział 6. • Dziedziczenie
function Figura(){
this.nazwa = 'figura';
this.toString = function() {return this.nazwa;};
}
function Figura2D(){
this.nazwa = 'figura 2D';
}
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 zo-
stał zastąpiony nowym obiektem Figura2D. Należy pamiętać (dotyczy to zwłaszcza osób przy-
zwyczajonych 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.
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"
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()
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 efek-
tywne, jednak można je stosować tylko w przypadku pól, których wartości nie różnią się po-
mię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 no-
wym 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, nie-
zależnie od wymiarów trójkąta. Tak jak poprzednio, najpierw należy opisać dziedziczenie, a dopie-
ro 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;};
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 odna-
leziona w Figura.prototype, a nie w instancji new Figura(), co miało miejsce w poprzednim
przykładzie.
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
function Figura2D(){}
// obsługa dziedziczenia
Figura2D.prototype = Figura.prototype;
Figura2D.prototype.constructor = Figura2D;
// rozszerzenie prototypu
Figura2D.prototype.nazwa = 'figura 2D';
177
JavaScript. Programowanie obiektowe
25
>>> my.toString()
"trójkąt"
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 war-
tość "trójkąt":
>>> var f = new Figura()
>>> f.nazwa
"trójkąt"
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.
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';
// 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
"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ć.
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 prze-
chowywał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
// 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;};
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()
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 zarezerwo-
wanym 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).
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 obiektu-
dziecka. Napiszmy teraz funkcję extend2(). extend2() ma taki sam interfejs jak extend(): po-
biera dwa konstruktory i kopiuje wszystkie pola (w tym metody) z prototypu rodzica do pro-
totypu 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ę.
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ła-
sną kopię pola nazwa. Otrzyma także własną kopię toString(), jednak kopia ta jest referencją, za-
tem 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ą je-
dynie 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
[1, 2, 3]
>>> A.prototype.nazwa = 'a';
"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
"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'];
[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
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 Java-
Script 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 dzie-
dziczenia na pewnym poziomie sprowadzają się do obiektów. Wróćmy do pierwszego z przy-
kł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 obiektu-
dziecka. Prototypy same są obiektami. Można zapomnieć o prototypach i konstruktorach i sko-
piować 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
Wykorzystanie obiektu:
>>> trójkąt.bok = 5; trójkąt.wysokość = 10; trójkąt.pobierzPole();
25
>>> trójkąt.toString();
Pewnym minusem tej metody dziedziczenia jest to, że trzeba jawnie przypisać wartości po-
lom nowego obiektu trójkąt, co zajmuje więcej miejsca niż wywołanie konstruktora z odpo-
wiednimi 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.
187
JavaScript. Programowanie obiektowe
które samo jest obiektem, zostanie na nim rekurencyjnie wywołana funkcja głębokiego ko-
piowania (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;
}
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]
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;};
Ten wzorzec dziedziczenia nazywa się prototypowym, ponieważ obiekt-rodzic staje się pro-
totypem obiektu-dziecka.
189
JavaScript. Programowanie obiektowe
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 (za-
wierają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 po-
siadał 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
});
8
>>> my.toString()
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 — ma-
my 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()
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 wielokrot-
nego 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ę ko-
piującą pola można umieścić w innej pętli, która przejdzie przez wszystkie obiekty przekaza-
ne 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 trze-
ciego, 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
};
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, prze-
waż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ć dziedzi-
czony 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 („wmik-
sowane”). 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 funkcjo-
nalnoś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
};
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
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.
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
"Trójkąt"
Nowy obiekt dziedziczy pole id rodzica, ale nie dziedziczy niczego, co zostało dodane do pro-
totypu 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 ta-
blicę 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. Przeanali-
zujmy 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);
202
195
JavaScript. Programowanie obiektowe
Obiekt ma własne pole id, jednak istnieje także druga wersja tego pola pochodząca z łańcu-
cha prototypów:
>>> t.__proto__.id
101
>>> delete t.id
true
>>> t.id
101
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
"undefined"
196
Rozdział 6. • Dziedziczenie
"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.
197
JavaScript. Programowanie obiektowe
198
Rozdział 6. • Dziedziczenie
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ć wzo-
rzec 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.
Analiza
Konstruktor Shape (figura) będzie zawierać wszystkie wspólne elementy. Oprócz niego zaimple-
mentujemy 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 rysowania wykorzystamy znacznik <canvas>. Co prawda nie jest on wspierany przez prze-
glądarkę Internet Explorer — jednak całe to zadanie ma być tylko przykładem.
Przydadzą się nam dwa konstruktory pomocnicze: Point („punkt”) oraz Line („linia”). Point
będzie potrzebny podczas definiowania figur, a Line ułatwi obliczenia, na przykład zwracając
długość boku figury (czyli odległość pomiędzy dwoma punktami).
200
Rozdział 6. • Dziedziczenie
Implementacja
Na początek dodajmy do pustej strony HTML znacznik <canvas> („płótno”):
<canvas height="600" width="800" id="canvas" />
Zapamiętaj, że współrzędne punktów na płótnie zaczynają się od x=0, y=0 (lewy górny róg)
i kończą na x=800, y=600 (prawy dolny róg).
Konstruktor Line pobiera dwa punkty i oblicza odległość pomiędzy nimi przy pomocy twierdzenia
Pitagorasa a2 + b2 = c2 (wyobraź sobie trójkąt prostokątny, w którym przeciwprostokątna łą-
czy dane dwa punkty).
function Line(p1, p2) {
this.p1 = p1;
this.p2 = p2;
this.length = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
}
201
JavaScript. Programowanie obiektowe
Pora na konstruktor Shape. Figury będą przechowywały definiujące je punkty (i linie pomię-
dzy nimi) jak własne pola. Poza tym konstruktor wywołuje metodę inicjalizującą, init(), która
zostanie zdefiniowana w prototypie.
function Shape() {
this.points = [];
this.lines = [];
this.init();
}
202
Rozdział 6. • Dziedziczenie
Następny w kolejce jest konstruktor prostokąta. Pobiera jeden punkt (lewy górny) oraz długości
dwóch boków. Następnie wypełnia tablice points w oparciu o te dane.
function Rectangle(p, side_a, side_b){
this.points = [
p,
203
JavaScript. Programowanie obiektowe
Mamy już wszystkie konstruktory, więc zajmijmy się dziedziczeniem. W tym wypadku sprawdzi
się dowolny spośród wzorców z konstruktorami (a nie obiektami). Proponuję skorzystać ze
zmienionej i uproszczonej wersji łańcucha prototypów (pierwszy wzorzec opisany w tym roz-
dziale). Należy utworzyć nową instancję rodzica i ustawić ją jako prototyp dziecka. Nie jest
konieczne tworzenie osobnej instancji dla każdego z dzieci — wystarczy im jeden współ-
dzielony prototyp.
(function () {
var s = new Shape();
Triangle.prototype = s;
Rectangle.prototype = s;
Square.prototype = s;
})()
Testowanie
Sprawdźmy, czy skrypt poprawnie rysuje kształty. Zacznijmy od definicji punktów tworzących
trójkąt:
>>> var p1 = new Point(100, 100);
>>> var p2 = new Point(300, 100);
>>> var p3 = new Point(200, 0);
482.842712474619
204
Rozdział 6. • Dziedziczenie
>>> t.getArea()
10000.000000000002
5000
>>> r.getPerimeter()
300
Kwadrat:
>>> var s = new Square(new Point(130, 130), 50);
>>> s.draw();
>>> s.getArea()
2500
>>> s.getPerimeter()
200
Rysowanie figur to sama przyjemność. Żeby oszczędzić sobie wysiłku, możemy ponownie wyko-
rzystać jeden z punktów trójkąta, definiując nowy kwadrat:
>>> new Square(p1, 200).draw()
Ćwiczenia
Pobaw się trochę przedstawionym powyżej kodem. Spróbuj wykonać następujące czynności:
1. Narysuj kilka trójkątów, kwadratów i prostokątów.
2. Dodaj konstruktory dla innych figur i kształtów, takich jak trapez, romb, latawiec
(szczególny przypadek rombu) czy pięciokąt.
205
JavaScript. Programowanie obiektowe
3. Zastanów się, czy ten sam efekt można osiągnąć, stosując inny wzorzec
dziedziczenia?
4. Wybierz jedną z metod dziedziczenia, która umożliwia dzieciom dostęp do rodziców
za pomocą uber. Zaimplementuj funkcjonalność, dzięki której rodzice będą mieli
dostęp do wszystkich swoich dzieci, na przykład za pomocą pola dzieci zawierającego
tablicę referencji.
206
7
Środowisko
przeglądarki
Wiesz już, że programy napisane w języku JavaScript nie działają samodzielnie, tylko we-
wnątrz środowiska. Właściwie wszystkie zagadnienia opisane w poprzednich rozdziałach do-
tyczyły JavaScriptu zgodnego ze standardem ECMAScript, który może być stosowany w wielu
różnych środowiskach. Ten rozdział dla odmiany jest poświęcony określonemu środowisku,
w którym JavaScript może zaprezentować pełnię możliwości: przeglądarce internetowej. Poruszo-
ne zostaną następujące tematy:
Q BOM (Browser Object Model, obiektowy model przeglądarki);
Q DOM (Document Object Model, obiektowy model dokumentu);
Q zdarzenia przeglądarki;
Q obiekt XMLHttpRequest.
<body>
<script type="text/javascript">
var a = 1;
a++;
</script>
</body>
</html>
Pierwszy znacznik <script> w powyższym przykładzie załącza zewnętrzny plik, plik.js, z ko-
dem w języku JavaScript. Drugi <script> zawiera kod JS osadzany bezpośrednio w kodzie
HTML. W obu przypadkach <script> ma atrybut type, dla którego XHTML 1.0 jest opisany
jako obowiązkowy w specyfikacji, jednak w praktyce kod zadziała także bez niego. Przeglądarka
wykonuje kod JS w kolejności jego napotkania. Oznacza to, że zmienna zdefiniowana w pliku
plik.js będzie istniała również wewnątrz drugiego bloku <script>.
Pierwszą grupę obiektów określa się mianem obiektowego modelu dokumentu (DOM), a drugą
— obiektowego modelu przeglądarki (DOM).
DOM to standard ustalany przez organizację World Wide Web Consortium (W3C). Ma on
kilka wersji, nazywanych poziomami. Ostatnią jak dotąd wersją jest poziom trzeci. Współczesne
przeglądarki implementują ten standard w różnym stopniu, można jednak założyć, że wszystkie są
zgodne z poziomem pierwszym. DOM jest standardem ustalonym post factum — powstał już
po tym, jak producenci przeglądarek zaimplementowali własne sposoby dostępu do doku-
mentu. Elementy, które były stosowane przed interwencją W3C, określa się często mianem
poziomu zero, mimo że w rzeczywistości nie istnieje taki standard. Część spośród elementów
DOM 0 została dodana do poziomu pierwszego, zaś pozostała część była zbyt zależna od po-
szczególnych przeglądarek i nie ma sensu jej omawiać.
BOM nie jest częścią żadnego standardu. Podobnie jak w przypadku DOM 0, istnieje pewien
zbiór obiektów wspólnych dla wszystkich przeglądarek, zaś pozostałe obiekty różnią się w zależ-
ności od producenta.
W tym rozdziale zajmę się tylko elementami BOM wspólnymi dla wszystkich przeglądarek
oraz DOM na poziomie pierwszym (wyraźnie zaznaczę wszelkie odstępstwa od tej zasady).
Ten „bezpieczny” podzbiór elementów sam w sobie jest bardzo duży i nie starczyłoby na nie-
go miejsca w tej książce. W razie wątpliwości polecam:
208
Rozdział 7. • Środowisko przeglądarki
BOM
BOM (Browser Object Model, obiektowy model przeglądarki) to zbiór obiektów dających dostęp
do przeglądarki i ekranu. Największe znaczenie mają tu globalne obiekty window (okno) oraz
window.screen (ekran).
1
>>> zmienna
123
>>> window.parseInt('123a456')
123
Poza pełnieniem funkcji obiektu globalnego window ma jeszcze jedno zadanie: dostarcza da-
nych na temat środowiska przeglądarki. Wszystkie ramki, pływające ramki iframe, wyskakujące
okienka czy zakładki (karty) przeglądarki mają własne obiekty window.
Przyjrzyjmy się zatem polom obiektu window związanym z przeglądarką. Implementacja nie-
których z nich zależy od przeglądarki, dlatego my zajmiemy się tylko tymi, na których można
polegać w każdym wypadku.
209
JavaScript. Programowanie obiektowe
window.navigator
navigator to obiekt wyposażony w wiedzę o przeglądarce i jej możliwościach. Jednym z jego
pól jest navigator.userAgent. Pole to zawiera długi tekst identyfikujący przeglądarkę. W Fire-
foksie wygląda on mniej więcej tak:
>>> window.navigator.userAgent
W Internet Explorerze:
Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET
CLR 2.0.50727; .NET CLR 3.0.04506.30)
Ponieważ przeglądarki różnią się funkcjonalnościami, programiści używają pola userAgent w celu
rozpoznania typu przeglądarki i wywołania odpowiedniego kodu. Poniższy kod sprawdza, czy
ma do czynienia z Internet Explorerem, poprzez wyszukanie w tekście fragmentu "MSIE":
if (navigator.userAgent.indexOf('MSIE') !== -1) {
// tak, to IE
} else {
// nie, to inna przeglądarka
}
Lepiej jednak nie polegać na tym mechanizmie, a zamiast niego wykorzystywać wykrywanie
możliwości. Problem polega na tym, że trudno jest nadążyć za wysypem nowych przeglądarek
i nowych wersji. Łatwiej jest po prostu sprawdzić, czy funkcja, z której chcemy skorzystać,
jest dostępna w przeglądarce użytkownika. Na przykład:
if (typeof window.addEventListener === 'function') {
// funkcja jest dostępna, skorzystajmy z niej
} else {
// hm, brak funkcji, trzeba to jakoś obejść
}
Kolejny powód, dla którego nie warto polegać na wykrywaniu typu przeglądarki, jest taki, że
niektóre z nich pozwalają użytkownikom na zmianę tekstu w polu navigator.userAgent i udawa-
nie, że korzystają oni z innej przeglądarki.
210
Rozdział 7. • Środowisko przeglądarki
window.location
Pole location zawiera obiekt przechowujący informacje o adresie URL aktualnie załadowanej
strony. Przykładowo location.href to pełny adres URL, zaś location.hostname to sama domena.
Pełną listę pól obiektu location można wyświetlić za pomocą prostej pętli.
211
JavaScript. Programowanie obiektowe
location posiada jeszcze trzy metody: reload() (przeładuj), assign() (przypisz) i replace()
(zamień).
Istnieje nadspodziewanie wiele sposobów przejścia na inną stronę. Oto kilka z nich:
>>> window.location.href = 'http://helion.pl'
>>> location.href = 'http://helion.pl'
>>> location = 'http://helion.pl'
>>> location.assign('http://helion.pl')
Metoda replace() działa prawie tak samo jak assign(). Jedyna różnica polega na tym, że zmiana
adresu za pomocą replace() nie spowoduje dodania nowej pozycji w historii przeglądarki.
>>> location.replace('http://www.yahoo.com')
Nie trzeba się zbytnio przemęczać. Kolejny sposób osiągnięcia tego samego celu to wpisanie:
>>> location = location
window.history
Pole window.history daje (ograniczony) dostęp do stron odwiedzonych wcześniej w ramach
tej samej sesji przeglądarki. Przykładowo możesz sprawdzić, ile adresów odwiedził użytkownik,
zanim udał się na Twoją stronę:
>>> window.history.length
Nie możesz jednak obejrzeć samych adresów URL. Z powodu ochrony prywatności użytkow-
ników nie jest możliwe uruchomienie następującego kodu:
>>> window.history[0]
Możesz za to odwiedzić strony, na które wszedł użytkownik, poruszając się wstecz lub w przód,
jakby został wciśnięty przycisk „przejdź do poprzedniej strony” lub „przejdź do następnej strony”.
>>> history.forward() //w przód
>>> history.back() //wstecz
212
Rozdział 7. • Środowisko przeglądarki
window.frames
Pole window.frames przechowuje zbiór ramek z bieżącej strony. Nie ma tu rozróżnienia na zwykłe
ramki i pływające ramki iframe. Niezależnie od tego, czy strona zawiera ramki, czy nie, pole
window.frames zawsze jest zdefiniowane i wskazuje window.
>>> window.frames === window
true
Jeśli chcesz sprawdzić, czy strona zawiera ramki, sprawdź wartość pola length:
>>> frames.length
Każda ramka zawiera inną stronę, która posiada swój własny globalny obiekt window. Załóżmy,
że na stronie znajduje się jedna ramka:
<iframe name="ramka" src="about:blank" />
Dostęp do obiektu window tej ramki można uzyskasz za pomocą dowolnej z poniższych linii kodu:
>>> window.frames[0]
>>> window.frames[0].window
>>> frames[0].window
>>> frames[0]
Możesz sięgnąć do ramek z poziomu strony, na której są osadzone. Możesz przeładować zde-
finiowaną powyżej ramkę w następujący sposób:
>>> frames[0].window.location.reload()
true
213
JavaScript. Programowanie obiektowe
Przy użyciu pola o nazwie top możesz z wnętrza dowolnej ramki uzyskać dostęp do strony naj-
wyższej w hierarchii (czyli do strony, która zawiera wszystkie ramki):
>>> window.frames[0].window.top === window
true
>>> window.frames[0].window.top === window.top
true
>>> window.frames[0].window.top === top
true
true
>>> frames[0].self == frames[0].window
true
Jeśli ramka posiada atrybut name, możesz odwołać się do niej poprzez nazwę, a nie tylko przez
indeks.
>>> window.frames['ramka'] === window.frames[0]
true
window.screen
Pole screen zawiera informacje na temat ustawień monitora. Pole screen.colorDepth pozwala
odczytać głębię koloru (jakoś wyświetlania kolorów) monitora. Tego typu dane można wykorzystać
podczas wyznaczania statystyk.
>>> window.screen.colorDepth
32
1440
>>> screen.availWidth //dostępna szerokość
1440
214
Rozdział 7. • Środowisko przeglądarki
900
>>> screen.availHeight //dostępna wysokość
847
Różnica pomiędzy height a availHeight polega na tym, że height to całkowita wysokość, podczas
gdy availHeight to height po odjęciu wysokości elementów systemu operacyjnego, takich jak
pasek zadań w Windows — analogicznie w przypadku width i availWidth.
window.open() i window.close()
Omówiłem najważniejsze pola obiektu window, których implementacja jest stabilna w prze-
glądarkach różnych producentów. Przejdę teraz do omawiania metod tego obiektu. Zacznę od
open(). Metoda pozwala otwierać nowe okna przeglądarki (okienka pop-up). Może się okazać,
że polityka producenta lub ustawienia użytkownika blokują wyskakujące okienka (z powodu
ich nadużywania w celach reklamowych), jednak zasadniczo otworzenie nowego okna powinno się
powieść, jeśli proces ten został zapoczątkowany przez użytkownika. Jeśli tak nie jest, a próba
otwarcia nowego okna ma miejsce w czasie ładowania głównej strony (i nie wynika bezpośrednio
z czynności użytkownika), nowe okno może zostać zablokowane.
win jest referencją do obiektu window nowego okienka. Jeśli okienko zostało zablokowane, win
otrzyma wartość fałszywą.
215
JavaScript. Programowanie obiektowe
window.moveTo(), window.resizeTo()
Przedstawię teraz dodatkowe, pochodzące z zamierzchłej przeszłości metody irytowania
użytkowników (o ile pozwalają na to ich ustawienia).
Q window.moveTo(100,100) przeniesie okno przeglądarki na pozycję x=100 i y=100
(licząc od lewego górnego rogu).
Q window.moveBy(10,-10) przeniesie okno o 10 pikseli w prawo i o 10 w górę
względem jego obecnego położenia.
Q window.resizeTo(x,y) i window.resizeBy(x,y) przyjmują te same parametry
co metody przesuwające okno, ale zamiast przesuwania zmieniają jego rozmiar.
Powiem to jeszcze raz: zawsze staraj się rozwiązywać problemy programistyczne bez ucieka-
nia się do tych metod.
alert() nie jest funkcją z ECMAScript, tylko metodą BOM. Istnieją jeszcze dwie metody
BOM umożliwiające komunikację z użytkownikiem za pomocą komunikatów systemowych:
Q confirm() daje użytkownikowi wybór pomiędzy OK i Anuluj.
Q prompt() pobiera tekst.
Jak to działa:
>>> var info = confirm('Wszystko gra?'); console.log(info);
W efekcie pojawi się okienko o wyglądzie zbliżonym do widocznego na obrazku (detale zależą
od przeglądarki i systemu operacyjnego):
Zauważ, że:
Q Do momentu zamknięcia okna w konsoli Firebug nie pojawi się żadna nowa
informacja.
Q Kliknięcie OK zwraca true, a
216
Rozdział 7. • Środowisko przeglądarki
Q kliknięcie Anuluj,
Q zamknięcie okna za pomocą krzyżyka (X) w prawym górnym rogu,
Q zamknięcie okna za pomocą klawisza Esc zwraca false.
Musisz jednak zatroszczyć się również o użytkowników, którzy wyłączyli obsługę JavaScriptu
(oraz o roboty indeksujące wyszukiwarek internetowych), i zapewnić im inną metodę po-
twierdzania chęci wykonania określonych czynności.
Funkcja pobiera opcjonalny drugi parametr, którego wartość jest wyświetlana jako domyślny
tekst w polu edycji.
window.setTimeout(), window.setInterval()
Metody setTimeout()i setInterval() pozwalają określić moment wykonania fragmentów kodu.
setTimeout() (ustaw opóźnienie) sprawia, że kod zostanie uruchomiony po upływie określonej
liczby milisekund. setInterval() (ustaw interwał) powoduje wielokrotne wywołania kodu co
pewien interwał (również wyrażony za pomocą liczby milisekund).
217
JavaScript. Programowanie obiektowe
Poniższy kod spowoduje wyświetlenie okienka dialogowego po upływie 2 sekund (czyli 2000
milisekund):
>>> function uuu(){alert('Uuu!');}
>>> setTimeout(uuu, 2000);
Funkcja zwróciła liczbę 4, która jest identyfikatorem opóźnienia. Identyfikator może zostać
użyty w celu usunięcia opóźnienia za pomocą funkcji clearTimeout(). Spójrz na poniższy kod
— jeśli uda Ci się zareagować odpowiednio szybko (przed upływem dwóch sekund), okienko
dialogowe nie zostanie wyświetlone.
>>> var id = setTimeout(uuu, 2000);
>>> clearTimeout(id);
Zmieńmy działanie funkcji uuu() tak, by stała się ona nieco mniej irytująca:
>>> function uuu() {console.log('Uuu!')};
Możemy teraz, przy użyciu setInterval(), zaplanować wywołania funkcji uuu() w taki sposób,
by była uruchamiana co dwie sekundy aż do momentu anulowania planu wywołań za pomocą
clearInterval().
>>> var id = setInterval(uuu, 2000);
Uuu!
Uuu!
Uuu!
Uuu!
Uuu!
Uuu!
>>> clearInterval(id)
Zwróć uwagę na to, że obie funkcje jako pierwszy argument przyjmują wskaźnik do funkcji.
Zamiast niego można przekazać łańcuch znaków, który zostanie uruchomiony za pomocą eval().
Pamiętaj jednak, że funkcja eval() to samo zło i należy unikać jej stosowania. A co, jeśli wy-
woływana zwrotnie funkcja potrzebuje argumentów? W takim wypadku możesz umieścić ją
wewnątrz innej funkcji.
218
Rozdział 7. • Środowisko przeglądarki
window.document
window.document to obiekt BOM związany z aktualnie załadowanym dokumentem (stroną).
Jego pola i metody należą już do grupy obiektów DOM. Weź zatem głęboki oddech (możesz
też zerknąć na ćwiczenia z BOM umieszczone na końcu rozdziału) — pora przenieść się do
świata DOM.
DOM
DOM (Document Object Model, obiektowy model dokumentu) pozwala reprezentować do-
kumenty XML i HTML w postaci drzew. Pola i metody DOM umożliwiają dostęp do wszystkich
elementów strony, przy czym elementy można zmieniać, usuwać lub dodawać. DOM to interfejs
programistyczny (API) niezależny od języka (co oznacza, że może on zostać zaimplementowa-
ny w dowolnym języku). Możesz na przykład generować strony internetowe po stronie serwera za
pomocą implementacji DOM w PHP (http://php.net/dom).
219
JavaScript. Programowanie obiektowe
jest jego rodzicem. Relacje typu rodzic – dziecko można przedstawić graficznie w postaci
drzewa genealogicznego, nazywanego drzewem DOM.
Wszystkie znaczniki są pokazane jako rozwijalne wierzchołki (inaczej węzły) drzewa. Widocz-
ne w wielu miejscach słowo #text również reprezentuje węzły, ale są one innego typu (węzły
tekstowe) i nie można już ich dalej rozwijać1. Przykładem węzła tekstowego jest słowo "second"
wewnątrz znacznika EM. Białe znaki także są uznawane za węzły tekstowe, dlatego pomiędzy
znacznikiem BODY a pierwszym P pojawia się napis #text, mimo że nie ma tam żadnego kodu.
Komentarze także są węzłami. Komentarz <!-- koniec --> pojawia się w drzewie jako węzeł
#comment.
Na powyższym zrzucie ekranu widoczne jest narzędzie DOM Inspector, będące dodatkiem
do przeglądarki Firefox 3 (https://addons.mozilla.org/pl/firefox/addon/6622). Po instalacji narzędzie
staje się dostępne poprzez menu Narzędzia/DOM Inspector.
W lewej części okna aplikacji widoczne jest drzewo DOM, natomiast po prawej wyświetlane
są dodatkowe informacje na temat aktualnie zaznaczonego węzła. Na powyższym zrzucie pre-
zentowany jest widok JavaScript Object, który nie jest widokiem domyślnym. Widoki można
przełączać za pomocą menu pokazanego na poniższym zrzucie.
1
W matematyce taki wierzchołek jest nazywany liściem drzewa — przyp. tłum.
220
Rozdział 7. • Środowisko przeglądarki
Każdy węzeł drzewa jest obiektem, którego wszystkie pola i metody można obejrzeć w wido-
ku JavaScript Object dodatku DOM Inspector. Widać tam także konstruktor, który został
użyty podczas tworzenia obiektu. Nie jest to może wiedza potrzebna na co dzień, niemniej
jednak warto zauważyć, że obiekt window.document jest tworzony przy użyciu konstruktora
HTMLDocument(), obiekt reprezentujący znacznik head — za pomocą HTMLHeadElement() itd. Nie
jest jednak możliwe samodzielne tworzenie obiektów przy użyciu tych konstruktorów.
221
JavaScript. Programowanie obiektowe
Powyższej listy obiektów Core DOM i HTML DOM pod żadnym pozorem nie można uznać
za zamkniętą. Pełen wykaz znajduje się pod adresem http://www.w3.org/TR/DOM-Level-1/.
Teoria już za nami, pora skoncentrować się na praktyce. Z najbliższych stron rozdziału do-
wiesz się, jak:
Q uzyskać dostęp do węzłów DOM,
Q zmieniać węzły,
Q tworzyć nowe węzły,
Q usuwać węzły.
222
Rozdział 7. • Środowisko przeglądarki
Węzeł document
document oferuje dostęp do bieżącego dokumentu. W celu przeanalizowania zawartości tego
obiektu kolejny raz skorzystamy z narzędzia Firebug jako ze ściągi. Wpisz document, a następnie
kliknij wynik.
Firebug pokaże zakładkę DOM, w której możesz przeglądać wszystkie pola i metody obiektu
document.
Wszystkie węzły (wśród nich węzeł dokumentu, węzły tekstowe, węzły elementów, węzły atry-
butów) posiadają pola nodeType (typ węzła), nodeName (nazwa węzła), and nodeValue (wartość
węzła).
>>> document.nodeType
Istnieje 12 typów węzłów. Poszczególnym typom zostały przypisane liczby całkowite. Jak wi-
dać powyżej, węzłowi dokumentu odpowiada liczba 9. Najbardziej przydatne typy to 1 (element),
2 (atrybut) i 3 (tekst).
Węzły mają także nazwy. W przypadku znaczników HTML nazwa węzła odpowiada nazwie
znacznika (pole tagName). Węzły tekstowe mają nazwę #text, a węzły dokumentu:
>>> document.nodeName
"#document"
223
JavaScript. Programowanie obiektowe
Węzły mogą mieć wartości. Wartością węzłów tekstowych jest przechowywany w nich tekst.
Węzeł dokumentu nie posiada wartości:
>>> document.nodeValue
null
documentElement
Co oprócz węzła document można znaleźć w strukturze drzewa XML? Każdy dokument po-
siada węzeł-korzeń, który opakowuje pozostałą część dokumentu. W przypadku dokumentu
HTML jest to znacznik <html>. Dostęp do korzenia jest możliwy dzięki polu documentElement
obiektu document.
>>> document.documentElement
<html>
"HTML"
>>> document.documentElement.tagName
"HTML"
Węzły-dzieci
Możesz sprawdzić, czy węzeł posiada jakiekolwiek dzieci, przy użyciu hasChildNodes():
>>> document.documentElement.hasChildNodes()
true
Element html ma dwoje dzieci — elementy head i body. Dostęp do nich jest możliwy przy
użyciu tablicopodobnej kolekcji childNodes.
>>> document.documentElement.childNodes.length
2
>>> document.documentElement.childNodes[0]
<head>
224
Rozdział 7. • Środowisko przeglądarki
>>> document.documentElement.childNodes[1]
<body>
<html>
Jakim cudem body ma aż dziewięcioro dzieci? Policzmy: trzy akapity plus jeden komentarz to
cztery węzły. Białe znaki pomiędzy tymi węzłami to kolejne 3 węzły (tekstowe) — jak dotąd, nali-
czyliśmy siedem węzłów. Ósmy węzeł to białe znaki pomiędzy znacznikiem body a pierwszym p,
dziewiąty to znaki pomiędzy komentarzem a zamykającym znacznikiem </body>. Wszystko się
zgadza.
Atrybuty
Ponieważ pierwszym dzieckiem węzła body jest sekwencja białych znaków, pierwszy akapit
jest drugim dzieckiem (o indeksie 1):
>>> bd.childNodes[1]
<p class="opener">
true
225
JavaScript. Programowanie obiektowe
>>> bd.childNodes[1].attributes.length
Dostęp do atrybutu jest możliwy za pomocą indeksu lub nazwy. Wartość można pobrać przy
użyciu metody getAttribute().
>>> bd.childNodes[1].attributes[0].nodeName
"class"
>>> bd.childNodes[1].attributes[0].nodeValue
"opener"
>>> bd.childNodes[1].attributes['class'].nodeValue
"opener"
>>> bd.childNodes[1].getAttribute('class')
"opener"
"P"
Tekst akapitu możesz pobrać, korzystając z pola textContent. Pole to nie istnieje w Internet
Explorerze, gdzie należy skorzystać z analogicznego pola innerText.
>>> bg.childNodes[1].textContent
"first paragraph"
Jest jeszcze pole innerHTML. Nie jest ono częścią standardu DOM, ale istnieje we wszystkich wio-
dących przeglądarkach. Umożliwia ono dostęp do kodu HTML zawartego wewnątrz znaczni-
ka. Jest to nieco sprzeczne z ideą DOM, zgodnie z którą dokument powinien być traktowany
jako drzewo z węzłami, a nie jako łańcuch znaków zawierający znaczniki. Jednak w praktyce
pole innerHTML jest bardzo przydatne i na pewno niejednokrotnie się na nie natkniesz.
>>> bd.childNodes[1].innerHTML
"first paragraph"
Pierwszy akapit (pierwszy znacznik <p>) zawiera tylko tekst, zatem innerHTML ma taką samą
wartość jak textContent (lub innerText w IE). Drugi akapit zawiera jednak dodatkowy węzeł em:
>>> bd.childNodes[3].innerHTML
"<em>second</em> paragraph"
226
Rozdział 7. • Środowisko przeglądarki
>>> bd.childNodes[3].textContent
"second paragraph"
Inny sposób pobrania tekstu pierwszego akapitu polega na wykorzystaniu nodeValue węzła tek-
stowego zawartego wewnątrz węzła p:
>>> bd.childNodes[1].childNodes.length
1
>>> bd.childNodes[1].childNodes[0].nodeName
"#text"
>>> bd.childNodes[1].childNodes[0].nodeValue
"first paragraph"
getElementsByTagName() pobiera nazwę (nazwę węzła elementu) i zwraca kolekcję HTML (obiekt
tablicopodobny) węzłów o danej nazwie:
>>> document.getElementsByTagName('p').length
Możesz odwołać się do wybranego elementu listy przy użyciu nawiasów kwadratowych lub meto-
dy item(), w obu przypadkach przekazując indeks elementu (liczony od 0). Raczej nie zaleca się
stosowania item(), ponieważ nawiasy kwadratowe są bardziej czytelne i szybciej się je zapisuje.
>>> document.getElementsByTagName('p')[0]
<p class="opener">
>>> document.getElementsByTagName('p').item(0)
<p class="opener">
"first paragraph"
227
JavaScript. Programowanie obiektowe
Ostatnie p:
>>> document.getElementsByTagName('p')[2]
<p id="closer">
Atrybuty można odczytać za pomocą kolekcji attributes lub omówionej już metody getAttribute().
Najłatwiej jednak wykorzystać nazwę atrybutu w postaci pola elementu. Aby sięgnąć do atrybutu
id, wystarczy odwołać się do id jako do pola obiektu:
>>> document.getElementsByTagName('p')[2].id
"closer"
Niestety nie powiedzie się próba pobrania w ten sposób atrybutu class pierwszego akapitu.
class jest wyjątkiem — jest to słowo zarezerwowane w specyfikacji ECMAScript. Zamiast tego
można skorzystać z className:
>>> document.getElementsByTagName('p')[0].className
"opener"
We wcześniejszych wersjach przeglądarki IE '*' nie może być nazwą znacznika. W celu pobrania
wszystkich elementów w tej przeglądarce można skorzystać z kolekcji document.all (chociaż
możliwość pobrania wszystkich elementów rzadko kiedy okazuje się przydatna). Od wersji 7
wspierane jest wywołanie document.getElementsByTagName('*'), jednak zwróci ono wszystkie
węzły, a nie tylko węzły elementów.
<p id="closer">
2
Sibling to po angielsku brat lub siostra, jednak wygodniej będzie posługiwać się jednym słowem, stąd
tłumaczenie rówieśnik — przyp. tłum.
228
Rozdział 7. • Środowisko przeglądarki
"\n "
>>> akapit.previousSibling
"\n "
>>> akapit.previousSibling.previousSibling
<p>
>>> akapit.previousSibling.previousSibling.previousSibling
"\n "
>>> akapit.previousSibling.previousSibling.nextSibling.nextSibling
<p id="closer">
Element body jest używany tak często, że dorobił się własnego skrótu:
>>> document.body
<body>
>>> document.body.nextSibling
null
>>> document.body.previousSibling
<head>
Pomocne mogą się okazać również pola firstChild (pierwsze dziecko) i lastChild (ostatnie
dziecko). firstChild jest równoważne childNodes[0], a lastChild odpowiada childNodes
´[childNodes.length - 1].
>>> document.body.firstChild
"\n "
>>> document.body.lastChild
"\n "
>>> document.body.lastChild.previousSibling
229
JavaScript. Programowanie obiektowe
Poniższy rysunek ukazuje powiązania rodzinne pomiędzy węzłem body oraz trzema zawarty-
mi w nim akapitami. Dla uproszczenia węzły tekstowe odpowiadające białym znakom zostały
pominięte.
230
Rozdział 7. • Środowisko przeglądarki
"final!!!"
Jako że innerHTML akceptuje nie tylko łańcuchy znaków, ale także kod źródłowy, możesz utworzyć
wewnątrz akapitu nowy węzeł em:
>>> my.innerHTML = '<em>my</em> final';
"<em>my</em> final"
<em>
>>> my.firstChild.firstChild
"my"
Inny sposób zmiany tekstu to pobranie węzła tekstowego i zmiana jego pola nodeValue:
>>> my.firstChild.firstChild.nodeValue = 'your';
"your"
Modyfikacja stylu
Często celem nie jest zmiana zawartości węzła, tylko zmiana sposobu prezentacji. Elementy
posiadają pole style, którego pola odpowiadają poszczególnym własnościom CSS. Dodajmy do
akapitu czerwone obramowanie:
>>> my.style.border = "1px solid red";
Własności CSS często zawierają łączniki (-), które z kolei nie są dopuszczalne w nazwach ję-
zyka JavaScript. W tego typu sytuacjach należy opuścić łącznik, a kolejną literę zamienić na
wielką. W ten sposób padding-top zamieni się w paddingTop, margin-left w marginLeft itd.
>>> my.style.fontWeight = 'bold';
"bold"
Możesz modyfikować atrybuty niezależnie od tego, czy wcześniej były ustawione, czy nie:
>>> my.align = "right";
"right"
231
JavaScript. Programowanie obiektowe
>>> my.name
>>> my.name = 'myname';
"myname"
>>> my.id
"closer"
>>> my.id = 'further'
"further"
Zabawa formularzami
Jak już wspominałem, JavaScipt doskonale nadaje się do walidacji formularzy po stronie klienta,
co pozwala zmniejszyć liczbę żądań wysyłanych do serwera. Przećwiczmy teraz obsługę for-
mularzy na przykładzie formularza z bardzo znanej strony — google.pl.
Wypisując inputs[0], inputs[2] itd., przekonasz się, że pierwsze pole jest ukryte, drugie to
pole, w które użytkownik wpisuje zapytanie, trzecie to przycisk Szukaj w Google, czwarte to
Szczęśliwy traf, piąte i szóste to przyciski opcji wyszukiwania (Szukaj w Internecie lub Szukaj
na stronach kategorii: język polski), zaś na końcu znajdują się dodatkowe dwa pola ukryte.
"q"
232
Rozdział 7. • Środowisko przeglądarki
"moje zapytanie"
"Pechowy traf"
Nie będziemy wywoływać funkcji ręcznie. Zamiast tego ustawmy interwał, dzięki któremu
funkcja będzie wywoływana co sekundę.
>>> var myint = setInterval(przełącz, 1000);
Wynik? Przycisk zacznie mrugać, co sprawi, że pechowa osoba nie zdąży na czas go kliknąć.
Kiedy znudzi Ci się ten efekt, po prostu usuń interwał.
>>> clearInterval(myint)
233
JavaScript. Programowanie obiektowe
"jeszcze jeden"
Nowy element będzie miał wszystkie domyślne pola, takie jak style, które będzie można mo-
dyfikować:
>>> myp.style
CSSStyleDeclaration length=0
>>> myp.style.border = '2px dotted blue'
Nowy węzeł możesz dodać do drzewa DOM za pomocą appendChild(). Wywołanie tej metody
na rzecz document.body spowoduje utworzenie dodatkowego dziecka zaraz za ostatnim dotych-
czas dzieckiem:
>>> document.body.appendChild(myp)
234
Rozdział 7. • Środowisko przeglądarki
W ten sposób możesz utworzyć dowolną liczbę węzłów tekstowych i zagnieździć je wewnątrz
siebie w dowolny sposób. Załóżmy, że na końcu węzła body chcesz dodać następujący frag-
ment kodu HTML:
<p>jeszcze jeden akapit <strong>pogrubiony</strong></p>
cloneNode()
Inny sposób tworzenia węzłów polega na kopiowaniu (inaczej klonowaniu) węzłów już ist-
niejących. Metoda cloneNode() tworzy kopię węzła. Przyjmuje ona parametr typu boolean:
true oznacza, że chcemy utworzyć głęboką kopię (kopiowane są także dzieci); false, że kopia
ma być płytka. Przetestujmy tę metodę.
Nie uda się zaobserwować żadnej różnicy w wyglądzie strony, ponieważ w wyniku płytkiego
kopiowania powstał nowy węzeł P bez żadnych dzieci. Oznacza to, że nie został skopiowany
235
JavaScript. Programowanie obiektowe
węzeł tekstowy, będący dzieckiem węzła P. Efekt wykonania powyższego kodu odpowiada
następującemu poleceniu:
>>> document.body.appendChild(document.createElement('p'));
Co innego w przypadku głębokiej kopii: zostanie skopiowane całe poddrzewo DOM, począw-
szy od P, w tym węzły tekstowe oraz element EM.
>>> document.body.appendChild(el.cloneNode(true))
<em>
"second"
insertBefore()
Korzystając z appendChild(), możesz dodawać nowo powstałe węzły jedynie na końcu wybra-
nego elementu. Więcej władzy daje metoda insertBefore() (wstaw przed), pozwalająca do-
kładnie określić docelową lokalizację elementu. Działa ona tak jak appendChild(), ale przyjmuje
dodatkowy parametr. Parametr ten pozwala wskazać element, przed który zostanie wstawiony
nowy węzeł. Pokażę to na przykładzie. Poniższy kod wstawia węzeł tekstowy na końcu ele-
mentu body:
>>> document.body.appendChild(document.createTextNode('boo!'));
Następujący kod tworzy taki sam węzeł i wstawia go jako pierwsze dziecko body:
document.body.insertBefore(
document.createTextNode('boo!'),
document.body.firstChild
);
Usuwanie węzłów
Do usuwania węzłów z drzewa DOM służy metoda removeChild() (usuń dziecko). Przetestujmy
ją na przykładzie tego samego dokumentu, dostępnego na stronie http://www.phpied.com/files/
´jsoop/ch7.html:
<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
236
Rozdział 7. • Środowisko przeglądarki
<p id="closer">final</p>
<!-- and that's about it -->
</body>
Metoda zwraca usuwany węzeł na wypadek, gdyby był on potrzebny podczas dalszych operacji.
Można wywoływać na nim wszystkie metody DOM, tyle że element nie jest już częścią drzewa:
>>> removed
<p>
>>> removed.firstChild
<em>
Jest jeszcze metoda replaceChild() (zastąp dziecko), która usuwa węzeł i wstawia w jego miej-
sce inny. Po wykonaniu powyższego kodu drzewo dokumentu ma następującą postać:
<body>
<p class="opener">first paragraph</p>
<p id="closer">final</p>
<!-- and that's about it -->
</body>
<p id="closer">
Tak samo jak removeChild(), metoda replaceChild() zwraca referencję do węzła, który został
usunięty z drzewa.
>>> replaced
<p id="closer">
237
JavaScript. Programowanie obiektowe
Szybki sposób usunięcia całej zawartości poddrzewa to przypisanie polu innerHTML pustego
łańcucha znaków. Poniższy kod wymazuje wszystkie dzieci elementu BODY:
>>> document.body.innerHTML = '';
""
Sprawdźmy:
>>> document.body.firstChild
null
Usuwanie treści przy użyciu innerHTML jest bardzo proste. Gdybyśmy jednak chcieli trzymać się
specyfikacji DOM, musielibyśmy przejść przez wszystkie węzły-dzieci i usunąć każdy z nich
z osobna. Oto funkcja, która usuwa wszystkie węzły będące dziećmi węzła n:
function removeAll(n) {//usuń wszystkie
while (n.firstChild) {
n.removeChild(n.firstChild);
}
}
Usunięcie wszystkich dzieci elementu BODY (sprowadzenie elementu do pary znaczników <body>
´</body>):
>>> removeAll(document.body);
Jednym z obiektów istniejących tylko w HTML jest document.body. Znacznik <body> istnieje
w prawie każdym dokumencie HTML i bardzo często jest potrzebny — dlatego właśnie powstał
obiekt o wiele wygodniejszy w stosowaniu niż odpowiadający mu document.getElementsByTag
´Name('body')[0].
238
Rozdział 7. • Środowisko przeglądarki
Przyciski i pola tekstowe są dostępne poprzez pole elements kolekcji. Pierwsze pole tekstowe
pierwszego formularza na stronie można pobrać za pomocą linii:
>>> document.forms[0].elements[0]
Atrybuty danego znacznika są reprezentowane jako pola obiektu. Jeśli pierwsze pole pierw-
szego formularza ma postać:
<input name="search" id="search" type="text" size="50" maxlength="255"
value="Podaj adres e-mail..." />
to tekst wyświetlany w tym polu (wartość value atrybutu) można zmienić w następujący sposób:
>>> document.forms[0].elements[0].value = '[email protected]'
Jeśli formularz lub jego elementy posiadają atrybut name, można odwołać się do nich za pomocą
jego wartości:
239
JavaScript. Programowanie obiektowe
document.write()
Metoda document.write() pozwala dodawać do strony kod HTML w czasie jej ładowania.
Możesz napisać coś takiego:
<p>Aktualna data to <script>document.write("<em>" + new Date() +
"</em>");</script></p>
Efekt będzie taki sam, jak gdyby data została na sztywno wpisana w kodzie strony:
<p> Aktualna data to <em>Sat Feb 23 2008 17:48:04 GMT+0100</em></p>
Zapamiętaj, że document.write() używa się tylko podczas ładowania strony. Jeśli spróbujesz
zrobić to później, nadpiszesz treść całej strony.
document.write() przydaje się rzadko. Jeśli wydaje Ci się, że musisz z niej skorzystać, zastanów
się jeszcze raz. Z reguły ten sam efekt można osiągnąć przy pomocy zalecanych i bardziej ela-
stycznych metod DOM poziomu pierwszego.
"CNNid=Ga50a0c6f-14404-1198821758-6; SelectedEdition=www;
s_sess=%20s_dslv%...
240
Rozdział 7. • Środowisko przeglądarki
"Mój tytuł"
Należy zwrócić uwagę, że powyższy kod nie zmienia wartości znacznika <title>, tylko tytuł wy-
świetlany w oknie przeglądarki. Nie jest to wartość pobierana za pomocą document.getElements
´ByTagName('title')[0].
"http://search.yahoo.com/search?p=cnn&vc=&fr=yfp-t-
105&toggle=1&cop=mss&ei=UTF-8&fp_ip=PL"
Zapamiętaj jednak, że w ten sposób wolno tylko uogólniać domeny. Możesz zmienić
www.yahoo.com na yahoo.com, ale nie uda się zmienić yahoo.com na www.yahoo.com ani na
żadną inną domenę poza Yahoo!.
>>> document.domain
"www.yahoo.com"
>>> document.domain = 'yahoo.com'
"yahoo.com"
>>> document.domain = 'www.yahoo.com'
241
JavaScript. Programowanie obiektowe
Wcześniej w tym rozdziale opisywałem obiekt window.location. Teraz powiem tylko, że te same
funkcjonalności oferuje document.location.
Zdarzenia
Wyobraź sobie, że słuchasz radia i nagle spiker ogłasza „Uwaga, proszę państwa, zdarzyło się
coś strasznego! Na Ziemi wylądowali kosmici!”. Być może pomyślisz sobie: „No i co z tego?”.
Być może inni słuchacze pomyślą: „Na pewno przybywają w pokoju”. Jeszcze innym przez
głowę przebiegnie myśl: „Wszyscy zginiemy!”. W podobny sposób działają zdarzenia przeglą-
darki. Przeglądarka informuje o nich Twój kod, jeśli zechce on podłączyć się do audycji. Przy-
kładowe zdarzenia to:
Q kliknięcie przycisku przez użytkownika,
Q wpisanie znaku w polu tekstowym,
Q zakończenie ładowania strony.
Możesz stworzyć funkcję (nazywaną obserwatorem zdarzenia lub uchwytem zdarzenia) i po-
wiązać ją z określonym zdarzeniem. Taka funkcja zostanie wywołana przez przeglądarkę, jak
tylko dojdzie do zdarzenia. Zobaczmy, jak się to robi.
Gdy użytkownik kliknie tekst osadzony wewnątrz <div>, nastąpi zdarzenie kliknięcia (click),
w wyniku którego zostanie wywołany kod zawarty wewnątrz atrybutu onclick. Co prawda nie
została jawnie utworzona funkcja obsługi zdarzenia, jednak taka funkcja jest tworzona w tle.
Zawiera ona kod będący wartością atrybutu onclick.
Pola elementów
Kolejny sposób uruchomienia kodu w odpowiedzi na zdarzenie kliknięcia to przypisanie polu
onclick węzła elementu DOM wartości będącej funkcją. Na przykład:
242
Rozdział 7. • Środowisko przeglądarki
<div id="my-div">kliknij</div>
<script type="text/javascript">
var myelement = document.getElementById('my-div');
myelement.onclick = function() {
alert('Ałka!');
alert('Drugie ałka!');
}
</script>
Ten sposób jest lepszy, ponieważ dzięki niemu znacznik <div> nie musi zawierać żadnego kodu
w języku JavaScript. Zawsze staraj się stosować zasadę, która mówi, że HTML służy do przecho-
wywania treści, JavaScript odpowiada za zachowanie, a CSS za formatowanie. Nie należy mieszać
tych warstw, jeśli nie jest to absolutnie konieczne.
Minusem tej metody jest to, że do zdarzenia można przypisać tylko jedną funkcję — tak jakby
program radiowy miał tylko jednego słuchacza. Co prawda wewnątrz funkcji może się dziać mnó-
stwo rzeczy, jednak nie zawsze jest to wygodne, tak samo jak nie byłaby wygodna sytuacja,
w której wszyscy słuchacze programu radiowego musieliby siedzieć w jednym pomieszczeniu.
Jak widzisz, addEventListener() jest metodą wywoływaną na węźle elementu, która jako
pierwszy parametr przyjmuje typ zdarzenia, a jako drugi — wskaźnik do funkcji. Możesz ko-
rzystać z funkcji anonimowych (jak function(){alert('Uuu!')}) lub z istniejących już funkcji,
takich jak console.log. Podana przez Ciebie funkcja zostanie wywołana, gdy nastąpi zdarzenie.
Jako parametr zostanie jej przekazany obiekt zdarzenia. Jeśli uruchomisz powyższy kod i klikniesz
ostatni akapit na stronie, zobaczysz informację o obiektach wyświetlaną (logowaną) w konsoli
Firebug.
243
JavaScript. Programowanie obiektowe
Przechwytywanie i bąbelkowanie
addEventsListener() przyjmuje jeszcze trzeci parametr, który we wszystkich dotychczasowych
przykładach miał wartość false. Zobaczmy, do czego służy.
244
Rozdział 7. • Środowisko przeglądarki
Klikając link, klikasz także element listy (<li>), listę (<ul>), <body> i, ostatecznie, całość do-
kumentu. W zależności od tego, w jaki sposób zaimplementujesz propagację zdarzeń, kliknię-
cie linku może zostać — lub nie — uznane za kliknięcie całego dokumentu. Masz do wyboru
dwie możliwości:
Q Przechwytywanie zdarzeń: najpierw ma miejsce kliknięcie dokumentu, które jest
propagowane (przekazywane) do elementu body, potem do listy i dopiero na koniec
do linku.
Q Bąbelkowanie zdarzeń: najpierw ma miejsce kliknięcie linku, które następnie
(jak bąbelek powietrza w wodzie) powoli przesuwa się coraz wyżej, aż do poziomu
dokumentu.
Specyfikacja DOM poziomu drugiego wspomina o trzech fazach propagacji zdarzeń: prze-
chwytywaniu, osiągnięciu celu oraz bąbelkowaniu. Oznacza to, że zdarzenie przesuwa się od
poziomu dokumentu do linku (celu), a potem z powrotem w górę, do dokumentu. Obiekty
zdarzeń mają pole eventPhase (faza zdarzenia), które zawiera informację o aktualnej fazie.
245
JavaScript. Programowanie obiektowe
Żeby powiedzieć całą prawdę: w IE istnieje sposób przechwytywania zdarzeń (metody setCapture()
i releaseCapture()), ale tylko w przypadku zdarzeń myszki. Przechwytywanie jakichkolwiek
innych zdarzeń (np. naciśnięć klawiszy) nie jest wspierane.
Zatrzymanie propagacji
Poniższy przykład demonstruje, w jaki sposób zatrzymać bąbelkowanie zdarzenia. Nasz testowy
dokument zawiera fragment:
<p id="closer">final</p>
246
Rozdział 7. • Środowisko przeglądarki
Specyfikacja DOM nie wspomina o propagacji zdarzeń na poziom okna przeglądarki. To lo-
giczne, zważywszy na to, że DOM zajmuje się dokumentem, a nie przeglądarką. IE nie prze-
kazuje zdarzeń oknu przeglądarki, natomiast Firefox to robi.
Jeśli teraz klikniesz obszar akapitu, zobaczysz cztery okna dialogowe z następującymi komu-
nikatami:
Q akapit kliknięty
Q body kliknięte
Q dokument kliknięty
Q okno kliknięte
Jeśli ponowisz kliknięcie, zobaczysz okna dialogowe związane z body, dokumentem i oknem,
ale nie pojawi się żadna informacja o akapicie.
Spróbujmy zatrzymać propagację zdarzenia. Funkcja dodana jako obserwator otrzymuje obiekt
zdarzenia jako parametr. Możemy wywołać na nim metodę stopPropagation().
function paraHandler(e){
alert('akapit kliknięty');
e.stopPropagation();
}
Jeśli teraz klikniesz akapit, zobaczysz tylko jeden komunikat, ponieważ zdarzenie nie będzie
bąbelkowało na wyższe poziomy.
Warto wiedzieć, że nie jest możliwe usunięcie obserwatorów korzystających z funkcji anoni-
mowych. Przy usuwaniu obserwatora konieczne jest podanie wskaźnika do wcześniej przeka-
zanej funkcji. Dwie takie same funkcje anonimowe to dwa różne obiekty w pamięci. Poniższy
kod nie przyniesie żadnego efektu:
document.body.removeEventListener('click',
function(){
alert('clicked body')
},
false); // NIE USUNIE obserwatora
247
JavaScript. Programowanie obiektowe
Możesz na przykład spłatać figla osobom odwiedzającym Twoją stronę, przy każdym kliknięciu
linku pytając ich, czy na pewno chcą przenieść się w inne miejsce. Jeśli użytkownik wybierze
Anuluj (wówczas confirm() zwróci false), to wywołana zostanie metoda preventDefault().
// wszystkie linki
var all_links = document.getElementsByTagName('a');
for (var i = 0; i < all_links.length; i++) { // pętla przez wszystkie linki
all_links[i].addEventListener(
'click', // typ zdarzenia
function(e){ // obserwator
if (!confirm('Czy na pewno chcesz przejść na inną stronę?')){
e.preventDefault();
}
},
false); // nie stosuj przechwytywania
}
Istnieje nieliczna grupa zdarzeń, które nie pozwolą na anulowanie zachowania domyślnego.
Jeśli chcesz się upewnić, czy anulowanie jest dozwolone, sprawdź, czy pole cancellable
(anulowalne) ma wartość true.
Przeanalizuj przykład, który wypisuje w konsoli nazwę (nodeName) elementu będącego celem
kliknięcia:
document.addEventListener('click', function(e){
console.log(e.target.nodeName);
}, false);
248
Rozdział 7. • Środowisko przeglądarki
Typy zdarzeń
Wszystkie przedstawione do tej pory przykłady obsługi zdarzeń były oparte o kliknięcia. Jakie
jeszcze zdarzenia może rozpoznać przeglądarka? Łatwo się domyślić, że typy zdarzeń nie będą
w pełni spójne w różnych przeglądarkach. Jeśli interesuje Cię pełna lista zdarzeń obsługiwanych
249
JavaScript. Programowanie obiektowe
przez daną przeglądarkę, muszę odesłać Cię do odpowiedniej dokumentacji. Poniżej przed-
stawiam najważniejsze zdarzenia zaimplementowane we wszystkich wiodących przeglądarkach:
Q Zdarzenia myszki:
Q mousedown, mouseup, click (naciśnięcie przycisku myszki, zwolnienie przycisku,
kliknięcie — dokładnie w tej kolejności);
Q mouseover (kursor nad elementem), mouseout (kursor był nad elementem, ale został
przesunięty), mousemove (ruch myszką).
Q Zdarzenia klawiatury:
keydown, keypress, keyup (klawisz na dole, naciśnięcie klawisza, klawisz u góry
Q
— dokładnie w tej kolejności).
Q Zdarzenia okna i ładowania:
Q load (załadowanie: obrazek lub strona i wszystkie jej komponenty zostały
załadowane), unload (użytkownik opuszcza stronę), beforeunload (skrypt może
zapewnić użytkownikowi możliwość anulowania zdarzenia unload).
Q abort (użytkownik zatrzymuje ładowanie strony w Firefoksie lub obrazka w IE),
error (błąd JavaScriptu w Firefoksie i IE lub niemożność załadowania obrazka w IE).
Q resize (zmiana rozmiaru okna przeglądarki), scroll (przewinięcie strony),
contextmenu (pojawia się menu związane z prawym przyciskiem myszki).
Q Zdarzenia formularzy:
Q focus (przejście do pola formularza), blur (opuszczenie pola formularza);
Q change (opuszczenie pola po zmianie jego wartości), select (zaznaczenie tekstu
w polu tekstowym);
Q reset, submit (wyczyszczenie zawartości formularza lub jego wysłanie).
Na tym kończę omawianie zdarzeń. Pod koniec rozdziału znajdziesz kilka ćwiczeń związanych
z obsługą zdarzeń w różnych przeglądarkach.
XMLHttpRequest
XMLHttpRequest() to obiekt (konstruktor) umożliwiający wysyłanie żądań HTTP z JavaScriptu.
Został on wprowadzony najpierw w przeglądarce IE, gdzie był zaimplementowany jako
obiekt ActiveX. Od IE w wersji 7 jest to natywny obiekt przeglądarki — tak samo jest w Firefok-
sie, Safari i Operze. Zgodność implementacji w różnych przeglądarkach umożliwiła rozwój
tak zwanych aplikacji AJAX, dzięki którym w celu zmiany treści strony nie jest konieczne
przeładowywanie całego dokumentu. Przy pomocy JavaScriptu możesz wysłać żądanie HTTP
do serwera, odebrać odpowiedź oraz zaktualizować jedynie fragment strony. Technologia ta
pozwala na tworzenie szybciej reagujących stron przypominających aplikacje desktopowe.
AJAX rozwija się jako Asynchronous JavaScript and XML, czyli asynchroniczny JavaScript i XML.
250
Rozdział 7. • Środowisko przeglądarki
Q Słowo asynchroniczny oznacza tyle, że po wysłaniu żądania HTTP kod nie musi
czekać na odpowiedź. Może normalnie działać, a w chwili nadejścia odpowiedzi
zostanie o niej powiadomiony za pośrednictwem zdarzenia.
Q JavaScript — to akurat oczywiste, obiekty XMLHttpRequest (w skrócie XHR) tworzymy
za pomocą JavaScriptu.
Q XML bierze się z tego, że początkowo programiści tworzyli żądania HTTP dla
dokumentów XML i używali zawartej w tych dokumentach informacji do aktualizacji
strony. Teraz rzadko stosuje się to rozwiązanie, ponieważ można zażądać danych
w postaci czystego tekstu, w formacie JSON lub w postaci kodu HTML, który można
od razu wstawić na stronę.
Wysłanie żądania
Obiekt tworzy się w następujący sposób (za chwilę zajmę się niespójnościami w przeglądar-
kach różnych producentów):
var xhr = new XMLHttpRequest();
Pierwszy parametr to typ żądania HTTP (GET, POST, HEAD itp.). Najczęściej stosowane są GET
i POST. Z GET korzysta się wtedy, gdy żądaniu towarzyszy bardzo niewiele tekstu, w przeciwnym
przypadku należy wybrać POST. Drugi parametr to URL żądania. W powyższym przykładzie
jest to plik plik.txt, znajdujący się w tym samym katalogu co strona. Ostatni parametr (typu
boolean) określa, czy żądanie jest asynchroniczne (true), czy nie (false).
251
JavaScript. Programowanie obiektowe
Metoda send() przyjmuje dowolne dane, które mają być wysłane wraz z żądaniem. W przypadku
żądań GET jest to pusty łańcuch znaków (dane są doklejane do adresu URL), w przypadku żą-
dań POST jest to łańcuch postaci klucz=wartość&klucz2=wartość2.
Na tym etapie żądanie zostało wysłane, a Twój kod (i użytkownika) może zająć się innymi
sprawami. Funkcja myCallback zostanie wywołana, gdy żądanie zostanie obsłużone przez serwer.
Przetworzenie odpowiedzi
Do zdarzenia readystatechange (zmiana stanu gotowości) zostało dołączone zdarzenie. Czym
właściwie jest stan gotowości i jak może się zmieniać?
Stan gotowości jest polem obiektu XHR (o nazwie readyState). Każda jego zmiana powoduje
pojawienie się zdarzenia readystatechange. Pole readyState może przyjmować następujące
wartości:
Q 0: niezainicjowane
Q 1: ładuje się
Q 2: załadowane
Q 3: interaktywne
Q 4: zakończone
Jeśli xhr.readyState ma wartość 4, a xhr.status jest równy 200, możesz pobrać otrzymaną
treść, korzystając z pola xhr.responseText. W poniższym przykładzie funkcja myCallback wy-
świetla okienko dialogowe zawierające treść pobraną z podanego adresu URL.
function myCallback() {
if (xhr.readyState < 4) {
return; // jeszcze niegotowe
}
if (xhr.status !== 200) {
alert('Błąd!'); // kod statusu żądania HTTP jest inny niż OK
return;
}
// wszystko w porządku, zatem do dzieła!
alert(xhr.responseText);
}
252
Rozdział 7. • Środowisko przeglądarki
Po odebraniu żądanych treści możesz dodać je do strony, obliczyć coś na ich podstawie lub
wykonać jakąkolwiek potrzebną Ci czynność.
Opisany dwuetapowy proces (wyślij żądanie, przetwórz odpowiedź) jest podstawą wszystkich
funkcjonalności XHR/AJAX. Znając go, możesz stworzyć następcę Gmail i Yahoo! Maps. Aha,
musimy jeszcze zająć się drobną kwestią różnic pomiędzy przeglądarkami.
Jeśli szukasz w pełni przenośnego rozwiązania, musisz sprawdzić, czy przeglądarka użytkow-
nika implementuje XMLHttpRequest jako obiekt natywny. Jeśli nie — spróbuj zastosować sposób
z IE. Tworzenie instancji obiektu XHR może wyglądać tak:
var ids = ['MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP'];
var xhr;
if (typeof window.XMLHttpRequest === 'function') {
xhr = new XMLHttpRequest();
} else {
for (var i = 0; i < ids.length; i++) {
try {
xhr = new ActiveXObject(ids[i]);
break;
} catch (e){}
}
}
Co się dzieje? Tablica ids zawiera listę identyfikatorów ActiveX. Zmienna xhr będzie wskazywała
nowy obiekt XHR. Kod najpierw sprawdza, czy istnieje funkcja windows.XMLHttpRequest. Jeśli
tak, uznaje, że przeglądarka posiada natywny obiekt XMLHttpRequest() (zatem przeglądarką
jest Firefox, Safari, Opera, IE7 lub nowszy). W przeciwnym wypadku kod spróbuje utworzyć
obiekt za pomocą kolejnych identyfikatorów z tablicy. Fragment catch(e) po cichu przechwytuje
błędy, dzięki czemu możliwe są kolejne iteracje pętli. Jak tylko uda się utworzyć obiekt xhr,
następuje wyjście z pętli.
Kod jest dość długi, zatem warto zamknąć go wewnątrz funkcji. Jedno z ćwiczeń pod koniec
rozdziału zachęca Cię do stworzenia własnej funkcji obsługującej AJAX.
253
JavaScript. Programowanie obiektowe
A jak asynchroniczny
Skoro wiesz już, jak utworzyć obiekt XHR, przekaż mu URL i obsłuż otrzymaną odpowiedź. Co
się stanie, jeśli asynchronicznie prześlesz dwa żądania? Czy kod pomyli się, jeśli odpowiedź
na drugie żądanie nadejdzie wcześniej niż odpowiedź na pierwsze?
W naszym przykładzie obiekt XHR był globalny, a funkcja myCallback wymagała obecności
tego obiektu w celu pobrania wartości pól readyState, status i responseText. Inny sposób,
który nie wymaga polegania na zmiennych globalnych, to umieszczenie wywoływanej funkcji
wewnątrz domknięcia. Można to zrobić w następujący sposób:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = (function(myxhr){
return function(){myCallback(myxhr);}
})(xhr);
xhr.open('GET', 'plik.txt', true);
xhr.send('');
W tej wersji programu funkcja myCallback() otrzyma obiekt XHR w postaci parametru, dzięki
czemu nie będzie musiała szukać go w globalnej przestrzeni nazw. Oznacza to także, że przed
momentem otrzymania odpowiedzi oryginalny obiekt xhr mógł zostać użyty przez drugie żądanie
lub nawet usunięty. Domknięcie nadal będzie wskazywało oryginalny obiekt.
X jak XML
Chociaż obecnie zalecanym formatem przesyłu danych jest JSON (omawiany w następnym
rozdziale), XML nadal jest rozsądnym wyborem. Poza polem responseText obiekty XHR posia-
dają jeszcze inne pole, o nazwie responseXML. Jeśli za pośrednictwem żądania HTTP pobie-
rzesz dokument XML, responseXML będzie wskaźnikiem do obiektu dokumentu XML DOM.
Możesz przetwarzać ten dokument za pomocą wszystkich omówionych metod Core DOM, takich
jak getElementsByTagName() czy getElementById().
Przykład
Podsumujmy przykładem pozyskaną dotychczas wiedzę na temat XHR. Przykładowa strona,
z którą możesz eksperymentować, znajduje się pod adresem http://www.phpied.com/files/jsoop/
xhr.html.
Strona główna, xhr.html, to zwykła statyczna strona zawierająca tylko trzy pary znaczników <div>.
<div id="text">Text will be here</div> <!--tu będzie tekst-->
<div id="html">HTML will be here</div> <!--tu będzie kod HTML-->
<div id="xml">XML will be here</div> <!--tu będzie kod XML-->
254
Rozdział 7. • Środowisko przeglądarki
Przy użyciu konsoli Firebug możesz napisać kod, który zażąda trzech plików i wgra ich za-
wartość pomiędzy odpowiednie znaczniki <div>.
Wszystkie pliki znajdują się w tym samym katalogu co xhr.html. Z powodów bezpieczeństwa
XMLHttpRequest można stosować tylko do plików, które znajdują się w tej samej domenie.
Funkcja pobiera URL żądania oraz wskaźnik do funkcji, która ma zostać wywołana zwrotnie po
przyjściu odpowiedzi. Możemy wywołać funkcję request trzy razy, po jednym razie dla każdego
pliku:
request(
'http://www.phpied.com/files/jsoop/content.txt',
function(o){
document.getElementById('text').innerHTML = o.responseText;
}
);
request(
'http://www.phpied.com/files/jsoop/content.html',
function(o){
document.getElementById('html').innerHTML = o.responseText;
}
);
255
JavaScript. Programowanie obiektowe
request(
'http://www.phpied.com/files/jsoop/content.xml',
function(o){
document.getElementById('xml').innerHTML =
o.responseXML.getElementsByTagName('root')[0].firstChild.nodeValue;
}
);
Podczas przetwarzania dokumentu XML możesz pobrać węzeł związany z <root> jako
o.responseXML.documentElement (zamiast o.responseXML.getElementsByTagName('root')[0]).
Pamiętaj, że korzeń dokumentu XML możesz pobrać poprzez pole documentElement. W do-
kumentach HTML korzeniem zawsze jest znacznik <html>.
256
Rozdział 7. • Środowisko przeglądarki
Podsumowanie
Ten rozdział pokrył naprawdę duży zakres materiału. Na początku przedstawiłem obiekty BOM
(Browser Object Model, obiektowy model przeglądarki), które są w spójny sposób zaimplemento-
wane w przeglądarkach różnych producentów:
Q pola globalnego obiektu window, takie jak navigator, location, history, frames,
screen;
Q metody takie jak setInterval(), setTimeout(), alert(), confirm(), prompt(),
moveTo(), moveBy(), resizeTo() i resizeBy().
Omówiłem także pewne pola pochodzące z DOM poziomu zero (sprzed standaryzacji), które
zostały przeniesione do DOM poziomu pierwszego:
Q Kolekcje: document.forms, images, links, anchors, applets. Ich stosowanie
nie jest zalecane, jako że DOM 1 oferuje o wiele bardziej elastyczną metodę
getElementsByTagName().
Q document.body, które jest wygodnym sposobem dostępu do <body>.
Q document.title, cookie, referrer, domain.
Później przyszła pora na zdarzenia przeglądarki, które można obserwować. Ich implementacja
w sposób działający we wszystkich przeglądarkach jest dość trudna, ale możliwa. Zdarzenia bą-
belkują, a Ty wiesz już, jak korzystać z delegacji, by nasłuchiwać zdarzeń na bardziej global-
nym poziomie. Potrafisz także zatrzymać propagację zdarzeń oraz zmienić domyślną obsługę
zdarzeń przeglądarki.
257
JavaScript. Programowanie obiektowe
Ćwiczenia
Dotychczas wszystkie ćwiczenia umieszczane pod koniec różnych rozdziałów książki można było
wykonać, w całości opierając się na wiedzy przekazanej w rozdziale. Tym razem niektóre z ćwi-
czeń mogą wymagać dodatkowej lektury lub przynajmniej pewnej ilości eksperymentowania.
1. BOM
Jako ćwiczenie z BOM spróbuj zaimplementować złą, natrętną, nieprzyjazną
użytkownikowi stronę Web 1.0. Niech okno przeglądarki trzęsie się i przesuwa
jak podczas trzęsienia ziemi. Skorzystaj w tym celu z jednej z funkcji move*()
oraz z jednego lub więcej wywołań setInterval() — na koniec zatrzymaj wszystko
przy pomocy setTimeout(). Następnie zaskocz użytkownika wyskakującym okienkiem
rozmiaru 200×200 pikseli, które powoli zwiększa swój rozmiar aż do momentu
osiągnięcia wymiarów 400×400. Możesz jeszcze wyświetlić na pasku stanu
(window.status) aktualną datę i czas i aktualizować tę wartość co sekundę, jak
w zegarku. Konieczna może okazać się zmiana domyślnych ustawień przeglądarki
— część opisanych efektów może być zablokowana, ponieważ użytkownicy raczej
nienawidzą tego typu wstawek. W Firefoksie ustawienia możesz zmienić w okienku
Narzędzia/Opcje/Włącz obsługę języka JavaScript/Zaawansowane.
2. DOM
2. 1. Zmień sposób implementacji funkcji spacerDOM(). Spraw, by jako parametr
akceptowała dowolną funkcję, i usuń wpisane na sztywno wywołanie
console.log().
2.2. Usuwanie treści za pomocą innerHTML jest bardzo łatwe (document.body.innerHTML
´= ''), ale nie zawsze jest to najlepsze rozwiązanie. Problem polega na tym,
że jeśli wiele obserwatorów jest dołączonych do usuwanych elementów, IE nie
usunie obserwatorów, co prowadzi do wycieków pamięci (przechowywane są
referencje do nieistniejących elementów). Zaimplementuj ogólną funkcję, która
usuwa węzły DOM, najpierw usuwając związanych z nimi obserwatorów. Możesz
przejść iteracyjnie przez wszystkie atrybuty węzła, sprawdzając, czy wartość
któregoś z nich nie jest funkcją. Jeśli jest, prawdopodobnie jest to atrybut
podobny do onclick. Musisz przypisać mu wartość null przed usunięciem
danego elementu z drzewa.
258
Rozdział 7. • Środowisko przeglądarki
259
JavaScript. Programowanie obiektowe
4. XMLHttpRequest
4.1. Utwórz własny obiekt użytkowy o nazwie ajax. Przykład użycia:
function myCallback(xhr) {
alert(xhr.responseText);
}
ajax.request('plik.txt', 'get', myCallback);
ajax.request('skrypt.php', 'post', myCallback,
'imię=Jan&nazwisko=Kowalski');
4.2. Dodaj technologię AJAX do wyszukiwania Google. Przy pomocy Firebuga
możesz wstawić w kod strony fragmenty napisane w języku JavaScript.
Umożliwi Ci to wysyłanie żądań do stron w domenie google.com przy użyciu
XHR. Wejdź na stronę google.com i napisz kod, który sprawi, że wyniki nie
będą wyświetlane na osobnej stronie, tylko pojawią się pod formularzem
wyszukiwania — bez przeładowywania całej strony. Wykorzystaj obiekty
z ćwiczenia 3.1 i 4.1. Wykonaj następujące kroki:
Q Dodaj obserwatora do zdarzenia submit formularza wyszukiwania i anuluj
zachowanie domyślne (tak by formularz nie został wysłany).
Q Utwórz obiekt XHR i zażądaj strony URL z adresu http://www.google.
com/search?q=mojezapytanie, gdzie mojezapytanie to tekst wpisany
w pole wyszukiwania.
Q W funkcji obsługującej zdarzenie dodaj do <body> nowy znacznik <div>
o identyfikatorze content i ustaw w jego polu innerHTML wartość
responseText obiektu XHR.
Użytkownik będzie miał możliwość wpisania zapytania w pole wyszukiwania.
Po wciśnięciu Enter wyniki wyszukiwania pojawią się pod formularzem, przy
czym strona nie zostanie przeładowana. Użytkownik powinien mieć możliwość
zadawania wielu zapytań. Kolejne zapytania nie powinny już powodować
utworzenia znacznika <div>, tylko aktualizację zawartego w nim kodu HTML.
260
8
Wzorce kodowania
i wzorce projektowe
Skoro znasz już obiektowe funkcjonalności JavaScriptu, takie jak prototypy i dziedziczenie,
oraz potrafisz radzić sobie z obiektami przeglądarki, możemy przenieść się na nieco inny,
wyższy poziom. Przyjrzyjmy się najczęściej spotykanym sposobom korzystania z JavaScriptu,
określanych mianem wzorców. W wielkim skrócie, wzorce to uznane, dobre rozwiązania po-
pularnych problemów.
Pewnie nie raz zdarzyło Ci się stanąć przed problemem programistycznym, który wydawał Ci się
dziwnie znajomy. W takiej sytuacji warto jest pogrupować podobne do siebie problemy w klasy
posiadające wspólne rozwiązania. Wzorzec to sprawdzone rozwiązanie (lub podejście do rozwią-
zania) problemu, które jest na tyle ogólne, że można stosować je w wielu przypadkach. Cza-
sami wzorzec sprowadza się do samej idei lub rozpoznawalnej nazwy — niejednokrotnie oka-
zuje się, że samo rozpoznanie i nazwanie problemu może istotnie ułatwić jego rozwiązanie.
Poza tym o wiele łatwiej jest pracować w zespole programistów, kiedy wszyscy jego członko-
wie używają spójnej, zrozumiałej terminologii do określania problemów i ich rozwiązań.
Zdarza się oczywiście, że problem, któremu musisz stawić czoła, jest unikalny i nie pasuje do
żadnego znanego wzorca. W takiej sytuacji nie należy na siłę stosować wzorców, naginając
problem tak, by do pasował do któregoś z nich — chyba że potrafisz samodzielnie zapropo-
nować nowy, dopasowany wzorzec.
Wzorce kodowania
Pierwsza część rozdziału poświęcona jest wzorcom nierozerwalnie związanym z funkcjonal-
nościami JavaScriptu. Celem niektórych z nich jest nadanie lepszej struktury kodowi źródło-
wemu (np. wzorce dotyczące przestrzeni nazw), inne mają na celu zwiększenie wydajności
aplikacji (np. leniwe definicje i rozgałęzianie kodu w czasie inicjalizacji), a jeszcze inne pozwalają
emulować funkcjonalności tradycyjnie niedostępne w języku, na przykład pola prywatne.
W tym podrozdziale omawiam następujące wzorce:
Q wydzielenie warstwy zachowania,
Q przestrzenie nazw,
Q rozgałęzianie kodu w czasie inicjalizacji,
Q leniwe definicje,
Q obiekty konfiguracyjne,
Q prywatne pola i metody,
Q metody uprzywilejowane,
Q funkcje prywatne w roli metod publicznych,
Q funkcje samowywołujące się,
Q łańcuchowanie,
Q JSON.
Izolowanie zachowania
Jak wiadomo, strona internetowa składa się z trzech warstw:
Q warstwa treści (nazywana także warstwą struktury) — HTML;
Q warstwa prezentacyjna — CSS;
Q warstwa zachowania (inaczej warstwa biznesowa) — JavaScript.
Warstwa treści
Treść strony jest zapisywana za pomocą języka HTML. Treść powinna być oznaczana za po-
mocą jak najmniejszej ilości znaczników, nadających znaczenie danemu fragmentowi strony.
Przykładowo jeśli tworzysz menu strony, z reguły dobrym pomysłem jest wykorzystanie
znaczników <ul> i <li>, ponieważ menu najczęściej jest po prostu listą linków.
Treść (wraz z kodem HTML) nie powinna zawierać żadnych elementów formatujących. Za
sposób wyświetlania informacji odpowiada warstwa prezentacji w postaci CSS (Cascading Style
Sheets, czyli kaskadowych arkuszy stylów). Oznacza to, że:
262
Rozdział 8. • Wzorce kodowania i wzorce projektowe
Warstwa prezentacji
Dobrym sposobem rozdzielenia warstwy prezentacyjnej od warstwy treści jest wykasowanie
wszystkich domyślnych ustawień przeglądarki — można wykorzystać do tego arkusz reset.css
z biblioteki Yahoo! UI library. Dzięki temu posunięciu programista przy wyborze znaczników
nie będzie kierował się domyślnym sposobem wyświetlania zawartości danego znacznika, tylko
jego przeznaczeniem.
Zachowanie
Trzecią składową kodu strony jest warstwa zachowania. Powinna ona zostać wyraźnie oddzielona
zarówno od warstwy treści, jak i od warstwy prezentacyjnej. Opis zachowania powinien zostać
zamknięty wewnątrz znaczników <script>, a sam kod przeniesiony do zewnętrznych plików. Wy-
maga to rezygnacji z osadzonych atrybutów, takich jak onclick, onmouseover itp. Zamiast tego zale-
ca się korzystanie z opisanych w poprzednim rozdziale metod addEventListener i attachEvent.
263
JavaScript. Programowanie obiektowe
<body>
<form id="myform" method="post" action="server.php">
<fieldset>
<legend>Szukaj</legend>
<input
name="szukaj"
id="szukaj"
type="text"
/>
<input type="submit" />
</fieldset>
</form>
<script type="text/javascript" src="zachowanie.js"></script>
</body>
Przestrzenie nazw
Należy unikać stosowania zmiennych globalnych w celu zmniejszenia prawdopodobieństwa
kolizji nazw. Jednym ze sposobów ograniczenia liczby zmiennych globalnych jest przypisanie
zmiennych i funkcji do określonych przestrzeni nazw. Pomysł jest prosty: tworzysz tylko jeden
obiekt globalny, a wszystkie inne zmienne i funkcje stają się polami tego obiektu.
264
Rozdział 8. • Wzorce kodowania i wzorce projektowe
Dzięki temu wspomniany już obiekt użytkowy myevent nie musi zajmować globalnej nazwy,
ale może stać się polem obiektu MYAPP.
// podobiekt
MYAPP.event = {};
265
JavaScript. Programowanie obiektowe
Metoda namespace()
Niektóre biblioteki, między innymi YUI, implementują metodę użytkową o nazwie namespace(),
która ułatwia umieszczanie obiektów w określonych przestrzeniach nazw. Na przykład:
MYAPP.namespace('dom.style');
Wynik wykonania powyższych dwóch linii powinien odpowiadać wynikowi uruchomienia na-
stępującego kodu:
var MYAPP = {
event: {},
dom: {
style: {}
}
}
266
Rozdział 8. • Wzorce kodowania i wzorce projektowe
Można zoptymalizować wykonanie takiego kodu, rozgałęziając przynajmniej pewne jego części
w czasie inicjalizacji — czyli podczas ładowania skryptu — a nie w czasie wykonania. Korzy-
stając z możliwości dynamicznego definiowania funkcji, możesz zaprogramować rozgałęzie-
nie, które w zależności od typu przeglądarki w inny sposób zaimplementuje ciało tej samej
funkcji. Zobaczmy, jak to zrobić.
267
JavaScript. Programowanie obiektowe
Leniwe definicje
Wzorzec leniwego definiowania funkcji jest zbliżony do wzorca rozgałęziania w czasie inicja-
lizacji. W przypadku leniwych definicji kod jest rozgałęziany podczas pierwszego wywołania
funkcji. Wywołana funkcja redefiniuje swoją treść w najlepszy (w danych warunkach) możli-
wy sposób. Różnica jest taka, że rozgałęzianie podczas inicjalizacji odbywa się dokładnie raz,
podczas ładowania, natomiast w przypadku leniwej inicjalizacji rozgałęzienie może się nie
wydarzyć — jeśli funkcja ani razu nie zostanie wywołana. Pewną przewagą leniwych definicji
jest to, że proces inicjalizacji jest istotnie lżejszy.
268
Rozdział 8. • Wzorce kodowania i wzorce projektowe
};
} else {
MYAPP.myevent.addListener = function(el, type, fn) {
el['on' + type] = fn;
};
}
MYAPP.myevent.addListener(el, type, fn);
}
};
Obiekt konfiguracyjny
Ten wzorzec przydaje się w sytuacji, gdy musisz posługiwać się funkcją lub metodą, która
przyjmuje wiele parametrów. Precyzyjna definicja słowa „wiele” zależy od Ciebie, jednak
w praktyce okazuje się, że funkcje posiadające więcej niż trzy parametry zaczynają sprawiać
kłopoty, zwłaszcza gdy część parametrów jest nieobowiązkowa.
Zamiast przekazywania funkcji wielu parametrów możesz wykorzystać jeden parametr będący
obiektem. Pola obiektu będą miały znaczenie odpowiadające poszczególnym parametrom. Jest
to szczególnie wygodne podczas przekazywania parametrów konfiguracyjnych, które z reguły
są liczne i w większości opcjonalne (niepodanie wartości parametru jest równoważne przeka-
zaniu wartości domyślnej). Oto zalety wykorzystania pojedynczego obiektu zamiast wielu pa-
rametrów:
Q Kolejność nie ma znaczenia.
Q Łatwo pominąć parametry, których wartości nie chcesz przekazywać.
Q Łatwiej dodać nowe pole do przekazywanego obiektu niż nowy argument do funkcji
(w takim wypadku trzeba zmienić wszystkie wywołania).
Q Kod staje się bardziej czytelny, ponieważ zawiera nie tylko wartości parametrów
konfiguracji, ale także ich nazwy (w postaci nazw pól).
Załóżmy, że istnieje konstruktor Button, tworzący przyciski. Jego pierwszy argument to tekst,
który ma się pojawić na przycisku (atrybut value znacznika <input>), drugi — opcjonalny —
parametr to typ (atrybut type) przycisku.
// konstruktor tworzący przyciski
var MYAPP = {};
MYAPP.dom = {};
MYAPP.dom.Button = function(text, type) {
var b = document.createElement('input');
b.type = type || 'submit';
b.value = text;
return b;
}
269
JavaScript. Programowanie obiektowe
Wykorzystanie konstruktora jest bardzo proste: wystarczy przekazać mu łańcuch znaków. Zaraz
potem można dołączyć nowy przycisk do dokumentu:
document.body.appendChild(new MYAPP.dom.Button('Naciśnij!'));
Jak dotąd nic nie zapowiada kłopotów. Wyobraź sobie jednak, że nagle przyjdzie Ci do głowy
pomysł dodania do konstruktora pewnych dodatkowych cech przycisku, takich jako kolor
i czcionka. W pewnym momencie możesz ocknąć się z definicją wyglądającą mniej więcej tak:
MYAPP.dom.Button = function(text, type, color, border, font) {
// ....
}
Sprawa nie jest już taka prosta, zwłaszcza jeśli chcesz ustawić trzeci i piąty parametr, zaś dru-
gi i czwarty w ogóle Cię nie interesują:
new MYAPP.dom.Button('Naciśnij!', null, 'white', null, 'Arial, Verdana, sans-serif');
Inny przykład:
document.body.appendChild(
new MYAPP.dom.Button('stary', {color: 'red'})
);
Jak widzisz, w wygodny sposób można nadać wartości jedynie wybranemu podzbiorowi pa-
rametrów i nie trzeba pamiętać ich kolejności. Dodatkowo kod staje się bardziej czytelny, po-
nieważ wartościom parametrów towarzyszą nazwy.
270
Rozdział 8. • Wzorce kodowania i wzorce projektowe
Składnia JavaScriptu nie zawiera specjalnego wsparcia dla pól prywatnych, ale — zgodnie
z tym, co zostało powiedziane w rozdziale 3., możesz użyć zmiennych i metod lokalnych we-
wnątrz konstruktora i uzyskać ten sam poziom zabezpieczeń.
Wróćmy do przykładu konstruktora Button. Możesz utworzyć zmienną lokalną o nazwie styles,
która będzie przechowywała wszystkie wartości domyślne, oraz lokalną funkcję setStyle(),
pozwalającą nadać odpowiednie wartości polom obiektu. Zarówno zmienna, jak i funkcja będą
niewidoczne dla kodu poza konstruktorem. Lokalne pola prywatne można wykorzystać w na-
stępujący sposób:
var MYAPP = {};
MYAPP.dom = {};
MYAPP.dom.Button = function(text, conf) {
var styles = {
font: 'Verdana',
border: '1px solid black',
color: 'black',
background: 'grey'
};
function setStyles() {
for (var i in styles) {
b.style[i] = conf[i] || styles[i];
}
}
conf = conf || {};
var b = document.createElement('input');
b.type = conf['type'] || 'submit';
b.value = text;
setStyles();
return b;
};
styles jest tu polem prywatnym, a setStyle() metodą prywatną. Konstruktor korzysta z nich
wewnętrznie (mają one dostęp do wszystkich danych wewnątrz konstruktora), natomiast nie
są one dostępne dla kodu poza funkcją Button().
Metody uprzywilejowane
Metody uprzywilejowane (termin ten został ukuty przez Douglasa Crockforda) są prawie
zwykłymi metodami publicznymi, które mają dostęp do prywatnych pól i metod. Ich celem
jest kontrolowane udostępnianie na zewnątrz funkcjonalności prywatnych.
271
JavaScript. Programowanie obiektowe
Trzymając się wciąż tego samego przykładu, możemy utworzyć metodę getDefaults() (po-
bierz domyślne), która będzie zwracała obiekt styles. Dzięki temu kod poza konstruktorem
Button będzie mógł sprawdzić domyślne ustawienia stylów, ale nie będzie mógł ich zmienić.
W takim wypadku getDefaults() jest metodą uprzywilejowaną.
Niech _setStyle() i _getStyle() będą funkcjami prywatnymi, które zostaną jednak przypisane
publicznym polom setStyle i getStyle:
var MYAPP = {};
MYAPP.dom = (function(){
var _setStyle = function(el, prop, value) {
console.log('setStyle');
};
var _getStyle = function(el, prop) {
console.log('getStyle');
};
return {
setStyle: _setStyle,
getStyle: _getStyle,
inna: _setStyle
};
})()
272
Rozdział 8. • Wzorce kodowania i wzorce projektowe
Ten wzorzec sprawdza się zwłaszcza podczas jednorazowych zadań inicjalizacyjnych wyko-
nywanych podczas ładowania skryptu.
Wzorzec ten można rozszerzyć tak, by funkcja tworzyła i zwracała obiekty. Jeśli tworzenie
obiektów z pewnej grupy jest złożone i wymaga pewnych obliczeń inicjalizacyjnych, można
wykonać je w pierwszej części funkcji samowywołującej się i zwrócić pojedynczy obiekt, który
ma dostęp do wszelkich pól prywatnych z początkowej części funkcji:
var MYAPP = {};
MYAPP.dom = function(){
// kod inicjalizacyjny
function _private(){
// ... ciało
}
return {
getStyle: function(el, prop) {
console.log('getStyle');
_private();
},
setStyle: function(el, prop, value) {
console.log('setStyle');
}
};
}();
Łańcuchowanie
Łańcuchowanie to wzorzec umożliwiający wywoływanie metod w jednej linii, jak gdyby były
one kolejnymi ogniwami łańcucha. Może to być bardzo wygodne, gdy zachodzi potrzeba wywoła-
nia kilku powiązanych ze sobą metod. Zasada jest taka, że kolejna metoda jest wywoływana
bezpośrednio na wyniku zwróconym przez poprzednią, bez tworzenia zmiennych pośrednich.
Załóżmy, że konstruktor Element pozwala tworzyć różne elementy DOM. Kod tworzący nowy
znacznik <span> i dodający go do <body> mógłby wyglądać tak:
273
JavaScript. Programowanie obiektowe
Jak Ci wiadomo, konstruktory zwracają tworzony przez siebie obiekt this. Możesz sprawić,
by inne metody, takie jak setText() czy setStyle(), także zwracały this — umożliwi to wy-
woływanie kolejnej metody na instancji zwróconej przez poprzednią. Możliwe będzie nastę-
pujące połączenie w łańcuch:
var obj = new MYAPP.dom.Element('span');
obj.setText('cześć').setStyle('color', 'red').setStyle('font', 'Verdana');
document.body.appendChild(obj);
Jeśli nowo utworzony obiekt nie będzie używany po dodaniu go do drzewa, nie musisz nawet
korzystać ze zmiennej obj:
document.body.appendChild(
new MYAPP.dom.Element('span')
.setText('cześć')
.setStyle('color', 'red')
.setStyle('font', 'Verdana')
);
Łańcuchowanie jest intensywnie wykorzystywane w JQuery — być może nawet jest to jedna
z najłatwiej rozpoznawalnych cech tej popularnej biblioteki.
JSON
Na zakończenie części poświęconej wzorcom kodowania chciałbym powiedzieć kilka słów na
temat formatu JSON. JSON sam w sobie nie jest wzorcem kodowania, jednak jego stosowanie
rozwiązuje tyle problemów, że warto o nim wspomnieć w tej sekcji.
274
Rozdział 8. • Wzorce kodowania i wzorce projektowe
Od razu widać, że JSON potrzebuje mniejszej liczby bajtów. Jednak jego największą zaletą
nie jest mniejszy rozmiar, tylko to, że przetwarzanie danych w tym formacie za pomocą Java-
Scriptu jest nadspodziewanie proste. Załóżmy, że nasz program wysłał żądanie XHR i w polu
responseText obiektu XHR znajduje się odpowiedź w formacie JSON. Przechowywany tam
łańcuch znaków można zamienić we w pełni sprawny obiekt za pomocą funkcji eval():
var obj = eval( '(' + xhr.responseText + ')' );
Pewien problem stanowi fakt, że eval() nie jest najbezpieczniejszą z funkcji, zatem lepiej bę-
dzie zastąpić ją funkcjami z biblioteki dostępnej pod adresem http://json.org. Tworzenie
obiektu na podstawie łańcucha znaków w formacie JSON jest równie łatwe:
var obj = JSON.parse(xhr.responseText);
Prostota formatu JSON doprowadziła do tego, że stał się on niezależnym od języka standardem
wymiany danych, które można produkować po stronie serwera przy użyciu swojego ulubione-
go języka. W PHP istnieją funkcje json_encode() (zapisz jako JSON) i json_decode() (odtwórz
z formatu JSON), które umożliwiają serializację tablic i obiektów PHP do postaci łańcucha
znaków w formacie JSON i odwrotnie.
Wzorce projektowe
Druga część tego rozdziału poświęcona jest zastosowaniu w języku JavaScript wzorców pro-
jektowych, po raz pierwszy opisanych w książce Wzorce projektowe. Elementy oprogramo-
wania obiektowego wielokrotnego użytku, autorzy: Erich Gamma, Richard Helm, Ralph John-
son, John Vlissides (Wydawnictwo Naukowo-Techniczne), która często (z powodu liczby
autorów) nazywana jest Księgą czterech. Wzorce z tej książki można podzielić na trzy grupy:
275
JavaScript. Programowanie obiektowe
W Księdze czterech przedstawione zostały 23 wzorce projektowe. Kolejne wzorce zostały rozpo-
znane już po jej wydaniu. Nie jestem w stanie omówić wszystkich spośród nich. Skoncentruję
się jedynie na czterech, dla których przedstawię przykłady implementacji w języku Java-
Script. Staraj się nie zapominać, że wzorce to opis interfejsu i relacji pomiędzy obiektami, a nie
konkretna implementacja. Z reguły, gdy zrozumiesz już działanie wzorca, jego implementacja
— zwłaszcza w języku dynamicznym, a JavaScript należy do tej grupy — okaże się intuicyjna.
Singleton
Singleton jest wzorcem konstrukcyjnym, co oznacza, że jest on związany z tworzeniem obiektów.
Stosuje się go, gdy istnieje potrzeba posiadania w programie tylko jednego obiektu danej klasy.
W języku korzystającym z klas oznacza to, że tylko raz tworzona jest instancja danej klasy, zaś
kolejne próby utworzenia instancji zawsze zwrócą pierwotną tę instancję.
W języku JavaScript nie istnieje pojęcie klasy, zatem singleton jest domyślnym i najbardziej
naturalnym wzorcem. Każdy obiekt jest jedyny w swoim rodzaju. Jeśli nie zostanie skopiowa-
ny i użyty w roli prototypu innego obiektu, pozostanie opisaną pojedynczą instancją.
Singleton 2
Jeśli chcesz korzystać ze składni zbliżonej do klasycznych języków obiektowych, ale potrze-
bujesz singletonu, sytuacja staje się ciekawsza. Załóżmy, że masz już konstruktor obiektu
o nazwie Logger(), który ma zapisywać do pliku dane na temat zdarzeń w następujący sposób:
var my_log = new Logger();
my_log.log('jakieś zdarzenie');
// ... 1000 linii kodu później ...
276
Rozdział 8. • Wzorce kodowania i wzorce projektowe
Chodzi o to, żeby pomimo użycia operatora new tworzona była tylko jedna instancja i żeby to
ona była zwracana podczas kolejnych wywołań.
Zmienna globalna
Zdefiniowany powyżej cel można osiągnąć przy użyciu zmiennej globalnej przechowującej
pojedynczą instancję. Konstruktor musiałby wtedy wyglądać tak:
function Logger() {
if (typeof global_log === "undefined") {
global_log = this;
}
return global_log;
}
Pewnym minusem jest rzecz jasna sama zmienna globalna. Można ją nadpisać w dowolnym
momencie, również przypadkowo, a w takim wypadku traci się dostęp do instancji. Możliwe
jest również przypadkowe nadpisanie globalnej instancji utworzonej przez innego programistę.
Pole konstruktora
Znasz już to na pamięć: funkcje są obiektami, a jako takie posiadają pola. Możesz przypisać
pojedynczą instancję polu konstruktora.
function Logger() {
if (typeof Logger.single_instance === "undefined") {
Logger.single_instance = this;
}
return Logger.single_instance;
}
Jeśli zdefiniujesz zmienną a i przypiszesz jej zwracaną przez konstruktor wartość (var a = new
Logger()), a będzie wskazywało nowo utworzone pole — dokładnie tak, jak powinno.
W ten sposób unikniesz problemu globalnej przestrzeni nazw, ponieważ nie są tworzone żad-
ne zmienne globalne. Minus jest taki, że pole konstruktora jest dostępne w sposób publiczny,
zatem także może zostać nadpisane. W takim przypadku może dojść do utraty lub modyfikacji
pojedynczej instancji.
277
JavaScript. Programowanie obiektowe
Pole prywatne
Najlepsze rozwiązanie polega na rezygnacji ze stosowania pola dostępnego publicznie. Wiesz
już, w jaki sposób chronić dostęp do zmiennych za pomocą domknięć, zatem spróbuj samo-
dzielnie zaimplementować ten wzorzec w wersji z polem prywatnym, w ramach ćwiczeń do
tego rozdziału.
Fabryka
Fabryka to kolejny wzorzec konstrukcyjny. Przydaje się ona, gdy istnieje kilka podobnych ty-
pów obiektów, a programista nie jest w stanie z góry ocenić, który z nich okaże się potrzebny.
Decyzja zostanie podjęta w czasie wykonania, w reakcji na informację od użytkownika lub
inne kryteria.
Z konstruktorów korzysta się w dokładnie taki sam sposób: należy ustawić pole url oraz wy-
wołać metodę insert().
278
Rozdział 8. • Wzorce kodowania i wzorce projektowe
Teraz wyobraź sobie, że Twój program nie wie, który z obiektów będzie mu potrzebny. Użyt-
kownik dokonuje wyboru w czasie wykonania, na przykład klikając odpowiedni przycisk. Jeśli
type określa wymagany typ obiektu, prawdopodobnie konieczne okaże się zastosowanie in-
strukcji wyboru, takiej jak if lub switch:
var o;
if (type === 'Image') { //obrazek
o = new MYAPP.dom.Image();
}
if (type === 'Link') { //link
o = new MYAPP.dom.Link();
}
if (type === 'Text') { //tekst
o = new MYAPP.dom.Text();
}
o.url = 'http://...'
o.insert();
Kod oczywiście zadziała, ale jeśli konstruktorów będzie wiele, stanie się nieco przydługi. Poza
tym podczas tworzenia bibliotek nie zawsze można przewidzieć wszystkie możliwe typy kon-
struktorów. W takim wypadku o wiele lepiej sprawdza się funkcja factory (fabryka), która
zajmuje się tworzeniem obiektów typu określonego dynamicznie.
Powyższy kod jest jedynie prostym przykładem. Jeśli zamierzasz stosować ten wzorzec w prakty-
ce, prawdopodobnie konieczne będzie sprawdzanie poprawności przekazywanego typu oraz,
być może, wykonanie pewnych czynności konfiguracyjnych wspólnych dla wszystkich typów
obiektów.
279
JavaScript. Programowanie obiektowe
Dekorator
Dekorator należy do grupy wzorców strukturalnych. Nie opisuje on sposobu tworzenia obiektów,
tylko sposób rozszerzenia ich funkcjonalności. W przypadku dziedziczenia rozszerzanie funk-
cjonalności odbywa się w sposób liniowy (rodzic, dziecko, wnuk itd.). Innym możliwym roz-
wiązaniem jest stworzenie jednego obiektu podstawowego oraz puli różnych obiektów-
dekoratorów, które oferują dodatkowe funkcjonalności. Napisany przez Ciebie program może
następnie wybrać interesujące go dekoratory oraz ich kolejność. Różne programy mogą po-
bierać z tej samej puli różne zestawy dekoratorów. Przyjrzyj się, w jaki sposób można zaim-
plementować pobieranie dekoratorów:
var obj = {
function: zróbCoś(){
console.log('tak jest, już się robi');
},
// ...
};
obj = obj.getDecorator('deco1'); //wybór dekoratora
obj = obj.getDecorator('deco13'); //wybór dekoratora
obj = obj.getDecorator('deco5'); //wybór dekoratora
obj.zróbCoś();
Wszystko zaczyna się od prostego obiektu z metodą zróbCoś(). Następnie wybierane są pew-
ne obiekty-dekoratory (rozpoznawane za pomocą nazw). Wszystkie dekoratory posiadają me-
todę zróbCoś(), która najpierw wywołuje metodę o tej samej nazwie poprzedniego dekoratora,
a dopiero potem kontynuuje wykonywanie swojego własnego kodu. Każde dodanie dekorato-
ra powoduje nadpisanie obiektu obj jego ulepszoną wersją. Po dodaniu wszystkich wybranych
dekoratorów następuje wywołanie metody zróbCoś(). Wynikiem będzie wywołanie metod
zróbCoś() wszystkich wskazanych dekoratorów. Przeanalizujmy ten wzorzec na przykładzie
ubierania choinki.
Dekorowanie choinki
Łatwiej będzie Ci zrozumieć wzorzec dekorator na konkretnym przykładzie — niech będzie
to dekorowanie choinki. Zacznijmy od metody decorate():
var drzewko = {};
drzewko.decorate = function() {
alert('Upewnij się, że choinka się nie przewróci');
};
Potrzebna jest Ci także metoda getDecorator(), za pomocą której będziesz dodawać nowe
dekoratory. Dekoratory zostaną zaimplementowane jako konstruktory dziedziczące z bazowe-
go obiektu drzewko.
280
Rozdział 8. • Wzorce kodowania i wzorce projektowe
drzewko.getDecorator = function(deco){
drzewko[deco].prototype = this;
return new drzewko[deco];
};
W wyniku tego wywołania pojawią się następujące okienka alert() (dokładnie w tej kolejności):
Q Upewnij się, że choinka się nie przewróci
Q Dodaj niebieskie bombki
Q Gwiazda na czubku
Q Powieś kilka czerwonych bombek
Jak widzisz, możliwe jest dodawanie dowolnej liczby dekoratorów i łączenie ich w wybrany sposób.
281
JavaScript. Programowanie obiektowe
Obserwator
Wzorzec obserwator reprezentuje grupę wzorców czynnościowych, a co za tym idzie opisuje
interakcję i komunikację pomiędzy obiektami. W implementacji tego wzorca wyróżnia się na-
stępujące obiekty:
Q Jeden lub więcej obiektów obserwowanych (ang. publisher objects), które informują
o podejmowanych przez siebie ważnych działaniach.
Q Jeden lub więcej obiektów obserwujących (ang. subscriber objects), które podłączają
się do jednego lub wielu obiektów obserwowanych, czekają na informację od nich,
a następnie reagują w określony sposób.
Istnieją dwa podtypy tego wzorca: pchający (ang. push) i ciągnący (ang. pull). O push mówimy
wtedy, gdy to obserwowani są odpowiedzialni za poinformowanie podłączonych obserwato-
rów o zdarzeniu, a o pull, gdy to obserwujący muszą monitorować zmiany stanu obserwowa-
nych obiektów.
Oto obiekt observer, który zawiera wszystkie metody związane z nasłuchiwaniem. Dzięki
niemu dowolny obiekt może stać się obiektem obserwowanym, wysyłającym informacje do
wszystkich obserwatorów:
var observer = {
addSubscriber: function(callback) {
this.subscribers[this.subscribers.length] = callback;
},
282
Rozdział 8. • Wzorce kodowania i wzorce projektowe
removeSubscriber: function(callback) {
for (var i = 0; i < this.subscribers.length; i++) {
if (this.subscribers[i] === callback) {
delete(this.subscribers[i]);
}
}
},
publish: function(what) {
for (var i = 0; i < this.subscribers.length; i++) {
if (typeof this.subscribers[i] === 'function') {
this.subscribers[i](what);
}
}
},
make: function(o) { // zamienia obiekt w obiekt obserwowany
for(var i in this) {
o[i] = this[i];
o.subscribers = [];
}
}
};
Kolejnym obserwowanym obiektem może być gazeta „LA Times”, która wywołuje publish(),
gdy gotowy jest kolejny numer.
var la_times = {
newIssue: function() {
var paper = 'Marsjanie wylądowali na Ziemi!';
this.publish(paper);
}
};
283
JavaScript. Programowanie obiektowe
jaś i adaś mogą podłączyć się do tego samego obiektu blogger, wskazując metody, które mają
być wywołane po opublikowaniu nowych treści.
blogger.addSubscriber(jaś.czytaj);
blogger.addSubscriber(adaś.plotkuj);
Co nastąpi, gdy obiekt blogger napisze nowy post? jaś i adaś zostaną o tym poinformowani:
>>> blogger.napiszPost();
adaś może także rozpocząć obserwację gazety „LA Times”, jako że obiekt może odbierać in-
formacje od wielu obiektów publikujących:
>>> la_times.addSubscriber(adaś.plotkuj);
Po pojawieniu się nowego numeru „LA Times” zostanie wywołana metoda adaś.plotkuj().
>>> la_times.newIssue();
284
Rozdział 8. • Wzorce kodowania i wzorce projektowe
Podsumowanie
W ósmym i ostatnim rozdziale tej książki przedstawiłem popularne wzorce kodowania w ję-
zyku JavaScript, dzięki którym programy stają się bardziej czytelne, szybsze i lepiej współ-
pracują z innymi programami i bibliotekami. Następnie omówiłem ideę i implementację czte-
rech przykładowych wzorców projektowych pochodzących z tzw. Księgi czterech. Starałem
się pokazać Ci, że JavaScript to w pełni funkcjonalny, dynamiczny język obiektowy oraz że
implementacja klasycznych wzorców projektowych w języku dynamicznym wcale nie musi
być trudna. Wzorce to temat rzeka. Zachęcam Cię do odwiedzenia mojej strony poświęconej
wzorcom pod adresem JSPatterns.com.
Po przeczytaniu tej książki jesteś w posiadaniu wiedzy, która umożliwi Ci tworzenie skalo-
walnych, przenośnych i zaawansowanych aplikacji i bibliotek w języku JavaScript z zastoso-
waniem najlepszych praktyk programowania obiektowego. Bon voyage!
285
JavaScript. Programowanie obiektowe
286
A
Słowa zarezerwowane
Ten dodatek zawiera dwie listy słów zarezerwowanych. Pierwsza z nich to lista słów, które już
teraz mają określone znaczenie w języku, druga to lista słów zarezerwowanych na użytek przy-
szłych implementacji.
Słowa zarezerwowane mogą być nazwami pól obiektu pod warunkiem, że zostaną umieszczo-
ne w cudzysłowie (lub otoczone apostrofami).
var o = {break: 1}; // OK w Firefoksie, błąd w IE
var o = {'break': 1}; // OK
alert(o.break); // błąd IE
alert(o['break']); // OK
Q else
Q finally
Q for
Q function
Q if
Q in
Q instanceof
Q new
Q return
Q switch
Q this
Q throw
Q try
Q typeof
Q var
Q void
Q while
Q with
288
Dodatek A • Słowa zarezerwowane
Q implements
Q import
Q int
Q interface
Q long
Q native
Q package
Q private
Q protected
Q public
Q short
Q static
Q super
Q synchronized
Q throws
Q transient
Q volatile
289
JavaScript. Programowanie obiektowe
290
B
Funkcje wbudowane
Ten dodatek zawiera listę funkcji wbudowanych (tzn. metod obiektu globalnego), którym zo-
stał poświęcony rozdział 3.
Funkcja Opis
parseInt() Pobiera dwa parametry: obiekt wejściowy oraz podstawę i, jeśli to możliwe, zwraca
wynik będący liczbą całkowitą. Domyślnie za podstawę jest przyjmowana liczba 10
(co oznacza, że zwrócona zostanie liczba dziesiętna). Jeśli przekształcenie obiektu
wejściowego na liczbę całkowitą nie jest możliwe, funkcja zwróci NaN. Pominięcie
podstawy może doprowadzić do nieoczekiwanych wyników (na przykład gdy jako
parametr przekazany zostanie łańcuch znaków '08', który zostanie uznany za liczbę
ósemkową), dlatego najlepiej jest zawsze ją podawać.
>>> parseInt('10e+3')
10
>>> parseInt('FF')
NaN
>>> parseInt('FF', 16)
255
parseFloat() Funkcja pobiera jeden parametr, który próbuje przekształcić na liczbę zmiennoprzecinkową.
Parametr może być zapisany w postaci wykładniczej.
>>> parseFloat('10e+3')
10000
>>> parseFloat('123.456test')
123.456
JavaScript. Programowanie obiektowe
Funkcja Opis
isNaN() Nazwa funkcji jest skrótem od „is Not a Number”, czyli „nie jest liczbą”. Funkcja
pobiera jeden parametr. Jeśli nie jest on liczbą, zwracana jest wartość true,
w przeciwnym razie — wartość false. Sprawdzenie polega na próbie przekształcenia
parametru w liczbę.
>>> isNaN(NaN)
true
>>> isNaN(123)
false
>>> isNaN(parseInt('FF'))
true
>>> isNaN(parseInt('FF', 16))
false
isFinite() Zwraca true, jeśli parametr jest liczbą (lub może zostać przekształcony w liczbę),
ale jego wartością nie jest Infinity ani –Inifinity. Dla wartości nieliczbowych
oraz dla nieskończoności funkcja zwraca false.
>>> isFinite(1e+1000)
false
>>> isFinite(-Infinity)
false
>>> isFinite("123")
true
encodeURIComponent() Przekształca parametr w poprawnie zakodowany adres URL (lub jego fragment).
Więcej informacji na temat kodowania URL można znaleźć w angielskiej Wikipedii
pod adresem http://en.wikipedia.org/wiki/Url_encode.
>>>encodeURIComponent('http://phpied.com/')
"https%3A%2F%2Ffanyv88.com%3A443%2Fhttp%2Fphpied.com%2F"
>>> encodeURIComponent('jakiś skrypt?klucz=w@rtość')
"jaki%C5%9B%20skrypt%3Fklucz%3Dw%40rtosc"
decodeURIComponent() Pobiera zakodowany URL i sprowadza go do oryginalnej postaci.
>>> decodeURIComponent('%20%40')
"@"
encodeURI() Koduje URL, zakładając jednak, że podano pełen adres. Nazwa protokołu
(na przykład 'http:// ') i nazwa serwera (na przykład 'www.phpied.com')
nie zostaną zakodowane.
>>> encodeURI('http://phpied.com/')
"http://phpied.com/"
>>> encodeURI('jakiś skrypt?klucz=w@rtość')
"jaki%C5%9B%20skrypt?klucz=w@rto%C5%9B%C4%87"
292
Dodatek B • Funkcje wbudowane
Funkcja Opis
decodeURI() Odwrotność encodeURI().
>>>decodeURI("jaki%C5%9B%20skrypt?klucz=w@rto%C5%9B%C4%87")
"jakiś skrypt?klucz=w@rtość"
eval() Pobiera łańcuch znaków i uruchamia go jako kod w języku JavaScript. Zwraca wynik
ostatniego wyrażenia z łańcucha wejściowego.
Należy unikać stosowania tej funkcji.
>>> eval('1+2')
3
>>> eval('parseInt("123")')
123
>>> eval('new Array(1,2,3)')
[1, 2, 3]
>>> eval('new Array(1,2,3); 1+1;')
2
293
JavaScript. Programowanie obiektowe
294
C
Obiekty wbudowane
Object
Object() to konstruktor tworzący obiekty. Na przykład:
>>> var o = new Object();
String()
>>> var o = new Object(123);
>>> o.constructor
Number()
JavaScript. Programowanie obiektowe
Wszystkie obiekty, zarówno wbudowane, jak i stworzone przez programistę na potrzeby danej
aplikacji, dziedziczą z Object. W związku z tym wszystkie obiekty posiadają pola i metody
przedstawione poniżej.
296
Dodatek C • Obiekty wbudowane
297
JavaScript. Programowanie obiektowe
Array
Konstruktor Array() tworzy tablicę.
>>> var a = new Array(1,2,3);
Jeśli konstruktorowi zostanie przekazana pojedyncza wartość liczbowa, zostanie ona uznana
za długość tablicy. Zostanie utworzona tablica danej długości, wypełniona elementami undefined.
>>> var a = new Array(3);
>>> a.length
3
>>> a
[3.14]
298
Dodatek C • Obiekty wbudowane
299
JavaScript. Programowanie obiektowe
300
Dodatek C • Obiekty wbudowane
Function
W języku JavaScript funkcje są obiektami. Można je definiować za pomocą konstruktora
Function():
>>> var sum = new Function('a', 'b', 'return a + b;');
301
JavaScript. Programowanie obiektowe
Boolean
Konstruktor Boolean() tworzy obiekty typu Boolean (nie należy ich mylić z prostym typem
danych boolean). Obiekty te nie są zbyt przydatne — wymieniam je, ponieważ niniejsza lista
ma być kompletna.
>>> var b = new Boolean();
>>> b.valueOf()
false
>>> b.toString()
"false"
Obiekt utworzony za pomocą konstruktora Boolean() nie jest tym samym co prosta wartość
boolean. Wiadomo, że wszystkie obiekty są „prawdziwe”:
>>> b === false
false
>>> typeof b
"object"
Number
Tworzy obiekty typu Number, reprezentujące liczby:
>>> var n = new Number(101);
>>> typeof n
"object"
>>> n.valueOf();
101
Metody obiektów Number mogą być wywoływane na zmiennych liczbowych typu prostego —
dana zmienna zostanie wówczas niejawnie przekształcona na obiekt Number, a kod zadziała
zgodnie z oczekiwaniami.
>>> var n = 123;
>>> typeof n;
"number"
302
Dodatek C • Obiekty wbudowane
>>> n.toString()
"123"
303
JavaScript. Programowanie obiektowe
String
Konstruktor String() tworzy obiekty reprezentujące łańcuchy znaków. Odpowiadający im prosty
typ string zostanie przekształcony na obiekt, jeśli na zmiennej tego typu zostanie wywołana
metoda.
"object"
304
Dodatek C • Obiekty wbudowane
"string"
false
>>> s_obj == s_prim
true
Jeśli spróbujesz sprawdzić wartość pola length łańcucha znaków, który nie jest obiektem, zostanie
on niejawnie przekształcony na obiekt, dzięki czemu operacja zakończy się powodzeniem.
>>> "coś".length
305
JavaScript. Programowanie obiektowe
306
Dodatek C • Obiekty wbudowane
307
JavaScript. Programowanie obiektowe
Date
Konstruktor Date() może pobierać argumenty różnego typu:
Q Możesz podać wartości określające rok, miesiąc, dzień, godzinę, minutę, sekundę
i milisekundę, w następujący sposób:
>>> new Date(2011, 0, 1, 13, 30, 35, 500)
Sat Jan 01 2011 13:30:35 GMT+0100
Q Możesz pominąć dowolne z parametrów wejściowych. W takim wypadku zostanie
im nadana wartość 0. Zwróć uwagę, że miesiące liczone są od 0 (styczeń) do 11
(grudzień), godziny od 0 do 12, minuty i sekundy od 0 do 59, a milisekundy od 0 do 999.
Q Możesz przekazać znacznik czasu:
>>> new Date(1293917435500)
Sat Jan 01 2011 13:30:35 GMT+0100
Q Jeśli nie podasz argumentu, zostanie utworzony obiekt Date() reprezentujący
aktualną datę i czas:
>>> new Date()
Fri Apr 18 2008 01:13:00 GMT+0100
Q Jeśli przekażesz konstruktorowi łańcuch znaków, zostanie on sparsowany
w poszukiwaniu fragmentów określających datę:
>>> new Date('May 4, 2008')
Sun May 04 2008 00:00:00 GMT+0100
308
Dodatek C • Obiekty wbudowane
309
JavaScript. Programowanie obiektowe
310
Dodatek C • Obiekty wbudowane
Math
Math różni się od innych obiektów wbudowanych tym, że nie może być używany jako kon-
struktor do tworzenia obiektów — jest jedynie zbiorem funkcji i stałych. Poniżej przedstawi-
łem kilka przykładów, które uwidoczniają wspomnianą różnicę:
>>> typeof String.prototype
"object"
>>> typeof Date.prototype
"object"
>>> typeof Math.prototype
"undefined"
>>> typeof Math
"object"
>>> typeof String
"function"
311
JavaScript. Programowanie obiektowe
312
Dodatek C • Obiekty wbudowane
RegExp
Obiekt reprezentujący wyrażenie regularne można utworzyć przy użyciu konstruktora RegExp(),
przekazując mu jako pierwszy argument wzorzec wydarzenia, a jako drugi — modyfikatory.
>>> var re = new RegExp('[dn]o+dle', 'gmi');
Powyższe wyrażenie zostanie dopasowane do "noodle", "doodle", "doooodle" itp. Ten sam
efekt da użycie literału:
>>> var re = ('/[dn]o+dle/gmi'); // zalecane
313
JavaScript. Programowanie obiektowe
314
Dodatek C • Obiekty wbudowane
Obiekty Error
Obiekty błędów mogą być tworzone albo przez środowisko (przeglądarkę), albo przez Twój kod.
>>> var e = new Error('nie znam słowa jaavcsritp');
>>> typeof e
"object"
Poza konstruktorem Error istnieje jeszcze sześć konstruktorów błędów, z których wszystkie
dziedziczą z Error:
Q EvalError (błąd obliczeń);
Q RangeError (błąd zakresu);
Q ReferenceError (błąd referencji);
Q SyntaxError (błąd składni);
Q TypeError (błąd typu);
Q URIError (błąd URI).
315
JavaScript. Programowanie obiektowe
316
D
Wyrażenia regularne
["me"]
Jednak siła wyrażeń regularnych leży nie w dopasowaniach napisów znaków, ale w dopaso-
waniach wzorców. Poniższa tabela przedstawia składnię wyrażeń regularnych oraz przykłady
ich użycia.
Wzorzec Opis
[abc] Dopasowanie do znaków z danej klasy (grupy).
>>> "fragment tekstu".match(/[otu]/g)
["t", "t", "t", "u"]
[a-z] Klasa znaków zdefiniowana jako zakres. Przykładowo [a-d] odpowiada [abcd], [a-z] zostanie
dopasowane do wszystkich małych liter (podstawowego alfabetu łacińskiego), [a-zA-Z0-9_]
zostanie dopasowane do wszystkich liter (znowu: podstawowego alfabetu łacińskiego), liczb
lub znaku podkreślnika.
>>> "Fragment Tekstu".match(/[a-z]/g)
["r", "a", "g", "m", "e", "n", "t", "e", "k", "s", "t", "u"]
>>> "Fragment Tekstu".match(/[a-zA-Z]/g)
["F", "r", "a", "g", "m", "e", "n", "t", "T", "e", "k", "s", "t", "u"]
[^abc] Pasuje do wszystkiego, co nie zostanie dopasowane do danej klasy znaków.
>>> "Fragment Tekstu".match(/[^a-z]/g)
["F", " ", "T"]
JavaScript. Programowanie obiektowe
Wzorzec Opis
a|b Pasuje do a albo b. Znak | oznacza „lub”. Można użyć go więcej niż raz.
>>> "Fragment Tekstu".match(/t|T/g);
["t", "T", "t"]
>>> "Fragment Tekstu".match(/t|T|Fragment/g);
["Fragment", "T", "t"]
a(?=b) Wzorzec zostanie dopasowany do a tylko, jeśli po a następuje b.
>>> "Fragment Tekstu".match(/Fragment(?=Tek)/g);
null
>>> "Fragment Tekstu".match(/Fragment(?= Tek)/g);
["Fragment"]
a(?!b) Wzorzec zostanie dopasowany do a tylko, jeśli po a nie następuje b.
>>> "Fragment Tekstu".match(/Fragment(?! Tek)/g);
null
>>> "Fragment Tekstu".match(/Fragment(?!Tek)/g);
["Fragment"]
\ Znak uniku, który pozwala na wyłączenie specjalnego znaczenia danego znaku wewnątrz wzorca.
>>> "R2-D2".match(/[2-3]/g)
["2", "2"]
>>> "R2-D2".match(/[2\-3]/g)
["2", "-", "2"]
\n Nowa linia.
\r Powrót karetki.
\f Wysunięcie strony.
\t Tabulator poziomy.
\v Tabulator pionowy.
\s Spacja lub dowolny z pięciu powyższych znaków.
>>> "R2\n D2".match(/\s/g)
["\n", " "]
\S Odwrotność powyższego: pasuje do wszystkiego oprócz zdefiniowanych powyżej białych
znaków. Ten wzorzec jest równoważny [^\s].
>>> "R2\n D2".match(/\S/g)
["R", "2", "D", "2"]
\w Dowolna litera (podstawowego alfabetu łacińskiego), liczba lub podkreślnik. Odpowiada
[A-Za-z0-9_].
>>> "Fragment tekstu!".match(/\w/g)
["F", "r", "g", "m", "e", "n", "t", "t", "e", "k", "s", "t", "u"]
318
Dodatek D • Wyrażenia regularne
Wzorzec Opis
\W Odwrotność \w.
>>> "Fragment tekstu!".match(/\W/g)
[" ", "!"]
\d Pasuje do cyfr, dokładnie jak [0-9].
>>> "R2-D2 i C-3PO".match(/\d/g)
["2", "2", "3"]
\D Odwrotność \d. Pasuje do wszystkiego, co nie jest cyfrą — jak [^0-9] lub [^\d].
>>> "R2-D2 i C-3PO".match(/\D/g)
["R", "-", "D", " ", "i", " ", "C", "-", "P", "O"]
\b Dopasowanie do granicy słowa, takiej jak spacja lub znak interpunkcyjny.
Poniższy wzorzec zostanie dopasowany do R lub D, po którym następuje cyfra 2:
>>> "R2D2 i C-3PO".match(/[RD]2/g)
["R2", "D2"]
Jak wyżej, ale tylko na końcu słowa:
>>> "R2D2 i C-3PO".match(/[RD]2\b/g)
["D2"]
Ten sam wzorzec, ale wejściowy ciąg znaków zawiera łącznik, który także jest rozpoznawany
jako koniec słowa:
>>> "R2-D2 i C-3PO".match(/[RD]2\b/g)
["R2", "D2"]
\B Odwrotność \b.
>>> "R2-D2 i C-3PO".match(/[RD]2\B/g)
null
>>> "R2D2 i C-3PO".match(/[RD]2\B/g)
["R2"]
[\b] Znak powrotu (backspace).
\0 Znak null (czyli bajt zerowy).
\u0000 Wzorzec zostanie dopasowany do znaku w formacie Unicode reprezentowanego
przez czterocyfrową liczbę szesnastkową.
>>> " ".match(/\u0441\u0442\u043E/)
[" "]
\x00 Pasuje do znaków reprezentowanych za pomocą dwucyfrowej liczby szesnastkowej.
>>> "dudek".match(/\x64/g)
["d", "d"]
319
JavaScript. Programowanie obiektowe
Wzorzec Opis
^ Początek przeszukiwanego łańcucha znaków. Jeśli ustawiony jest modyfikator m
(tryb wielolinijkowy), wzorzec zostanie dopasowany do początku każdej linii.
>>> "regularne\nwyrażenie\nregularne".match(/r/g);
["r", "r", "r", "r", "r"]
>>> "regularne\nwyrażenie\nregularne".match(/^r/g);
["r"]
>>> "regularne\nwyrażenie\nregularne".match(/^r/mg);
["r", "r"]
$ Pasuje do końca łańcucha znaków lub, w trybie wielolinijkowym, do końca każdej linii.
>>> "regularne\nwyrażenie\nregularne".match(/e$/g);
["e"]
>>> "regularne\nwyrażenie\nregularne".match(/e$/mg);
["e", "e", "e"]
. Pasuje do każdego znaku oprócz nowej linii i końca wiersza.
>>> "regularne".match(/r./g);
["re"]
>>> "regularne".match(/r.../g);
["regu"]
* Pasuje do zera lub więcej wystąpień wzorca poprzedzającego znak. Przykładowo wzorzec
/.*/ pasuje do wszystkiego, także do pustego łańcucha znaków.
>>> "".match(/.*/)
[""]
>>> "cokolwiek".match(/.*/)
["cokolwiek"]
>>> "cokolwiek".match(/k.*e/)
["kolwie"]
? Pasuje do jednego lub więcej wystąpień wzorca poprzedzającego znak.
>>> "cokolwiek".match(/ok?/g)
["ok", "o"]
+ Pasuje do poprzedzającego znak wzorca, jeśli występuje on co najmniej raz.
>>> "cokolwiek".match(/ko+/g)
["ko"]
>>> "R2-D2 i C-3PO".match(/[a-z]/gi)
["R", "D", "i", "C", "P", "O"]
>>> "R2-D2 i C-3PO".match(/[a-z]+/gi)
["R", "D", "i", "C", "PO"]
320
Dodatek D • Wyrażenia regularne
Wzorzec Opis
{n} Pasuje do wzorca poprzedzającego znak, jeśli występuje on dokładnie n razy.
>>> "hossa".match(/s/g)
["s", "s"]
>>> "hossa".match(/s{2}/g)
["ss"]
>>> "wyrażenie regularne".match(/\b\w{3}/g)
["wyr", "reg"]
{min,max} Pasuje do wzorca poprzedzającego znak, jeśli występuje on pomiędzy min a max razy.
Można pominąć wartość max, aby określić tylko wartość minimalną. Nie można pominąć min.
W przykładzie litera „o” została powtórzona 10 razy.
>>> "haloooooooooo".match(/o/g)
["o", "o", "o", "o", "o", "o", "o", "o", "o", "o"]
>>> "haloooooooooo".match(/o{2}/g)
["oo", "oo", "oo", "oo", "oo"]
>>> "haloooooooooo".match(/o{2,}/g)
["oooooooooo"]
>>> "haloooooooooo".match(/o{2,6}/g)
["oooooo", "oooo"]
{wzorzec} Wzorzec otoczony nawiasami klamrowymi zostanie zapamiętany, dzięki czemu będzie
można podstawić go w innym miejscu.
Kolejne dopasowania są dostępne jako $1, $2… $9.
>>> "wyrażenie regularne".replace(/(r)/g, '$1$1')
"wyrrażenie rregularrne"
Dopasowanie "re" i podstawienie "ee":
>>> "wyrażenie regularne".replace(/(r)(e)/g, '$2$1')
"wyrażenie ergularne"
{?:wzorzec} Wzorzec, który ma zostać pominięty na liście $1, $2…
W poniższym przykładzie zostanie dopasowane fragment "re", ale "r" nie jest pamiętane,
w związku z czym jako $1 jest dostępny drugi wzorzec:
>>> "wyrażenie regularne".replace(/(?:r)(e)/g, '$1$1')
"wyrażenie eegularne"
Zwróć uwagę, że niektóre znaki specjalne mogą mieć więcej niż jedno znaczenie. Jest tak
w przypadku ^, ? i \b.
321
JavaScript. Programowanie obiektowe
322
Skorowidz
--, 39 adres
!, 49 URI, 80
!!, 50 URL, 80, 211
!=, 53 agregacja, 29
!==, 53 AJAX, 19, 25, 250
&&, 49 aktualizacja elementów tablicy, 57
/* */, 70 aktualnie załadowany dokument, 219
//, 70 alert(), 81, 118
? :, 63 AND, 49
__proto__, 163, 172 anulowanie zachowania domyślnego, 248
||, 49 aplikacje
++, 39 AJAX, 25
<, 54 rich media, 26
<=, 54 Web 2.0, 23
<div>, 243 appendChild(), 233, 234, 236
<font>, 263 apply(), 125, 155, 194, 301
<iframe>, 213 arguments, 126
<script>, 207, 263 argumenty, 74
<title>, 241 Array, 117, 118, 120, 298
=, 39 join(), 120
==, 53 składowe, 298
===, 53 slice(), 120
>, 53 sort(), 120
>=, 53 assign(), 212
asynchroniczność, 254
Asynchronous JavaScript and XML, 250
A atrybuty, 225
abort, 250 atrybuty znacznika, 239
ActiveXObject, 253 attachEvent(), 249, 263
addEventListener(), 243, 244, 246, 263 Attr, 222
addListener(), 268 availHeight, 215
Adobe Air, 26
Skorowidz
B D
bąbelkowanie zdarzeń, 244 dane anonimowe, 84
beforeunload, 250 dane JSON, 254, 274
blok kodu, 60 Date, 117, 136, 308
blokowanie pola, 239 getDay(), 140
blur, 250 getMonth(), 139
błędy, 146, 315 parse(), 139, 308
body, 228 setMonth(), 139
BOM, 208, 209, 257 składowe, 308
window, 209 UTC(), 139, 308
boolean, 40, 48 daty, 136, 308
Boolean, 117, 127, 302 decodeURI(), 76, 80, 293
break, 64 decodeURIComponent(), 76, 80, 292
Browser Object Model, 209, 257 default, 64
definiowanie funkcji, 122
deklaracja
C funkcje, 74
call(), 125, 155, 194, 301 zmienne, 36
callback function, 85 dekorator, 280
cancelBubble, 249 stosowanie, 280
case, 64 dekrementacja, 39
catch, 148 delegowanie zdarzeń, 246
change, 250 delete, 58
CharacterData, 222 detachEvent(), 249
charAt(), 132, 305 Digg, 23
charCodeAt(), 306 do…while, 66
childNodes, 224 document, 223
ciało funkcji, 74 Document, 221
ciągi znaków, 45 Document Object Model, 219, 257
clearInterval(), 218 document.anchors, 239
clearTimeout(), 218 document.applets, 239
click, 250 document.cookie, 240
cloneNode(), 235 document.documentElement, 224
Comment, 222 document.domain, 241
concat(), 298, 306 document.forms, 239
confirm(), 216 document.getElementsByTagName(), 239
console, 116 document.images, 239
constructor, 112, 117, 296 document.links, 239
contextmenu, 250 document.location, 242
cookies, 240 document.nodeName, 223
Core DOM, 221 document.nodeType, 223
createElement(), 233 document.nodeValue, 224
createTextNode(), 233 document.referrer, 241
CSS, 23, 231, 262 document.title, 241
kolory, 42 document.write(), 240
document.writeln(), 240
documentElement, 224
documentElement.nodeName, 224
324
Skorowidz
325
Skorowidz
326
Skorowidz
327
Skorowidz
M N
match(), 134, 143, 306 nadklasy, 180
Math, 117, 135, 311 nadpisanie, 29
Math.abs(), 313 nadpisywanie pól prototypu, 159
Math.acos(), 312 name, 315
Math.asin(), 312 NamedNodeMap, 222
Math.atan(), 136, 312 namespace(), 266
Math.atan2(), 312 NaN, 44, 45
Math.ceil(), 136, 312 narzędzia, 31
Math.cos(), 136, 312 navigator, 210
328
Skorowidz
329
Skorowidz
P pola, 158
problemy, 167
parametry, 74 prywatne składowe, 270
parseFloat(), 76, 78, 291 przechodzenie przez drzewo DOM, 230
parseInt(), 46, 76, 291 przechowywanie danych, 35
pętle, 60, 65 przechwytywanie zdarzeń, 244
do…while, 66 przeglądarki internetowe, 23
domknięcie, 96 przekazywanie obiektów, 114
for, 66 przenoszenie wspólnych pól do prototypu, 175
for…in, 69 przesłanianie metod, 31
nieskończone, 65 przesłonięcie, 29
while, 66 przestrzenie nazw, 264
Pierwsza Wojna Przeglądarkowa, 24 konstruktory, 265
pierwsze dziecko, 228 namespace(), 266
płytka kopia, 187 obiekty, 264
pobieranie listy pól, 160 przyciski, 239
pobieranie wartości, 98 public, 28
Point, 200 publisher objects, 282
pola, 27, 105 pull, 282
constructor, 112 push, 282
prywatne, 278 push(), 299
pola elementów, 242
pola tekstowe, 239
pole konstruktora, 277
R
polimorfizm, 30, 31 ramki, 213
pop(), 299 RangeError, 315
porównywanie, 53 readyState, 252, 254
obiekty, 114 readystatechange, 251, 252
POST, 251 ReferenceError, 315
prawa dostępu, 270 Referer, 241
preventDefault(), 248, 249, 268 referrer, 240
previousSibling, 228, 229 RegExp, 117, 140, 313
priorytety operatorów, 51 exec(), 142
private, 28 global, 141
Processing, 34 ignoreCase, 141
programowanie obiektowe, 27, 30 lastIndex, 141
prompt(), 216 multiline, 141
propertyIsEnumerable(), 161, 162, 297 składowe, 314
proste przypisanie wartości, 39 source, 141
protected, 28 test(), 142
prototype, 155 regular expressions, 140
prototypy, 155 releaseCapture(), 246
__proto__, 163 reload(), 212
dodawanie składowych, 156 removeChild(), 236
dziedziczenie, 177, 190 removeEventListener(), 247
isPrototypeOf(), 162 removeListener(), 268
korzystanie ze składowych, 157 replace(), 134, 143, 144, 212, 306
nadpisywanie pól, 159 replaceChild(), 237
pobieranie listy pól, 160 reset, 250
330
Skorowidz
331
Skorowidz
tablice
indeksy, 57
U
iteracja po elementach, 69 uber, 180
tworzenie, 118 uchwyt zdarzenia, 242
usuwanie elementów, 58 ukrywanie informacji, 28
tablice asocjacyjne, 105 ułamki, 41
tablice tablic, 58 undefined, 40, 54
tekst, 45 unescape(), 80
test(), 315 Uniform Resource Identifier, 80
Text, 222 Uniform Resource Locator, 80
textContent, 226, 227 unload, 250
this, 109, 158 unshift(), 300
title, 240 URI, 80
toDateString(), 309 URIError, 315
toExponential(), 129, 304 URL, 80
toFixed(), 129, 304 userAgent, 210
toLocaleDateString(), 309 ustalanie typu danych, 41
toLocaleLowerCase(), 307 ustawienia monitora, 214
toLocaleString(), 297, 309 ustawienie wartości, 98
toLocaleTimeString(), 309 usuwanie węzłów, 236
toLocaleUpperCase(), 307
toLowerCase(), 132, 307
toPrecision(), 129, 304
V
toString(), 117, 118, 173, 296 valueOf(), 117, 127, 297
toTimeString(), 309 var, 36
toUpperCase(), 132, 307 visibility, 233
toUTCString(), 309
treść strony, 262
Triangle, 203 W
true, 49, 128 walidacja formularzy, 24, 232
try, 148 warstwy
tworzenie prezentacja, 23, 263
łańcuch prototypów, 172 struktura, 23
obiekty, 109 treść, 262
obiekty XHR w IE w wersjach starszych niż 7, 253 zachowanie, 23, 263
tablice, 118 wartość boolowska, 40
węzły, 233 wartość NaN, 45
typ żądania HTTP, 251 wartość null, 54
TypeError, 315 warunki, 60
typeof, 41, 83 if, 61
typy danych, 40, 56 switch, 63
boolean, 40, 48 wcięcia, 60
liczby, 40, 41 Web 2.0, 19, 23
łańcuchy znaków, 40, 45 węzeł document, 223
niezdefiniowany, 40 węzły, 220, 222
null, 40, 54 kopiowanie, 235
object, 57 modyfikacja, 230
undefined, 54 tworzenie, 233
ustalanie typu danych, 41 usuwanie, 236
332
Skorowidz
333
Skorowidz
334
Notatki