0% found this document useful (0 votes)
18 views336 pages

JavaScript. Programowanie Obiektowe

Uploaded by

hubertcior
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
18 views336 pages

JavaScript. Programowanie Obiektowe

Uploaded by

hubertcior
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 336

Tytuł oryginału: Object-Oriented JavaScript

Tłumaczenie: Justyna Walkowska

ISBN: 978-83-246-5812-1

Copyright © Packt Publishing 2008. First published in the English language


under the title “Object-Oriented JavaScript”

Translation copyright © 2010 by Wydawnictwo Helion.

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.

Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości


lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione.
Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie
książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie
praw autorskich niniejszej publikacji.

Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi


bądź towarowymi ich właścicieli.

Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte


w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej
odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne
naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION
nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe
z wykorzystania informacji zawartych w książce.

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)

Pliki z przykładami omawianymi w książce można znaleźć pod adresem:


ftp://ftp.helion.pl/przyklady/jascob.zip

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.

• Poleć książkę na Facebook.com • Księgarnia internetowa


• Kup w wersji papierowej • Lubię to! » Nasza społeczność
• Oceń książkę
Chciałbym zadedykować tę książkę mojej żonie Evie oraz moim córkom
Zlatinie i Nathalie. Dziękuję Wam za cierpliwość i wsparcie.

Moim recenzentom, którzy z własnej, nieprzymuszonej woli poświęcili czas


na czytanie i recenzję książki na różnych etapach jej powstawania, chciałbym wyrazić
swój podziw i szacunek oraz podziękować za ich nieoceniony wkład.
Spis treści

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ł 2. Proste typy danych, tablice, pętle i warunki 35


Zmienne 35
Wielkość liter ma znaczenie 36
Operatory 37
Spis treści

Proste typy danych 40


Ustalanie typu danych — operator typeof 41
Liczby 41
Liczby ósemkowe i szesnastkowe 41
Wykładniki potęg 42
Nieskończoność 43
NaN 45
Łańcuchy znaków 45
Konwersje łańcuchów 46
Znaki specjalne 47
Typ boolean 48
Operatory logiczne 49
Priorytety operatorów 51
Leniwe wartościowanie 52
Porównywanie 53
Undefined i null 54
Proste typy danych — podsumowanie 56
Tablice 56
Dodawanie i aktualizacja elementów tablicy 57
Usuwanie elementów 58
Tablice tablic 58
Warunki i pętle 60
Bloki kodu 60
Warunki if 61
Sprawdzanie, czy zmienna istnieje 62
Alternatywna składnia if 63
Switch 63
Pętle 65
Pętla while 66
Pętla do…while 66
Pętla for 66
Pętla for…in 69
Komentarze 70
Podsumowanie 71
Ćwiczenia 71

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

Rozdział 4. Obiekty 103


Od tablic do obiektów 103
Elementy, pola, metody 105
Tablice asocjacyjne 105
Dostęp do własności obiektu 106
Wywoływanie metod obiektu 107
Modyfikacja pól i metod 108
Wartość this 109
Konstruktory 109
Obiekt globalny 110
Pole constructor 112
Operator instanceof 112
Funkcje zwracające obiekty 113
Przekazywanie obiektów 114
Porównywanie obiektów 114
Obiekty w konsoli Firebug 115
Obiekty wbudowane 117
Object 117
Array 118
Ciekawe metody obiektu Array 120
Function 122
Własności obiektu Function 123
Metody obiektu Function 125
Nowe spojrzenie na obiekt arguments 126
Boolean 127
Number 128

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

Rozdział 5. Prototypy 155


Pole prototype 155
Dodawanie pól i metod przy użyciu prototypu 156
Korzystanie z pól i metod obiektu prototype 157
Własne pola obiektu a pola prototypu 158
Nadpisywanie pól prototypu własnymi polami obiektu 159
Pobieranie listy pól 160
isPrototypeOf() 162
Ukryte powiązanie __proto__ 163
Rozszerzanie obiektów wbudowanych 165
Rozszerzanie obiektów wbudowanych — kontrowersje 166
Pułapki związane z prototypami 167
Podsumowanie 169
Ćwiczenia 170

Rozdział 6. Dziedziczenie 171


Łańcuchy prototypów 172
Przykładowy łańcuch prototypów 172
Przenoszenie wspólnych pól do prototypu 175
Dziedziczenie samego prototypu 177
Konstruktor tymczasowy — new F() 178
Uber: dostęp do obiektu-rodzica 180
Zamknięcie dziedziczenia wewnątrz funkcji 181
Kopiowanie pól 182
Uwaga na kopiowanie przez referencję! 184
Obiekty dziedziczą z obiektów 186
Głębokie kopiowanie 187
object() 189
Połączenie dziedziczenia prototypowego z kopiowaniem pól 190

8
Spis treści

Dziedziczenie wielokrotne 191


Miksiny 193
Dziedziczenie pasożytnicze 193
Wypożyczanie konstruktora 194
Pożycz konstruktor i skopiuj jego prototyp 196
Podsumowanie 197
Studium przypadku: rysujemy kształty 200
Analiza 200
Implementacja 201
Testowanie 204
Ćwiczenia 205

Rozdział 7. Środowisko przeglądarki 207


Łączenie JavaScriptu z kodem HTML 207
BOM i DOM — przegląd 208
BOM 209
Ponownie odkrywamy obiekt window 209
window.navigator 210
Firebug jako ściąga 210
window.location 211
window.history 212
window.frames 213
window.screen 214
window.open() i window.close() 215
window.moveTo(), window.resizeTo() 216
window.alert(), window.prompt(), window.confirm() 216
window.setTimeout(), window.setInterval() 217
window.document 219
DOM 219
Core DOM i HTML DOM 221
Dostęp do węzłów DOM 222
Węzeł document 223
documentElement 224
Węzły-dzieci 224
Atrybuty 225
Dostęp do zawartości znacznika 226
Uproszczone metody dostępowe DOM 227
Rówieśnicy, body, pierwsze i ostatnie dziecko 228
Spacer przez węzły DOM 230
Modyfikacja węzłów DOM 230
Modyfikacja stylu 231
Zabawa formularzami 232
Tworzenie nowych węzłów 233
Metoda w pełni zgodna z DOM 234
cloneNode() 235
insertBefore() 236
Usuwanie węzłów 236

9
Spis treści

Obiekty DOM istniejące tylko w HTML 238


Starsze sposoby dostępu do dokumentu 239
document.write() 240
Pola cookies, title, referrer i domain 240
Zdarzenia 242
Kod obsługi zdarzeń wpleciony w atrybuty HTML 242
Pola elementów 242
Obserwatorzy zdarzeń DOM 243
Przechwytywanie i bąbelkowanie 244
Zatrzymanie propagacji 246
Anulowanie zachowania domyślnego 248
Obsługa zdarzeń w różnych przeglądarkach 248
Typy zdarzeń 249
XMLHttpRequest 250
Wysłanie żądania 251
Przetworzenie odpowiedzi 252
Tworzenie obiektów XHR w IE w wersjach starszych niż 7 253
A jak asynchroniczny 254
X jak XML 254
Przykład 254
Podsumowanie 257
Ćwiczenia 258

Rozdział 8. Wzorce kodowania i wzorce projektowe 261


Wzorce kodowania 262
Izolowanie zachowania 262
Warstwa treści 262
Warstwa prezentacji 263
Zachowanie 263
Przykład wydzielenia warstwy zachowania 263
Przestrzenie nazw 264
Obiekt w roli przestrzeni nazw 264
Konstruktory w przestrzeniach nazw 265
Metoda namespace() 266
Rozgałęzianie kodu w czasie inicjalizacji 267
Leniwe definicje 268
Obiekt konfiguracyjny 269
Prywatne pola i metody 270
Metody uprzywilejowane 271
Funkcje prywatne w roli metod publicznych 272
Funkcje samowywołujące się 273
Łańcuchowanie 273
JSON 274
Wzorce projektowe 275
Singleton 276
Singleton 2 276
Zmienna globalna 277
Pole konstruktora 277
Pole prywatne 278

10
Spis treści

Fabryka 278
Dekorator 280
Dekorowanie choinki 280
Obserwator 282
Podsumowanie 285

Dodatek A Słowa zarezerwowane 287


Lista słów zarezerwowanych mających specjalne znaczenie w języku JavaScript 287
Lista słów zarezerwowanych na użytek przyszłych implementacji 288

Dodatek B Funkcje wbudowane 291

Dodatek C Obiekty wbudowane 295


Object 295
Składowe konstruktora Object 296
Składowe obiektów tworzonych przez konstruktor Object 296
Array 298
Składowe obiektów Array 298
Function 301
Składowe obiektów Function 301
Boolean 302
Number 302
Składowe konstruktora Number 303
Składowe obiektów Number 304
String 304
Składowe konstruktora String 305
Składowe obiektów String 305
Date 308
Składowe konstruktora Date 308
Składowe obiektów Date 309
Math 311
Składowe obiektu Math 312
RegExp 313
Składowe obiektów RegExp 314
Obiekty Error 315
Składowe obiektów Error 315

Dodatek D Wyrażenia regularne 317

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.

Był założycielem i dyrektorem generalnym firmy Electric Communities/Communities.com.


Założył firmę State Software, a pracując w niej, odkrył standard wymiany danych JSON. Teraz
jest architektem w Yahoo!.

Gamaiel Zavala to specjalista ds. interfejsów użytkownika w firmie Yahoo! w Santa Monica
w Kalifornii. Lubi pisać kod różnego rodzaju i zawsze stara się zrozumieć aplikacje na wszystkich
poziomach, od pakietów i protokołów, przez technologie obsługujące żądania, aż do interfejsu
użytkownika. Oprócz komputerów pochłania go jeszcze życie rodzinne z ukochaną żoną
i małym synkiem.

Jayme Cousins zaczął tworzyć komercyjne strony internetowe zaraz po ukończeniu geografii
na uniwersytecie. Wśród jego projektów można wymienić promocję niszowego oprogramowania
do analizy przestrzennej, nocne przygotowywanie internetowej wersji głównej gazety w jego
mieście, drukowanie nazw ulic na mapach, malowanie domów, a także wykładanie technologii
na uniwersyteckich kursach dla dorosłych. Obecnie mieszka przy klawiaturze w miejscowości
JavaScript. Programowanie obiektowe

London w Kanadzie wraz z żoną Heather i synkiem Alanem. Jayme wcześniej recenzował
książkę Learning Mambo wydaną przez Packt. Lubi znajdować zastosowania technologii służące
zwykłym ludziom i często ma wrażenie, że jego najważniejszym zadaniem jest tłumaczenie
z technomowy na język ludzki.

Obecnie Jayme prowadzi firmę In House Logic (www.inhouselogic.com), która oferuje two-
rzenie stron internetowych, doradztwo oraz szkolenie techniczne.

Julie London jest inżynierem oprogramowania z ponad ośmioletnim doświadczeniem w bu-


dowaniu firmowych aplikacji sieciowych. Przez wiele lat programowała we Flashu, teraz jed-
nak koncentruje się na innych technologiach klienckich, takich jak CSS, JavaScript i XSL.
Mieszka w Los Angeles i pracuje jako inżynier ds. interfejsów użytkownika w Yahoo!.

Nicholas C. Zakas to główny inżynier ds. interfejsów użytkownika w Yahoo! (w tym przede
wszystkim strony głównej), współtwórca biblioteki YUI oraz nauczyciel JavaScriptu w Yahoo!.
Jest autorem dwóch książek: JavaScript dla webmasterów. Zaawansowane programowanie
oraz Ajax. Zaawansowane programowanie (obie wydane przez Helion), a także wielu artykułów na
temat JavaScriptu dostępnych w sieci.

Nicholas początkowo pracował jako webmaster w małej firmie programistycznej, później zajął
się interfejsami użytkownika, by wreszcie całkowicie poświęcić się inżynierii 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.

Tenni Theurer dołączyła do Yahoo!, a konkretnie do grupy Exceptional Performance, na początku


roku 2006. Następnie objęła rządy jako kierownik i przygotowała zespół projektowy do jego
przewodniej roli w przyspieszaniu produktów Yahoo! dla wygody użytkowników na całym świecie.
W tej chwili zarządza wynikami pracy zajmującej się wyszukiwaniem grupy Search Distribution.
Tenni występowała na wielu konferencjach, w tym na Web 2.0 Expo, Ajax Experience, Rich Web
Experience, AJAXWorld, BlogHer, WITI i CSDN-DrDobbs. Gościnnie udziela się na blo-
gach Yahoo! Developer Network oraz Yahoo! User Interface Blog. Wcześniej pracowała w grupie
Pervasive Computing („przetwarzanie bez granic”) w IBM, gdzie zajmowała się technologiami
mobilnymi i bezpośrednio współpracowała z dużymi klientami podczas wdrożeń na szeroką skalę.

Wayne Shea to inżynier oprogramowania w Yahoo!. Wśród jego projektów można wymienić
badania nad poprawieniem wydajności sieci w urządzeniach mobilnych oraz tworzenie 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

Ta książka przedstawia JavaScript w jego prawdziwej postaci ekspresywnego, elastycznego


prototypowego obiektowego języka programowania. Po okresie odrzucenia, gdy JavaScript
traktowany był li tylko jako zabawka pozwalająca tworzyć bajeranckie przyciski zmieniające
kolor po najechaniu na nie kursorem, ten ciekawy język zasłużenie wraca do łask i jest silniej-
szy niż kiedykolwiek przedtem. Dzisiejszy świat Web 2.0, pełny technologii AJAX, dużych
aplikacji klienckich, aplikacji internetowych przypominających programy desktopowe, inte-
raktywnych map i klientów pocztowych, istnieje w dużej mierze dzięki JavaScriptowi. Jeśli
jeszcze nie znasz tego języka, teraz jest dobry moment na to, by wreszcie się go nauczyć.

Do zrozumienia tej książki nie jest Ci potrzebna żadna wcześniejsza wiedza programistyczna
— zaczniemy od zera i bez nerwów przejdziemy do bardziej zaawansowanych zagadnień.

Co znajdziesz w tej książce?


Rozdział 1. przedstawia historię, teraźniejszość oraz przyszłość JavaScriptu, a także najważ-
niejsze pojęcia związane z programowaniem obiektowym. Pokazuję także w nim, jak skonfi-
gurować środowisko Firebug, które umożliwi Ci samodzielne eksperymentowanie z kodem
w oparciu o przykłady z książki.

Rozdział 2. omawia podstawy języka: zmienne, typy danych, tablice, pętle i instrukcje 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ł 5. poświęcony jest prototypom.

Rozdział 6. ma poszerzyć Twoje javascriptowe horyzonty — przedstawiam w nim wiele róż-


nych sposobów na realizację dziedziczenia.

Rozdział 7. jest poświęcony przeglądarce. Omawiam w nim BOM (Browser Object Model,
obiektowy model przeglądarki), DOM (wprowadzony przez organizację W3C Document Object
Model, czyli obiektowy model dokumentu), zdarzenia przeglądarki oraz AJAX.

Rozdział 8. to omówienie wzorców projektowych, zarówno tych charakterystycznych dla


JavaScriptu, jak i tych niezależnych od języka, przeniesionych do JavaScriptu z Księgi Czterech1,
najważniejszej pracy poświęconej programistycznym wzorcom projektowym.

Dodatek A zawiera listę słów zarezerwowanych języka.

Dodatek B to wzbogacony przykładami przewodnik po funkcjach wbudowanych.

Dodatek C jest bardzo szczegółową ściągą z wszystkich metod i pól wszystkich obiektów
wbudowanych.

Dodatek D opisuje wyrażenia regularne.

Konwencje
Fragmenty tekstu przedstawiające informacje różnego typu zostały sformatowane w 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ść”.

Bloki kodu formatowane są tak:


var ksiazka = {
tytul: 'Paragraf 22',
wydana: 1961,
autor: {

1
Chodzi o książkę Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku,
autorstwa Ericha Gammy, Richarda Helma, Ralpha Johnsona i Johna Vlissidesa, wydaną przez
Wydawnictwa Naukowo-Techniczne — przyp. tłum.

20
Przedmowa

imie: 'Joseph',
nazwisko: 'Heller'
}
};

Jeśli pewien fragment kodu będzie miał większe znaczenie niż pozostałe, zostanie wytłuszczony:
function TwoDShape(){}
// obsługa dziedziczenia
TwoDShape.prototype = Shape.prototype;
TwoDShape.prototype.constructor = TwoDShape;

Nowe pojęcia i ważne słowa także zostaną wytłuszczone. Słowa widoczne na ekranie, będące
częścią menu lub wyświetlane w okienkach dialogowych, będą zapisane następująco: „klik-
nięcie przycisku Dalej spowoduje przejście do następnego slajdu”.

Dobre rady
Dobre rady będą wyglądały tak.

A tak będą wyglądały wskazówki, ostrzeżenia i uwagi.

21
JavaScript. Programowanie obiektowe

22
1

Wprowadzenie

Co łączy Yahoo! Maps, Google Maps, Yahoo! Mail, My Yahoo!, Gmail, Digg, YouTube oraz
szereg innych popularnych aplikacji „Web 2.0”? Wszystkie wymienione strony posiadają 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.

Najczęściej spotykanym środowiskiem działania aplikacji napisanych w języku JavaScript są


przeglądarki internetowe, jednak możliwości jest więcej. Przy pomocy JavaScriptu można
tworzyć różnego rodzaju widżety, dodatki i rozszerzenia. Nauka tego języka naprawdę się opłaca,
ponieważ umożliwia on rozwijanie bardzo różnych aplikacji.

Ta książka przedstawia język JavaScript ze szczególnym uwzględnieniem jego obiektowej natury.


Zaczynamy od zera — do jej zrozumienia nie jest Ci potrzebna żadna wcześniejsza wiedza pro-
gramistyczna. Jeden z rozdziałów jest poświęcony środowisku przeglądarki, jednak pozostała
część książki zawiera informacje, które stosują się do wszystkich środowisk.

Przejdźmy zatem do rozdziału 1., który krótko przedstawia historię języka JavaScript. 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.

Możliwość zmieniania uznawanych wcześniej za statyczne elementów stron internetowych


została bardzo ciepło przyjęta, w wyniku czego inne przeglądarki zostały dostosowane do obsługi
JavaScriptu. Internet Explorer (IE) firmy Microsoft w wersji 3.0 został wzbogacony o język
JScript, który był kopią JavaScriptu rozszerzoną o kilka funkcjonalności przeznaczonych tylko
dla IE. W wyniku coraz większych różnic pomiędzy przeglądarkami podjęto próbę ustandaryzo-
wania różnych implementacji języka. Europejskie Stowarzyszenie Producentów Komputerów1
(ECMA) stworzyło specyfikację ECMAScript. Obecnie obowiązuje standard ECMA-262. Java-
Script jest jego najpopularniejszą implementacją.

Nagły wzrost popularności JavaScriptu miał miejsce w czasie Pierwszej Wojny Przeglądarkowej
(1996 – 2001). Był to okres tak zwanej bańki internetowej. O udział w rynku walczyli dwaj główni
producenci przeglądarek: Netscape i Microsoft. Obie firmy starały się skusić klienta za 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.

Niespójności pomiędzy przeglądarkami irytowały programistów, jednak była to tylko część


problemu. Drugą częścią byli sami autorzy stron, którzy upychali w witrynach zbyt wiele
zbędnych funkcjonalności. Chętnie korzystali z wszystkich nowych możliwości dostarczanych
przez przeglądarkę, przez co strony były „ulepszane” o kwiatki takie jak animacje na pasku
stanu, jaskrawe kolory, migające napisy, trzęsące się okna przeglądarek, płatki śniegu, obiekty
podążające za kursorem itp., co często utrudniało korzystanie ze stron. Tego typu nadużycia
są drugim powodem złej reputacji języka JavaScript. Między innymi przez nie „prawdziwi”
programiści (programiści uznanych języków, takich jak Java czy C/C++) uznali JavaScript za
niewiele więcej niż zabawkę przeznaczoną dla projektantów interfejsów.

1
Obecnie Europejskie Stowarzyszenie na rzecz Standaryzacji Systemów Informacyjnych i Komunika-
cyjnych — przyp. tłum.

24
Rozdział 1. • Wprowadzenie

Wyrazy sprzeciwu wobec JavaScriptu doprowadziły do sytuacji, w której w niektórych pro-


jektach sieciowych zabronione zostało jakiekolwiek programowanie po stronie klienta —
wszystkie funkcjonalności miał obsługiwać jedynie przewidywalny i wiarygodny serwer. Rze-
czywiście, jaki sens miałoby podwajanie czasu wytwarzania aplikacji i spędzanie całości dodat-
kowego czasu na rozwiązywaniu problemów związanych z różnym działaniem kodu w róż-
nych przeglądarkach?

Zapowiedź zmian
Wszystko zmieniło się po zakończeniu Pierwszej Wojny Przeglądarkowej. Zmiany (na lepsze)
w sposobie wytwarzania aplikacji sieciowych zostały zapoczątkowane przez kilka procesów:
Q Microsoft wygrał wojnę i na okres około pięciu lat (co w czasie internetowym odpowiada
wieczności) firma wstrzymała się od dodawania nowych funkcjonalności do przeglądarki
Internet Explorer oraz do JavaScriptu. Dzięki temu programiści innych przeglądarek
zyskali czas na dogonienie lub nawet przewyższenie możliwości IE.
Q Ruch na rzecz standardów sieciowych zyskał przychylność zarówno programistów,
jak i producentów przeglądarek. To oczywiste, że programiści nie chcieli kodować
wszystkich funkcjonalności dwa (lub więcej) razy na wypadek, gdyby coś nie działało
w którejś z przeglądarek, a właśnie przed tym chronią ich uzgodnione standardy.
Co prawda nadal nie istnieje środowisko, które spełniałoby wszystkie możliwe
standardy, jednak można mieć nadzieję, że pojawi się ono w przyszłości.
Q Technologie i sposoby programowania osiągnęły bardzo dojrzały poziom, na którym
można już zajmować się zagadnieniami takimi jak użyteczność, dostępność czy
progresywne ulepszanie.

Dzięki nowym, zdrowszym metodologiom programiści zaczęli uczyć się lepszych sposobów
korzystania z narzędzi, które były dostępne od dość dawna. Po wydaniu aplikacji takich jak
Gmail czy Google Maps, które intensywnie wykorzystują programowanie po stronie klienta,
jasne stało się, że JavaScript to dojrzały, jedyny w swoim rodzaju i potężny prototypowy język
obiektowy. Najlepszym przykładem jego ponownego odkrycia jest szeroka akceptacja 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.

Przyjrzyjmy się każdemu z nich z osobna.

Obiekty
Jak sama nazwa wskazuje, w programowaniu obiektowym zasadniczą rolę odgrywają obiekty. Obiekt
w sposób programistyczny reprezentuje byt (osobę lub rzecz). Może reprezentować dowolny byt:
coś ze świata fizycznego lub bardziej abstrakcyjny koncept. Przyglądając się zwykłemu obiektowi
(na przykład kotu), widzimy, że posiada on pewne cechy charakterystyczne (kolor, imię, masa cia-
ła) oraz że może wykonać określone czynności (miauczeć, spać, chować się, uciekać). W pro-
gramowaniu obiektowym cechy obiektu nazywamy jego polami2, a jego czynności — metodami.

Istnieje pewna analogia pomiędzy terminologią programowania obiektowego a językiem mó-


wionym:
Q Obiekty najczęściej nazywa się przy użyciu rzeczowników (książka, osoba).
Q Metody to czasowniki.
Q Wartości pól to przymiotniki.

Rozważmy zdanie „Czarny kot śpi na mojej głowie”. „Kot” (rzeczownik) będzie obiektem, „czarny”
(przymiotnik) to wartość pola o nazwie kolor, a „śpi” to czynność, czyli metoda obiektu. Na
potrzeby objaśniania analogii możemy uznać, że „na mojej głowie” precyzuje sposób spania,
zatem przekażemy go jako parametr metodzie śpij.

2
Zamiast „pól” mogą pojawić się „dane”, „właściwości”, „atrybuty” — przyp. tłum.

27
JavaScript. Programowanie obiektowe

Klasy
W świecie rzeczywistym obiekty można grupować według pewnych kryteriów. Drozd i orzeł
to ptaki, zatem można powiedzieć, że należą do klasy Ptak. W programowaniu obiektowym
klasa to pewien szablon lub przepis na obiekt. Inna nazwa obiektu to „instancja” — mówimy,
że orzeł jest instancją klasy Ptak. Można stworzyć różne obiekty przy użyciu tej samej klasy,
ponieważ klasa jest tylko wzorem. Obiekty to konkretne instancje oparte o ten wzór.

Istnieje różnica pomiędzy JavaScriptem a „klasycznymi” językami obiektowymi, takimi jak


C++ lub Java. Należy od razu uświadomić sobie, że w języku JavaScript nie ma klas — wszystko
jest oparte na obiektach. Istnieją prototypy, które także są obiektami (szczegółowo omówię je
trochę później). W klasycznym języku obiektowym wydajemy polecenie „utwórz mi nowy
obiekt o nazwie Robert, który jest instancją klasy Osoba”. W prototypowym języku obiektowym
mówi się „Wezmę istniejący już obiekt Osoba i użyję go ponownie jako prototypu nowego
obiektu, który nazwę Robert”.

Kapsułkowanie
Kapsułkowanie to kolejna koncepcja związana z programowaniem obiektowym. Chodzi o to,
że obiekt zawiera w sobie dwie rzeczy:
Q dane (przechowywane w postaci pól);
Q sposoby działania na danych (metody).

Z pojęciem kapsułkowania wiąże się termin hermetyzacji (inaczej ukrywania informacji).


Może on oznaczać różne rzeczy, jednak w programowaniu obiektowym ma konkretne znaczenie.

Wyobraźmy sobie pewien obiekt, na przykład odtwarzacz MP3. Użytkownik ma do dyspozycji


interfejs: przyciski, wyświetlacz itp. Interfejs można wykorzystać w celu sprawienia, by obiekt
zrobił coś przydatnego, na przykład odtworzył piosenkę. Użytkownik nie wie, co dokładnie
dzieje się wewnątrz odtwarzacza. Co więcej, z reguły zupełnie go to nie obchodzi. Innymi słowy,
implementacja interfejsu jest ukryta przed użytkownikiem. Ten sam mechanizm jest używany
w programowaniu obiektowym, gdy obiekt jest wykorzystywany poprzez swoje metody. Nie ma
znaczenia, czy programista sam napisał kod, czy pochodzi on z zewnętrznej biblioteki. Programi-
sta nie musi wiedzieć, jak dokładnie działa metoda. W językach kompilowanych często w ogóle
nie można odczytać kodu, który sprawia, że obiekt działa tak, jak powinien. JavaScript jest
językiem interpretowanym, co sprawia, że kod jest widoczny, jednak idea jest ta sama — pro-
gramista pracuje z interfejsem obiektu, bez zawracania sobie głowy implementacją.

Kolejnym zagadnieniem związanym z hermetyzacją jest widoczność metod i pól. W niektó-


rych językach metody i pola mogą zostać opisane jako publiczne, prywatne lub chronione
(ang. public, private, protected). Ta kategoryzacja określa poziom dostępu użytkownika do po-
szczególnych części obiektu. Dla przykładu pola i metody prywatne są dostępne jedynie dla

28
Rozdział 1. • Wprowadzenie

wewnętrznej implementacji obiektu, podczas gdy do pól publicznych dostęp może uzyskać
każdy. W języku JavaScript wszystkie metody i pola są publiczne, jednak przedstawię sposoby
ochrony danych wewnątrz obiektu w celu zapewnienia prywatności.

Agregacja
Łączenie kilku obiektów w jeden nazywa się agregacją lub kompozycją. Agregacja pozwala
na podział problemu na mniejsze części, którymi łatwiej jest zarządzać („dziel i zwyciężaj”).
Jeśli problem jest tak złożony, że nie da się szczegółowo ogarnąć jego całości, można podzielić
go na mniejsze fragmenty, które, jeśli to konieczne, można dalej dzielić. W ten sposób można
myśleć o danym problemie na kilku różnych poziomach abstrakcji. Komputer to bardzo zło-
żony obiekt. Nie sposób wyobrazić sobie wszystkich czynności, które muszą zostać wykonane
podczas jego uruchamiania. Jednak można podzielić problem, mówiąc, że należy uruchomić
wszystkie obiekty, z których składa się komputer: monitor, mysz, klawiaturę itd. Następnie można
zagłębić się w konstrukcję każdego z tych obiektów. W ten sposób tworzy się obiekty składające
się z części, które mogą zostać ponownie wykorzystane.

Inna analogia: obiekt książka mógłby posiadać (agregować) jeden lub więcej obiektów autor,
obiekt wydawca, kilka obiektów rozdział, indeks itd.

Dziedziczenie
Dziedziczenie to bardzo elegancki sposób korzystania z już istniejącego kodu. Przykładowo
wyobraźmy sobie, że istnieje ogólny obiekt Osoba, posiadający pola takie jak nazwisko czy data
urodzenia, który posiada zaimplementowane funkcjonalności, takie jak chodzenie, mówienie
czy spanie. Nagle okazuje się, że jest nam potrzebny obiekt Programista. Oczywiście można
od nowa zaimplementować wszystkie pola i metody, które posiada Osoba. Rozsądniej jednak
powiedzieć, że Programista dziedziczy po Osobie, i oszczędzić sobie trochę pracy. W obiekcie
Programista konieczne będzie jedynie zaimplementowanie specjalistycznych funkcjonalności,
takich jak „pisz kod”, natomiast za podstawowe czynności będzie odpowiadał kod klasy Osoba.

W klasycznym programowaniu obiektowym klasy dziedziczą z innych klas. Ponieważ jednak


JavaScript nie posiada klas, obiekty dziedziczą z innych obiektów.

Kiedy obiekt dziedziczy z innego obiektu, najczęściej dodaje on nowe metody do już istnieją-
cych, tym samym rozszerzając stary obiekt. Zamiennie można stosować następujące dwa zdania:
„B dziedziczy z A” i „B rozszerza A”. Obiekt dziedziczący może także zmienić definicję 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.

Na wszelki wypadek powtórzmy poznane terminy.

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/.

Istnieje wiele ciekawych rozszerzeń do Firefoksa (wszystkie napisane w języku JavaScript!).


Jednym z nich jest Firebug — niezastąpione narzędzie do programowania stron, które posia-
da szereg przydatnych opcji. Można go pobrać ze strony http://www.getfirebug.com/. Po in-
stalacji należy uruchomić Firefoksa i przejść na dowolną stronę, a następnie wcisnąć F12 (pod
Windows) albo kliknąć małą ikonkę owada w prawym dolnym rogu okna przeglądarki. W ten
sposób otwiera się najciekawszą dla nas część narzędzia Firebug — konsolę.

31
JavaScript. Programowanie obiektowe

Korzystanie z konsoli Firebug

Kod można wpisywać bezpośrednio w konsoli Firebug — zostanie on wykonany po wciśnię-


ciu Enter. Wartość zwracana przez kod jest wypisywana na konsoli. Kod jest wykonywany
w kontekście aktualnie załadowanej strony. By to sprawdzić, wpisz document.location.href —
zwrócony zostanie adres URL bieżącej strony.

Konsola została wyposażona w funkcję automatycznego uzupełniania, podobną do tej w linii


komend systemu operacyjnego. Przykładowo jeśli wpiszesz docu i wciśniesz Tab, docu zosta-
nie uzupełnione do postaci document. Jeśli następnie dopiszesz . (operator kropki), kolejne naci-
śnięcia Tab będą powodowały iterowanie po wszystkich dostępnych polach i metodach obiektu
document.

Przy pomocy klawiszy góra ( ) i dół ( ) można przewijać listę już wykonanych poleceń w celu
wykonania któregoś z nich.

W konsoli mamy do dyspozycji tylko jedną linię, ale można wydać więcej poleceń, jeśli 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

Q Na stronie YUI Theater (http://developer.yahoo.com/yui/theater/) umieszczono


kilka bardzo wartościowych wykładów (w języku angielskim) Douglasa Crockforda.
Część pierwsza, „Theory of the DOM”, poświęcona jest historii przeglądarek, a część
druga, „The JavaScript Programming Language”, przedstawia między innymi historię
JavaScriptu.
Q Najważniejsze terminy związane z programowaniem obiektowym przedstawiono
w odpowiednim artykule w Wikipedii: http://en.wikipedia.org/wiki/Object-oriented_
programming. Wersja polska (http://pl.wikipedia.org/wiki/Programowanie_
obiektowe) jest w tej chwili uboższa od angielskiej. Warto także zapoznać się
z dokumentacją języka Java na stronie firmy Sun
(http://java.sun.com/docs/books/tutorial/java/concepts/index.html), należy jednak
pamiętać, że mowa tam o językach obiektowych korzystających z klas.
Q Możliwości dzisiejszego JavaScriptu są dobrze widoczne na stronie z widżetami
Yahoo! (http://widgets.yahoo.com/), na stronie Google Maps (http://maps.google.com/)
oraz w wersji języka graficznego Processing przetłumaczonego na JavaScript
(http://ejohn.org/blog/processingjs/).

34
2

Proste typy danych,


tablice, pętle i warunki

Zanim przejdziemy do obiektowych funkcjonalności JavaScriptu, przyjrzyjmy się jego pod-


stawom. Ten rozdział przedstawia:
Q proste typy danych, takie jak łańcuchy znaków i liczby;
Q tablice;
Q popularne operatory, takie jak +, -, delete i typeof;
Q polecenia sterujące, takie jak pętle oraz warunki if…else.

Zmienne
Zmienne służą do przechowywania danych. Podczas pisania programów wygodniej jest korzystać
ze zmiennych niż z samych danych, jako że łatwiej jest napisać pi niż 3.141592653589793,
zwłaszcza jeśli dane te są potrzebne w więcej niż jednym miejscu programu. Dane 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.

By móc korzystać ze zmiennej, należy wykonać dwa kroki:


Q zadeklarować zmienną,
Q zainicjalizować ją, czyli nadać jej wartość.
JavaScript. Programowanie obiektowe

Do deklaracji zmiennych służy słówko var. Na przykład:


var a;
var toJestZmienna;
var _i_to_tez;
var mix12trzy;

Nazwy zmiennych mogą składać się z dowolnej kombinacji liter, cyfr oraz znaku podkreślnika (_).
Nie można jednak zacząć od cyfry, dlatego nie jest poprawna poniższa deklaracja:
var 2trzy4piec;

Inicjalizacja zmiennej to nadanie jej wartości po raz pierwszy. Można zrobić to na dwa sposoby:
Q najpierw zadeklarować zmienną, a potem ją zainicjalizować;
Q zadeklarować i zainicjalizować zmienną za pomocą jednego polecenia.

Oto przykład zastosowania drugiego podejścia:


var a = 1;

Zmienna o nazwie a ma teraz wartość 1.

Można zadeklarować (i, jeśli ktoś chce, zainicjalizować) kilka zmiennych za pomocą jednego
słówka var. Wystarczy tylko rozdzielić deklaracje za pomocą przecinków:
var v1, v2, v3 = 'halo', v4 = 4, v5;

Wielkość liter ma znaczenie


W nazwach zmiennych rozróżniane są wielkie i małe litery. Można to sprawdzić przy użyciu
konsoli Firebug. Wpisz w konsoli poniższy kod, wciskając Enter na końcu każdej linii:
var wielkosc_liter_ma_znaczenie = 'male';
var WIELKOSC_LITER_MA_ZNACZENIE = 'wielkie';
wielkosc_liter_ma_znaczenie
WIELKOSC_LITER_MA_ZNACZENIE

Możesz przyspieszyć wpisywanie kodu, w trzeciej linii wpisując jedynie wie i wtedy 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

>>> var wielkosc_liter_ma_znaczenie = 'male';


>>> var WIELKOSC_LITER_MA_ZNACZENIE = 'wielkie';
>>> wielkosc_liter_ma_znaczenie

"male"
>>> WIELKOSC_LITER_MA_ZNACZENIE

"wielkie"

W liniach zaczynających się trzema znakami większości (>>>) widać kod wpisany przez 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

Q Wartości wejściowe (inaczej argumenty) to 1 i 2.


Q Wynikiem jest liczba 3.

Zamiast wartości 1 i 2 można użyć zmiennych. Można również użyć zmiennej do przechowania
wyniku operacji, co pokazuje następny przykład:
>>> var a = 1;
>>> var b = 2;
>>> a + 1;
2
>>> b + 2;
4
>>> a + b
3
>>> var c = a + b;
>>> c
3

Poniższa tabela zawiera podstawowe operatory arytmetyczne:

Symbol operatora Operacja Przykład


+ Dodawanie >>> 1 + 2
3
- Odejmowanie >>> 99.99 – 11
88.99
* Mnożenie >>> 2 * 3
6
/ Dzielenie >>> 6 / 4
1.5
% Modulo, czyli reszta >>> 6 % 3
z dzielenia 0
>>> 5 % 3
2
Czasami potrzebna jest możliwość sprawdzenia, czy liczba jest
parzysta, czy nieparzysta. Operator % bardzo to ułatwia. Liczby
nieparzyste dzielone przez 2 zwrócą wartość 1, zaś liczby
parzyste zwrócą 0.
>>> 4 % 2
0
>>> 5 % 2
1

38
Rozdział 2. • Proste typy danych, tablice, pętle i warunki

Symbol operatora Operacja Przykład


++ Zwiększenie Inkrementację dzielimy na postinkrementację i preinkrementację.
(inkrementacja) O postinkrementacji mówimy, gdy wartość wejściowa jest
wartości o 1 zwiększana po jej zwróceniu.
>>> var a = 123;var b = a++;
>>> b
123
>>> 1
124
Preinkrementacja ma miejsce, gdy wartość najpierw jest
zwiększana, a potem zwracana.
>>> var a = 123;var b = ++a;
>>> b
124
>>> 1
124
-- Zmniejszenie Postdekrementacja
(dekrementacja) >>> var a = 123;var b = a--;
wartości o 1 >>> b
122
>>> 1
124
Predekrementacja
>>> var a = 123;var b = --a;
>>> b
122
>>> 1
122

Proste przypisanie wartości, na przykład var a = 1, również jest operacją. Znak = to operator
prostego przypisania.

Istnieje rodzina operatorów, które są połączeniem operatorów przypisania i działania arytme-


tycznego. Są to tak zwane operatory złożone. Pozwalają one zmniejszyć ilość kodu programu.
Poniżej przedstawiam kilka przykładów.
>>> var a = 5
>>> a += 3;

a += 3; to skrócona wersja wyrażenia a = a + 3;

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.

Proste typy danych


Każda wartość, jakiej można użyć w kodzie, jest pewnego typu. W języku JavaScript istnieją
następujące proste typy danych:
1. Liczba — może to być liczba zmiennoprzecinkowa lub całkowita, na przykład 1,
100, 3.14.
2. Łańcuch znaków (string) — ciąg znaków dowolnej długości, na przykład "a",
"jeden", "pięć lub sześć".
3. Boolean (wartość boolowska) — może przyjmować wartości true (prawda)
lub false (fałsz).
4. Niezdefiniowany — jeśli spróbujesz pobrać wartość zmiennej, która nie istnieje,
otrzymasz specjalną wartość undefined. To samo stanie się podczas próby odczytu
zmiennej, która została zadeklarowana, ale jeszcze nie otrzymała wartości.
JavaScript nada jej wówczas wartość undefined.
5. Null — jest to kolejny specjalny typ danych. Obejmuje on tylko jedną wartość,
null, która oznacza brak wartości, wartość pustą, nic. Różnica pomiędzy null
a undefined jest taka, że zmienna o wartości null jest uważana za zdefiniowaną,
tyle że jej wartością jest nic. Wkrótce pokażę to na przykładach.

Każda zmienna, która nie należy do żadnego z wymienionych pięciu typów prostych, jest 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

Q być typu prostego (pięć typów danych opisanych powyżej), albo


Q być obiektem.

Ustalanie typu danych — operator typeof


Jeśli nie wiemy, jakiego typu jest zmienna lub wartość, możemy użyć operatora typeof. Ope-
rator ten zwraca tekst reprezentujący typ danych. Zwracaną wartością może być "number"
(liczba), "string", "boolean", "undefined", "object" lub "function". W kolejnych podrozdziałach
pokażę działanie typeof na przykładzie wszystkich pięciu prostych typów danych.

Liczby
Najprostszym przykładem liczby jest liczba całkowita. Jeśli przypiszesz zmiennej wartość 1
i użyjesz operatora typeof, zwrócony zostanie łańcuch znaków "number". W poniższym przykł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"

Istnieją jeszcze liczby zmiennoprzecinkowe (ułamki):


>>> var n2 = 1.23;
>>> typeof n2;

"number"

typeof można również wywołać na wartości nieprzypisanej do żadnej zmiennej:


>>> typeof 123;

"number"

Liczby ósemkowe i szesnastkowe


Jeśli liczba zaczyna się cyfrą 0, jest uznawana za liczbę ósemkową. Przykładowo ósemkowa
liczba 0377 odpowiada dziesiętnej liczbie 255.

41
JavaScript. Programowanie obiektowe

>>> var n3 = 0377;


>>> typeof n3;

"number"
>>> n3;

255

W ostatniej linii przykładu wypisana została dziesiętna reprezentacja liczby ósemkowej. Czę-
ściej od liczb ósemkowych używane są liczby szesnastkowe (heksadecymalne), które wykorzy-
stuje się między innymi do definiowania kolorów w arkuszach stylów CSS.

W CSS kolory można definiować na kilka sposobów. Oto dwa z nich:


Q Wykorzystanie wartości dziesiętnych do określenia ilości R (czerwieni), G (zieleni)
oraz B (niebieskiego) za pomocą wartości od 0 do 255. Przykładowo rgb(0, 0, 0)
oznacza kolor czarny, a rgb(255, 0, 0) to czerwony (maksymalna ilość czerwieni
i ani trochę zielonego lub niebieskiego).
Q Wykorzystanie liczb szesnastkowych do określenia ilości danego koloru za pomocą
tylko dwóch znaków. Przykładowo #000000 to czarny, a #ff0000 to czerwony. Jest tak
dlatego, że ff w systemie szesnastkowym odpowiada dziesiętnej liczbie 255.

Liczby szesnastkowe w języku JavaScript zaczynają się od 0x.


>>> var n4 = 0x00;
>>> typeof n4;

"number"
>>> n4;

0
>>> var n5 = 0xff;
>>> typeof n5;

"number"
>>> n5;

255

Wykładniki potęg
1e1 (co można zapisać również jako 1e+1, 1E1 lub 1E+1) odpowiada liczbie jeden z jednym 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

Wynikiem dzielenia przez zero jest nieskończoność.


>>> var a = 6 / 0;
>>> a

Infinity

Nieskończoność oznacza największą liczbę (a raczej liczbę nieco większą od największej), a co


z liczbą najmniejszą? Zapisujemy ją jako nieskończoność ze znakiem minus, czyli minus nie-
skończoność.
>>> var i = -Infinity;
>>> i

-Infinity
>>> typeof i

"number"

Czy to oznacza, że korzystamy z czegoś, co ma rozmiar dokładnie dwóch nieskończoności —


od zera do nieskończoności, a potem od zera w dół do minus nieskończoności? No cóż, ma to
jedynie wartość rozrywkową — nie da się wykorzystać tego faktu w żaden praktyczny sposób.
Jeśli dodamy do siebie nieskończoność i minus nieskończoność, nie otrzymamy zera, tylko
wartość NaN (Not a Number, czyli wartość niebędąca liczbą).
>>> Infinity - Infinity

NaN
>>> -Infinity + Infinity

NaN

Każde inne działanie arytmetyczne z nieskończonością jako argumentem zwróci wartość


Infinity:
>>> Infinity - 20

Infinity
>>> -Infinity * 3

-Infinity
>>> Infinity / 2

Infinity
>>> Infinity - 99999999999999999

Infinity

44
Rozdział 2. • Proste typy danych, tablice, pętle i warunki

NaN
Czym zatem jest NaN, które widzieliśmy przed chwilą? Wychodzi na to, że wbrew swojej nazwie
„wartość niebędąca liczbą” jest specjalną wartością, która… także jest liczbą.
>>> typeof NaN
"number"
>>> var a = NaN;
>>> a

NaN

NaN otrzymasz, jeśli spróbujesz wykonać działanie na czymś, co powinno być liczbą, i to dział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"

Oto przykład liczby użytej w kontekście tekstowym:


>>> var s = '1';
>>> typeof s;
"string"

45
JavaScript. Programowanie obiektowe

Jeśli użyjesz pustego cudzysłowu, nadal będzie to łańcuch, tyle że pusty:


>>> var s = ""; typeof s;

"string"

Wcześniej widzieliśmy już przykład użycia operatora + na argumentach będących liczbami —


realizował on dodawanie. Jeśli użyjemy tego samego operatora na łańcuchach znaków, spo-
woduje on wykonanie operacji konkatenacji (czyli sklejenia) łańcuchów.
>>> var s1 = "raz"; var s2 = "dwa"; var s = s1 + s2; s;

"razdwa"
>>> typeof s;

"string"

Podwójne znaczenie operatora + może prowadzić do błędów. Z tego powodu dobrze jest przed
konkatenacją upewnić się, że argumenty są łańcuchami, a przed dodawaniem — że są 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"

Jeśli konwersja się nie powiedzie, wynikiem będzie NaN:


>>> var d = '101 dalmatyńczyków';
>>> d * 1

NaN

Leniwy sposób zamiany dowolnej wartości na łańcuch znaków polega na jej konkatenacji z 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

Podwójne użycie ! przywróci pierwotną wartość:


>>> var b = !!true;
>>> b;

true

Jeśli użyjemy operatora logicznego na wartości, która nie jest typu boolean, zostanie ona za-
mieniona na ten typ.
>>> var b = "jeden";
>>> !b;

false

W powyższym przykładzie łańcuch "jeden" został zamieniony na wartość boolowską true,


a następnie zanegowany — wynikiem negacji true jest false. W kolejnym przykładzie stosu-
jemy podwójny operator negacji, zatem wynikiem będzie true.
>>> var b = "jeden";
>>> !!b;

true

Użycie podwójnej negacji jest prostym sposobem zmiany dowolnej wartości na jej 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

Q pusty łańcuch "" (lub ''),


Q null,
Q undefined,
Q liczba 0,
Q liczba NaN,
Q boolowskie false.

O powyższych wartościach mówi się czasem, że są fałszywe, podczas gdy pozostałe wartości
są prawdziwe (w tym między innymi łańcuchy "0" oraz "false").

Przejdźmy teraz do przykładów pozostałych dwóch operatorów logicznych — operatorów


AND oraz OR. Jeśli użyjemy AND (&&), wynik ma wartość true wtedy i tylko wtedy, gdy
wszystkie argumenty mają wartość true. W przypadku OR (!!) wynik ma wartość true wtedy
i tylko wtedy, gdy przynajmniej jeden z operatorów ma wartość true.
>>> var b1 = true; var b2 = false;
>>> b1 || b2

true
>>> b1 && b2

false

Poniższa tabela zawiera wszystkie możliwe operacje na dwóch argumentach i ich wyniki:

Operacja Wynik
true && true true
true && false false
false && true false
false && false false
true || true true
true || false true
false || true true
false || false false

Można użyć kilku operatorów logicznych w jednym wyrażeniu:


>>> true && true && false && true

false
>>> false || true || false

true

50
Rozdział 2. • Proste typy danych, tablice, pętle i warunki

Można także łączyć operatory && z ||. W takim wypadku należy skorzystać z nawiasów, by
jednoznacznie określić kolejność wykonywania działań. Na przykład:
>>> false && false || true && true

true
>>> false && (false || true) && true

false

Priorytety operatorów
Być może zastanawiasz się, dlaczego pierwsze z powyższych wyrażeń (false && false ||
true && true) zwróciło wartość true. Odpowiedź jest związana z priorytetami operatorów.
Pewnie pamiętasz z lekcji matematyki, że:
>>> 1 + 2 * 3

Jest tak dlatego, że mnożenie ma priorytet wyższy niż dodawanie, zatem najpierw obliczane
jest 2 * 3, co odpowiada zapisowi:
>>> 1 + (2 * 3)

W przypadku operatorów logicznych jest podobnie. Najwyższy priorytet ma operator !. Od-


powiadające mu działanie jest wykonywane najpierw, chyba że wyrażenie zawiera nawiasy, które
każą zrobić co innego. Następny w kolejności jest operator &&, a ostatni ||. Innymi słowy:
>>> false && false || true && true

true
jest równoważne:
>>> (false && false) || (true && true)

true

Dobra rada
Zamiast polegać na priorytetach operatorów, używaj nawiasów. Dzięki temu Twój kod będzie bardziej
czytelny i łatwiejszy do zrozumienia.

51
JavaScript. Programowanie obiektowe

Leniwe wartościowanie
Jeśli wyrażenie zawiera kilka następujących po sobie operatorów logicznych, a wynik staje się
oczywisty przed osiągnięciem końca wyrażenia, pozostałe operacje nie zostaną wykonane,
ponieważ nie mają wpływu na wynik. Na przykład:
>>> true || false || true || false || true

true

Ponieważ wszystkie operatory są operatorami OR i mają ten sam priorytet, wynik ma wartość
true, jeśli co najmniej jeden z argumentów to true. Skoro pierwszy argument ma wartość true,
silnik JavaScript postanawia być leniwy (no dobrze, wydajny) i rezygnuje ze sprawdzania 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.

Symbol operatora Opis Przykład


== Sprawdzenie równości: >>> 1 == 1
zwraca true, jeśli wartości argumentów są sobie true
równe. Przed porównaniem argumenty są >>> 1 == 2
sprowadzane do tego samego typu. false
>>> 1 == '1'
true
=== Sprawdzenie równości oraz zgodności typów: >>> 1 === '1'
zwraca true, jeśli wartości argumentów są sobie false
równe i są tego samego typu. Ten sposób >>> 1 === 1
porównywania jest zasadniczo lepszy true
i bezpieczniejszy, ponieważ nie jest
przeprowadzana żadna niekontrolowana
konwersja typów.
!= Sprawdzenie różności: >>> 1 != 1
zwraca true, jeśli argumenty nie są sobie równe false
(po konwersji typów). >>> 1 != '1'
false
>>> 1
true
!== Sprawdzenie różności bez konwersji typów: >>> 1 !== 1
zwraca true, jeśli argumenty nie są sobie równe false
lub są różnego typu. >>> 1 !== '1'
true
> Zwraca true, jeśli lewy argument jest większy >>> 1 > 1
od prawego. false
>>> 33 > 22
true
>= Zwraca true, jeśli lewy argument jest większy >>> 1 >= 1
od prawego lub argumenty są równe. true

53
JavaScript. Programowanie obiektowe

Symbol operatora Opis Przykład


< Zwraca true, jeśli lewy argument jest mniejszy >>> 1 < 1
od prawego. false
>>> 1 < 2
true
<= Zwraca true, jeśli lewy argument jest mniejszy >>> 1 <= 1
od prawego lub argumenty są równe. true
>>> 1 <= 2
true

Warto zapamiętać, że NaN nie jest równe niczemu innemu, nawet sobie.
>>> NaN == NaN

false

Undefined i null
Wartość undefined otrzymasz, jeśli spróbujesz użyć zmiennej, która nie istnieje lub której nie
została przypisana żadna wartość. Jeśli zadeklarujesz zmienną bez inicjalizacji, JavaScript
automatycznie nada jej wartość undefined.

Próba odwołania się do takiej zmiennej zakończy się błędem:


>>> foo

foo is not defined

Operator typeof użyty na nieistniejącej zmiennej zwróci łańcuch "undefined":


>>> typeof foo

"undefined"

Jeśli zmienna została zadeklarowana, ale nie nadano jej wartości, odwołanie się do niej nie
powoduje błędu, jednak operator typeof nadal zwraca wartość "undefined":
>>> var somevar;
>>> somevar
>>> typeof somevar

"undefined"

Z wartością null jest trochę inaczej. JavaScript sam nie nadaje zmiennym tej wartości — może
to zrobić jedynie programista w swoim kodzie.

54
Rozdział 2. • Proste typy danych, tablice, pętle i warunki

>>> var somevar = null

null
>>> somevar

null
>>> typeof somevar

"object"

Chociaż różnica pomiędzy null i undefined może się wydawać niewielka, są sytuacje, w których
ma kluczowe znaczenie. Możemy na przykład otrzymać odmienne wyniki działań 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

Konwersja na typ boolean:


>>> !!undefined

false
>>> !!null

false

Konwersja na łańcuch znaków:


>>> "" + null

"null"
>>> "" + undefined

"undefined"

55
JavaScript. Programowanie obiektowe

Proste typy danych — podsumowanie


Podsumujmy krótko, co zostało powiedziane do tej pory:
Q Istnieje pięć prostych typów danych:
Q liczba,
Q łańcuch znaków (string),
Q boolean,
undefined (niezdefiniowany),
Q

Q null.

Q Wszystko, co nie jest typu prostego, jest obiektem.


Q Typ liczbowy służy do przechowywania dodatnich lub ujemnych liczb całkowitych
i zmiennoprzecinkowych, liczb szesnastkowych i ósemkowych, liczb zapisanych
za pomocą wykładników potęg oraz specjalnych liczb NaN, Infinity oraz –Infinity.
Q Łańcuchy to ciągi znaków w cudzysłowie lub apostrofach.
Q Możliwe wartości typu boolean to true i false.
Q Typ danych null składa się z tylko jednej wartości: null.
Q Typ danych undefined składa się z tylko jednej wartości: undefined.
Q Podczas konwersji na typ boolean wszystkie wartości zostaną zamienione na true,
z wyjątkiem sześciu wartości fałszywych:
Q " "
Q null
Q undefined
Q 0
Q NaN
Q false

Tablice
Skoro znasz już proste typy danych, pora przejść do nieco ciekawszej struktury danych — tablicy.

By zadeklarować zmienną i jako wartość przypisać jej pustą tablicę, należy użyć pustych 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.

Tablicę zawierającą trzy elementy definiuje się w następujący sposób:


>>> var a = [1,2,3];

Jeśli w konsoli Firebug wpiszemy nazwę tablicy, zostanie wypisana jej zawartość:
>>> a

[1, 2, 3]

Czym zatem jest tablica? Jest po prostu listą wartości. Nie jest konieczne używanie jednej
zmiennej do przechowania jednej wartości — można trzymać dowolnie wiele wartości jako
elementy jednej tablicy. Tylko jak się do nich odwołać? Elementy są indeksowane liczbami,
począwszy od 0. Pierwszy element ma indeks (inaczej pozycję) 0, drugi ma indeks 1, i tak dalej.
Oto trzyelementowa tablica z poprzedniego przykładu:

Indeks Wartość
0 1
1 2
2 3

Aby odwołać się do elementu tablicy, należy podać indeks, otoczony nawiasami kwadratowy-
mi. a[0] oznacza pierwszy element tablicy a, a[1] drugi i tak dalej.
>>> a[0]

1
>>> a[1]

Dodawanie i aktualizacja elementów tablicy


Za pomocą indeksów można nie tylko odczytywać elementy tablicy, ale także je aktualizować
(zmieniać). Poniższy przykład pokazuje zmianę trzeciego elementu tablicy (czyli elementu o
indeksie 2). Następnie tablica jest wypisywana na ekran.
>>> a[2] = 'trzy';

"trzy"
>>> a

[1, 2, "trzy"]

57
JavaScript. Programowanie obiektowe

Można dodać nowe elementy, korzystając z nieistniejącego wcześniej indeksu.


>>> a[3] = 'cztery';

"cztery"
>>> a

[1, 2, "trzy", "cztery"]

Jeśli dodając nowy element tablicy, poda się zbyt wysoki indeks — tak że pomiędzy nowym
elementem a pozostałymi elementami tablicy zostanie przerwa — elementy z „pośrednimi”
indeksami zostaną wypełnione wartością undefined. Na przykład:
>>> a[6] = 'nowy';
"nowy"
>>> a
[1, 2, 3, undefined, undefined, undefined, "nowy"]

Usuwanie elementów
Do usuwania elementów służy operator delete. W praktyce nie usuwa on elementu, tylko
ustawia jego wartość na udefined. W związku z tym po usunięciu elementu długość tablicy nie
ulega zmianie.
>>> var a = [1, 2,
>>> delete a[1];
true
>>> a
[1, undefined, 3]

Tablice tablic
Tablica może zawierać dowolne wartości, w tym inne tablice.
>>> var a = [1, "dwa", false, null, undefined];
>>> a
[1, "dwa", false, null, undefined]
>>> a[5] = [1,2,3]
[1, 2, 3]
>>> a
[1, "dwa", false, null, undefined, [1, 2, 3]]

58
Rozdział 2. • Proste typy danych, tablice, pętle i warunki

Przyjrzyjmy się teraz tablicy posiadającej dwa elementy, z których każdy sam jest tablicą:
>>> var a = [[1,2,3],[4,5,6]];
>>> a

[[1, 2, 3], [4, 5, 6]]

Pierwszym elementem jest a[0], które jest tablicą.


>>> a[0]

[1, 2, 3]

Aby odwołać się do elementów zagnieżdżonej tablicy, należy użyć następnego zestawu nawia-
sów kwadratowych.
>>> a[0][0]

1
>>> a[1][2]

Warto wiedzieć, że za pomocą nawiasów kwadratowych można odwoływać się do poszczegól-


nych znaków wewnątrz łańcucha.
>>> var s = 'raz';
>>> s[0]

"r"
>>> s[1]

"a"
>>> s[2]

"z"

Tablicami można bawić się na wiele innych sposobów (opowiem o nich w rozdziale 4.), jednak
na teraz wystarczy zapamiętać, że:
Q Tablica jest magazynem danych.
Q Tablica zawiera indeksowane elementy.
Q Indeksy zaczynają się od zera i zwiększają się o jeden dla każdego kolejnego
elementu.
Q Do elementów tablicy odwołujemy się za pomocą indeksów otoczonych nawiasami
kwadratowymi.
Q Tablica może zawierać dowolne dane, w tym inne tablice.

59
JavaScript. Programowanie obiektowe

Warunki i pętle
Warunki są prostą, ale użyteczną metodą kontroli przepływu sterowania za pomocą fragmentu
kodu. Pętle umożliwiają powtarzanie pewnych operacji bez konieczności powtarzania kodu.
W tym podrozdziale zajmiemy się:
Q warunkami if,
Q instrukcjami switch,
Q pętlami while, do…while, for oraz for…in.

Bloki kodu
Podczas tworzenia warunków i pętli często będzie pojawiało sie pojęcie bloku kodu, dlatego
od razu wyjaśnijmy sobie, co ono oznacza.

Blok kodu to zero lub więcej wyrażeń otoczonych nawiasami klamrowymi.


{
var a = 1;
var b = 3;
}

Bloki można zagnieżdżać wewnątrz innych bloków, praktycznie w nieskończoność:


{
var a = 1;
var b = 3;
var c, d;
{
c = a + b;
{
d = a - b;
}
}
}

Dobre rady
 Używaj średników na końcu linii. Pomimo tego, że średnik nie jest obowiązkowy, jeśli linia zawiera
tylko jedno wyrażenie, warto wyrobić sobie nawyk ich stosowania. Dla zwiększenia czytelności kodu
wyrażenia wewnątrz bloku powinny być umieszczone w osobnych liniach zakończonych średnikiem.
 Stosuj wcięcia wewnątrz nawiasów klamrowych. Niektórzy stosują wcięcia wielkości znaku tabulacji,
inni wielkości czterech spacji, a jeszcze inni dwóch spacji. Wielkość wcięcia nie ma znaczenia, pod
warunkiem, że będziesz konsekwentny. W powyższym listingu stosuję wcięcia wielkości dwóch 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.

Przejdźmy zatem do pętli i warunków! Uwaga: większość przykładów wymaga przejścia do


trybu wielu linii w konsoli Firebug.

Warunki if
Oto prosty przykład warunku if:
var result = '';
if (a > 2) {
result = 'a jest większe od 2';
}

Części wyrażenia warunkowego if to:


Q instrukcja if („jeżeli”);
Q warunek w nawiasie;
Q blok kodu, który ma zostać wykonany, jeśli warunek jest spełniony.

Warunek (część w nawiasie) zawsze zwraca wartość logiczną (boolean). Może zawierać:
Q operację logiczną: !, &&, ||;
Q porównanie, np. ===, !=, >;
Q każdą wartość lub zmienną, którą można zamienić na boolean;
Q kombinację powyższych.

Wyrażenie warunkowe if może jeszcze zawierać opcjonalną część else. W części else umieszcza
się blok kodu, który ma zostać wykonany, gdy warunek nie zostanie spełniony (będzie miał
wartość false).
if (a > 2) {
result = 'a jest wieksze od 2';
} else {
result = 'a NIE jest wieksze od 2';
}

Pomiędzy if a else może się znaleźć dowolnie wiele warunków if…else. Na przykład:
if (a > 2 || a < -2) {
result = 'a nie jest pomiędzy -2 i 2';
} else if (a === 0 && b === 0) {
result = 'a i b mają wartość 0';

61
JavaScript. Programowanie obiektowe

} else if (a === b) {
result = 'a i b są równe';
} else {
result = 'Poddaję się!';

Warunki można zagnieżdżać, umieszczając nowe warunki wewnątrz bloków.


if (a === 1) {
if (b === 2) {
result = 'a ma wartość 1, zaś b ma wartość 2';
} else {
result = 'a ma wartość 1, ale b nie ma wartości 2';
}
} else {
result = 'a nie ma wartości 1, nie wiem nic na temat b';
}

Sprawdzanie, czy zmienna istnieje


Często przydatna okazuje się możliwość sprawdzenia, czy dana zmienna istnieje. Leniwy spo-
sób polega na umieszczeniu zmiennej jako warunku wyrażenia if, na przykład if(zmienna)
{...}, jednak nie jest to najlepsza metoda. Spójrzmy na przykład, który sprawdza, czy zmien-
na o nazwie somevar istnieje, a jeśli tak, to ustawia wartość zmiennej result na 'tak':
>>> var result = '';
>>> if (somevar){result = 'tak';}
somevar is not defined
>>> result;
""

Kod najwyraźniej działa, ponieważ result nie ma wartości 'tak'. Jednak są problemy. Po
pierwsze, wygenerowane zostało ostrzeżenie: somevar is not defined (zmienna somevar nie
istnieje), a jako spece od JavaScriptu nie chcemy, by nasz kod powodował takie zachowania.
Po drugie, sam fakt, że if(somevar) zwróciło false, wcale nie musi oznaczać, że zmienna nie
została zdefiniowana. Może być tak, że somevar istnieje, ale zawiera jedną z fałszywych warto-
ści, takich jak false albo 0.

Lepiej sprawdzić istnienie zmiennej za pomocą typeof.


>>> if (typeof somevar !== "undefined"){result = 'tak';}
>>> result;
""

Operator typeof zawsze zwraca łańcuch, który można porównać z "undefined". Należy 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

>>> var somevar;


>>> if (typeof somevar !== "undefined"){result = 'tak';}
>>> result;

""
>>> somevar = undefined;
>>> if (typeof somevar !== "undefined"){result = 'tak';}
>>> result;

""

Jeśli zmienna została zdefiniowana i zainicjalizowana wartością inną niż undefined, typem
zwracanym przez typeof nie będzie już "undefined ".
>>> somevar = 123;
>>> if (typeof somevar !== "undefined"){result = 'tak';}
>>> result;

"tak"

Alternatywna składnia if
Jeśli warunek jest bardzo prosty, można skorzystać z alternatywnej składni if. Warunek zawarty
w poniższym fragmencie kodu:
var a = 1;
var result = '';
if (a === 1) {
result = "a ma wartość jeden";
} else {
result = "a nie ma wartości jeden";
}

można zapisać w skrócony sposób:


var result = (a === 1) ? "a ma wartość jeden" : "a nie ma wartości jeden";

Tej składni powinno używać się jedynie w przypadku bardzo prostych warunków. Postaraj się
jej nie nadużywać, ponieważ zmniejsza ona czytelność kodu.
Znak ? nazywamy operatorem trójkowym.

Switch
Jeśli podczas pisania warunków zdasz sobie sprawę, że Twój kod zawiera zbyt wiele części
else…if, należy rozważyć zamianę warunku if na switch.
var a = '1';
var result = '';
switch (a) {

63
JavaScript. Programowanie obiektowe

case 1:
result = 'Liczba 1';
break;
case '1':
result = 'Łańcuch 1';
break;
default:
result = 'Nie wiem';
break;
}
result

Zmiennej result zostanie przypisana wartość 'Łańcuch 1'. Wyrażenie warunkowe switch 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.

Oto procedura wykonania warunku switch, krok po kroku:


1. Oblicz i zapamiętaj wartość wyrażenia w nawiasie po instrukcji switch.
2. Przejdź do pierwszego bloku case, porównaj jego wartość z wartością z kroku 1.
3. Jeśli wynik porównania z kroku drugiego zwraca true, wykonaj kod aktualnego
bloku case.
4. Po wykonaniu bloku case sprawdź, czy blok kończy się instrukcją break. Jeśli tak,
wyjdź z wyrażenia warunkowego.
5. Jeśli nie pojawiło się słowo break lub w kroku drugim zwrócona została wartość
false, przejdź do następnego bloku case. Powtórz kroki od 2. do 5.
6. Jeśli dotarłeś tutaj (wykonanie procedury nie zakończyło się na kroku 4.), wykonaj
kod w części default.

64
Rozdział 2. • Proste typy danych, tablice, pętle i warunki

Dobre rady
 Stosuj wcięcia w liniach zawierających case i głębsze wcięcia w liniach z kodem opisującym bloki.
 Nie zapominaj o stosowaniu break.
 Niekiedy chce się świadomie ominąć break, jednak takie sytuacje zdarzają się bardzo rzadko. Okre-
śla się je mianem spadków. Zawsze należy je dokumentować, ponieważ na pierwszy rzut oka mogą
wyglądać jak przypadkowe pominięcia słówka break. Z drugiej strony, może się zdarzyć, że 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ą.

Powiedzmy, że podróżujesz (a właściwie Twój program podróżuje) z punktu A do punktu B.


W pewnym momencie osiągasz punkt, w którym następuje sprawdzenie warunku C. Od wy-
niku tego sprawdzenia zależy, czy program wejdzie w pętlę L. Po wejściu w pętlę wykonujesz
jedną iterację. Następnie ponownie sprawdzasz warunek, by dowiedzieć się, czy potrzebna
jest kolejna iteracja. Wreszcie możesz udać się do punktu B.

Istnieją pętle nieskończone, w których warunek zawsze jest spełniony, a kod pozostaje w pę-
tli „na zawsze”. Taka sytuacja prawie zawsze jest wynikiem błędu logicznego.

W języku JavaScript istnieją cztery rodzaje pętli:


Q pętla while,
Q pętla do…while,
Q pętla for,
Q pętla for…in.

65
JavaScript. Programowanie obiektowe

Pętla while

Pętla while jest najprostszym typem pętli. Wygląda tak:


var i = 0;
while (i < 10) {
i++;
}

Po instrukcji while następuje para nawiasów z warunkiem oraz blok kodu w nawiasach 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

Poza warunkiem C i blokiem kodu L mamy tu następujące elementy:


Q Inicjalizacja — kod, który jest wykonywany przed wejściem programu w pętlę
(na diagramie oznaczona jako 0).
Q Inkrementacja — kod, który jest wykonywany po każdej iteracji (na diagramie
oznaczona jako ++).

Pętlę for najczęściej stosuje się w następujący sposób:


Q W części inicjalizacyjnej definiuje się zmienną, najczęściej o nazwie i, na przykład
var i = 0;.
Q W części warunkowej porównuje się i z pewną wartością graniczną, na przykład
i < 100.
Q W części inkrementacyjnej zwiększa się i o 1, na przykład i++.

Pełen przykład:
var kara = '';
for (var i = 0; i < 100; i++) {
kara += 'Nigdy więcej tego nie zrobię, ';
}

Wszystkie trzy części (inicjalizacja, warunek, inkrementacja) mogą zawierać wiele wyrażeń,
rozdzielonych przecinkami. Przykład można napisać nieco inaczej, umieszczając definicję 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';
}

Wynikiem jest następujący łańcuch znaków:


"

**********

**********

**********

**********

**********

**********

**********

**********

**********

**********

"

68
Rozdział 2. • Proste typy danych, tablice, pętle i warunki

Oto inny przykład, w którym wykorzystałem zagnieżdżone pętle oraz operację modulo w celu
narysowania płatka śniegu:
var res = '\n', i, j;
for(i = 1; i <= 7; i++) {
for(j = 1; j <= 15; j++) {
res += (i * j) % 8 ? ' ' : '*';
}
res+= '\n';
}

Wynik:
"

* * *

*******

* * *

"

Pętla for…in
Pętli for…in używa się w celu iteracji po elementach tablicy (lub obiektu, co zobaczymy 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.

Dozwolone są dwa typy komentarzy:


Q Komentarze jednolinijkowe — zaczynają się sekwencją znaków //, a kończą wraz
z końcem linii.
Q Komentarze wielolinijkowe — zaczynają się od /*, a kończą */. Sekwencja */
może znajdować się w tej samej linii lub wiele linii dalej. Ignorowane jest wszystko,
co znajdzie się pomiędzy tymi znakami.

Przykłady:
// początek linii
var a = 1; // inne miejsce w linii
/* komentarz wielolinijkowy umieszczony w jednej linii */
/*
komentarz
zajmujący
kilka linii
*/

Istnieją narzędzia, takie jak JSDoc, które potrafią analizować kod i pobierać istotne informacje
z komentarzy.

70
Rozdział 2. • Proste typy danych, tablice, pętle i warunki

Podsumowanie
W tym rozdziale opowiedziałem, z jakich elementów buduje się programy w języku JavaScript.
Znasz już proste typy danych:
Q liczba,
Q łańcuch znaków (string),
Q boolean,
Q undefined,
Q null.

Poznaliśmy także sporo operatorów:


Q operatory arytmetyczne: +, -, *, / i %;
Q operatory inkrementacji i dekrementacji: ++ i --;
Q operatory przypisania: =, +=, -=, *=, /= i %=;
Q specjalne operatory: typeof i delete;
Q operatory logiczne: &&, || i !;
Q operatory porównania: ==, ===, !=, !==, <, >, >= i <=.

Następnie pokazałem Ci, jak korzystać z tablic w celu przechowywania danych oraz jak 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

Q false === ""


Q typeof "2E+2"
Q a = 3e+3; a++;
2. Jaką wartość będzie miała zmienna v po wykonaniu następującej operacji?
>>> var v = v || 10;
Poeksperymentuj, nadając wcześniej v wartości 100, 0, null, a także kasując jej
wartość (delete v).
3. Napisz skrypt, który wypisuje tabliczkę mnożenia. Wskazówka: użyj pętli
zagnieżdżonej w innej pętli.

72
3

Funkcje

Opanowanie funkcji ma kluczowe znaczenie podczas nauki każdego języka programowania,


a w przypadku JavaScriptu jest jeszcze ważniejsze niż zwykle. Jest tak dlatego, że w tym ję-
zyku funkcje mają bardzo wiele zastosowań i w dużej mierze to dzięki nim JavaScript jest tak
elastyczny i ekspresywny. W miejscach, gdzie w innych językach programowania trzeba by
było stosować specjalną składnię w celu wykorzystania obiektowości, JavaScript udostępnia
funkcje. Ten rozdział omawia:
Q definiowanie funkcji i korzystanie z nich,
Q przekazywanie funkcjom parametrów,
Q funkcje predefiniowane dostępne za darmo,
Q zasięg zmiennych,
Q podejście, zgodnie z którym funkcje to tylko dane specjalnego typu.

Zrozumienie powyższych tematów da nam solidne oparcie przed przejściem do kolejnej czę-
ści rozdziału, w której przedstawione zostaną pewne ciekawe zastosowania funkcji:
Q funkcje anonimowe;
Q wywołania zwrotne;
Q samowywołujące się funkcje;
Q funkcje wewnętrzne (zdefiniowane wewnątrz innych funkcji);
Q funkcje, które zwracają inne funkcje;
Q funkcje, które zmieniają swoją definicję;
Q domknięcia.
JavaScript. Programowanie obiektowe

Czym jest funkcja?


Funkcje pozwalają zgrupować pewną ilość kodu, nadać jej nazwę, a następnie ponownie wy-
korzystać przy użyciu tej właśnie nazwy. Spójrzmy na przykład:
function sum(a, b) {
var c = a + b;
return c;
}

Z jakich części składa się funkcja?


Q Słowo kluczowe function.
Q Nazwa funkcji, w przykładzie jest to sum.
Q Oczekiwane parametry (argumenty), w tym wypadku a i b. Funkcja może mieć ich
zero lub więcej. Jeśli jest ich więcej niż jeden, parametry rozdziela się przecinkami.
Q Blok kodu, nazywany ciałem funkcji.
Q Instrukcja return, która umożliwia zwrócenie obliczonej wartości funkcji. Funkcja
zawsze zwraca wartość. Jeśli nie robi tego w sposób jawny, niejawnie zwraca
wartość undefined.

Zwróć uwagę, że funkcja może zwrócić tylko jedną wartość. Jeśli potrzebne jest zwrócenie
większej liczby wartości, należy umieścić je w tablicy i zwrócić tablicę jako wartość funkcji.

Wywoływanie funkcji
Aby skorzystać z funkcji, należy ją wywołać. Funkcję wywołuje się poprzez podanie jej nazwy
i argumentów umieszczonych w nawiasie.

Wywołajmy zatem funkcję sum(), przekazując jej dwa argumenty i przypisując zwracaną
przez nią wartość zmiennej result.
>>> var result = sum(1, 2);
>>> result;

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');

[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();

Wyrażenie arguments.length zwraca liczbę parametrów podanych podczas wywołania funkcji.


Jeśli nie rozumiesz jego składni, nie przejmuj się, wrócimy do tego w następnym rozdziale.
Wtedy także dowiesz się, że arguments w rzeczywistości nie jest tablicą, ale obiektem tablico-
podobnym.

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()

Zasada czarnej skrzynki


Z reguły podczas korzystania z funkcji Twój program nie musi wiedzieć, jakie czynności są wykonywane
wewnątrz danej funkcji. Możesz myśleć o funkcjach jako o czarnych skrzynkach — podajesz im pewne
wartości (w postaci parametrów wejściowych) i odbierasz od nich zwracane wyniki. Jest to prawdziwe
dla wszystkich funkcji — tych wbudowanych w język JavaScript, tych pisanych przez Ciebie oraz tych
stworzonych przez Twoich współpracowników lub nieznanych Ci programistów.

parseInt()
parseInt() pobiera argument dowolnego typu (najczęściej łańcuch znaków) i próbuje 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

Spróbujmy teraz sparsować liczby o różnych podstawach: 10 (liczba dziesiętna) i 8 (liczba


ósemkowa).
>>> parseInt('0377', 10)

377
>>> parseInt('0377', 8)

255

Jeśli drugi argument nie zostanie podany, za podstawę uznawana jest liczba 10, z następują-
cymi wyjątkami:
Q Jeśli jako pierwszy argument przekazany zostanie łańcuch zaczynający się od 0x,
drugiemu argumentowi (jeśli nie został podany) przypisana zostanie wartość 16
(liczba zostanie uznana za szesnastkową).
Q Jeśli pierwszy parametr zaczyna się od 0, drugi otrzyma wartość 8.
>>> parseInt('377')

377
>>> parseInt('0377')

255
>>> parseInt('0x377')

887

77
JavaScript. Programowanie obiektowe

Najbezpieczniejszym rozwiązaniem jest określanie podstawy za każdym razem. Jeśli tego nie
zrobisz, kod prawdopodobnie zadziała w 99% przypadków (ponieważ najczęściej parsuje się
liczby dziesiętne), jednak jeśli trafisz na liczbę zapisaną w innym systemie, możesz osiwieć,
zanim uda Ci się znaleźć przyczynę błędu. Wyobraź sobie na przykład, że parsujesz pola 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

parseFloat(), w przeciwieństwie do parseInt(), jest w stanie poprawnie zinterpretować zapis


wykładniczy.
>>> parseFloat('123e-2')

1.23
>>> parseFloat('123e2')

12300

78
Rozdział 3. • Funkcje

>>> parseInt('1e10')

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

Ta funkcja także stara się zamienić parametr wejściowy na liczbę:


>>> isNaN('1.23')

false
>>> isNaN('a1.23')

true

Funkcja isNaN() jest potrzebna także dlatego, że liczba NaN nie jest równa samej sobie. 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"

Działanie przeciwne do encodeURI() i encodeURIComponent() mają funkcje decodeURI() i decode


´URIComponent(). W starszym kodzie można natknąć się na starsze funkcje escape() i unescape(),
jednak są one przestarzałe i nie należy ich stosować.

eval()
Funkcja eval() pobiera łańcuch znaków i uruchamia go jako kod w języku JavaScript:
>>> eval('var ii = 2;')
>>> ii

2
eval('var ii = 2;') działa dokładnie tak samo jako var ii = 2;

80
Rozdział 3. • Funkcje

Są sytuacje, w których eval() się przydaje, jednak w miarę możliwości należy tej funkcji 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.

Bonus — funkcja alert()


Spójrzmy jeszcze na bardzo popularną funkcję alert(). Nie należy ona do rdzenia języka (nie
ma jej w specyfikacji ECMA), ale można z niej korzystać w środowisku przeglądarki. Pozwala
ona na wyświetlanie komunikatów w okienku dialogowym. Czasami przydaje się to podczas
testowania i debugowania aplikacji, chociaż w tym celu lepiej korzystać z debugera Firebug.
Na poniższym rysunku widać efekt wykonania kodu alert("halo!").

Pamiętaj tylko, że okienko dialogowe blokuje wątek przeglądarki, co oznacza, że żaden inny
kod nie zostanie wykonany, zanim użytkownik nie kliknie OK. Jeśli aplikacja jest często 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

local is not defined

Ponadto należy mieć na uwadze, że jeśli do deklaracji zmiennej nie zostanie użyta instrukcja
var, zmienna będzie miała zasięg globalny. Spójrzmy na przykład:

Co się stało? Funkcja f() zawiera zmienną local. Przed wywołaniem funkcji zmienna nie 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

Poniższy przykład ilustruje ważny aspekt podziału na zmienne lokalne i globalne.


var a = 123;
function f() {
alert(a);
var a = 1;
alert(a);
}
f();

Być może spodziewasz się, że pierwszy alert() wyświetli 123 (wartość globalnej zmiennej a),
a drugi wyświetli 1 (wartość lokalnej zmiennej a). Jednak stanie się inaczej. Pierwszy alert()
pokaże "undefined". Stanie się tak dlatego, że wewnątrz funkcji zasięg lokalny jest ważniejszy
od globalnego. Zmienna lokalna nadpisuje zmienną globalną o tej samej nazwie. Podczas 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

>>> typeof sum;

"undefined"
>>> typeof add;

"function"
>>> add(1, 2);

Ponieważ funkcje to dane przypisane do zmiennych, stosujemy tę samą konwencję nazw co


przy nazywaniu zmiennych — nazwa funkcji nie może zaczynać się liczbą i może zawierać
dowolną kombinację liter, cyfr oraz znaku podkreślnika.

Funkcje anonimowe
JavaScript pozwala na rozrzucanie fragmentów danych po całym programie. Wyobraź sobie,
że Twój program zawiera następujący fragment kodu:
>>> "test"; [1,2,3]; undefined; null; 1;

Kod wygląda dość dziwnie, ponieważ nie robi nic pożytecznego, jednak jest poprawny i nie
spowoduje błędu. Można powiedzieć, że zawiera dane anonimowe, czyli nieprzypisane do
żadnej zmiennej i nieposiadające nazwy.

Wiesz już, że funkcje można traktować jak wszystkie inne dane. W związku z tym ich także
można używać bez podania nazwy:
>>> function(a){return a;}

Anonimowe fragmenty danych w kodzie nie mogą być zbyt przydatne, chyba że są funkcjami.
W takim wypadku istnieją dwa bardzo eleganckie zastosowania tych danych:
Q Funkcję anonimową można przekazać jako parametr do innej funkcji. Funkcja
odbierająca ten parametr może przeprowadzić operacje na otrzymanej funkcji.
Q Funkcje anonimowe możne definiować i od razu uruchamiać.

Przyjrzyjmy się uważniej obu zastosowaniom funkcji anonimowych.

Wywołania zwrotne
Skoro funkcje to dane, które można przypisać zmiennym, to można je definiować, kasować,
kopiować… Dlaczego zatem nie miałoby być możliwe przekazywanie ich jako parametrów do
innych funkcji?

84
Rozdział 3. • Funkcje

Oto przykład funkcji, która pobiera dwie funkcje jako parametry, wywołuje je, po czym 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;
}

Możemy przekazać je oryginalnej funkcji i obejrzeć wynik:


>>> wywolaj_i_dodaj(jeden, dwa);

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.

Przykłady wywołań zwrotnych


Przeanalizujmy częsty scenariusz: mamy funkcję, która zwraca wartość, przekazywaną na-
stępnie kolejnej funkcji. W naszym przykładzie pierwsza funkcja, pomnozRazyDwa(), przyjmuje
trzy parametry, przechodzi przez nie w pętli oraz zwraca tablicę zawierającą wynik. Druga
funkcja, dodajJeden(), pobiera wartość, dodaje do niej jeden, po czym zwraca wynik.

85
JavaScript. Programowanie obiektowe

function pomnozRazyDwa(a, b, c) {
var i, ar = [];
for(i = 0; i < 3; i++) {
ar[i] = arguments[i] * 2;
}
return ar;
}
function dodajJeden(a) {
return a + 1;
}

Przetestujmy te funkcje:
>>> pomnozRazyDwa(1, 2, 3);

[2, 4, 6]
>>> dodajJeden(100)

101

Załóżmy teraz, że chcemy, by tablica myarr zawierała trzy elementy, z których każdy przejdzie
przez obie funkcje. Zacznijmy od pomnozRazyDwa().
>>> var myarr = [];
>>> myarr = pomnozRazyDwa(10, 20, 30);

[20, 40, 60]

Możemy teraz wywoływać funkcję dodajJeden() w pętli, raz dla każdego elementu tablicy:
>>> for (var i = 0; i < 3; i++) {myarr[i] = addOne(myarr[i]);}
>>> myarr

[21, 41, 61]

Wszystko zadziała, ale jest tu pole do poprawek. Po pierwsze, przykład uruchamia dwie pętle,
które mogą być kosztowne, jeśli powtórzeń jest wiele. Żądany wynik można otrzymać przy
użyciu jednej tylko pętli. Oto, jak zmienić funkcję pomnozRazyDwa() tak, by jako parametr
przyjmowała funkcję i wywoływała ją przy każdej iteracji:
function pomnozRazyDwa(a, b, c, callback) {
var i, ar = [];
for(i = 0; i < 3; i++) {
ar[i] = callback(arguments[i] * 2);
}
return ar;
}

Zmieniona wersja funkcji pozwala na wykonanie tej samej pracy przy pomocy jednego 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

>>> myarr = pomnozRazyDwa(1, 2, 3, dodajJeden);

[3, 5, 7]

Zamiast definiowania funkcji dodajJeden() można skorzystać z funkcji anonimowej, dzięki


czemu zdefiniowana zostanie jedna zmienna globalna mniej.
>>> myarr = pomnozRazyDwa(1, 2, 3, function(a){return a + 1});

[3, 5, 7]

Oczywiście tej samej funkcji można jako parametr przekazać różne funkcje anonimowe:
>>> myarr = multiplyByTwo(1, 2, 3, function(a){return a + 2});

[4, 6, 8]

Funkcje samowywołujące się


Omówiliśmy już funkcje anonimowe i wywołania zwrotne. Przejdźmy teraz do innego zastosowa-
nia funkcji anonimowych — wywoływania funkcji zaraz po ich zdefiniowaniu. Oto przykład:
(
function(){
alert('uuu!');
}
)()

Początkowo może to wyglądać groźnie, ale tak naprawdę to proste — funkcję anonimową
umieszcza się w nawiasie, po którym następuje inny nawias (w przykładzie jest pusty). Drugi
nawias oznacza „uruchom teraz”. To w nim umieszcza się ewentualne parametry funkcji.
(
function(imie){
alert('Cześć ' + imie + '!');
}
)('stary')

Jedną z zalet samowywołujacych się funkcji anonimowych jest to, że kod zostanie wykonany
bez tworzenia nadmiaru zmiennych. Minus jest taki, że tej samej funkcji nie da się uruchomić
dwukrotnie (chyba że znajdzie się wewnątrz pętli lub innej funkcji). Dlatego anonimowe funkcje
samowywołujące najlepiej nadają się do wykonywania jednokrotnych zadań inicjalizacyjnych.

Funkcje wewnętrzne (prywatne)


Skoro funkcje są zwykłymi wartościami, nic nie stoi na przeszkodzie, by zdefiniować funkcję
wewnątrz innej funkcji.

87
JavaScript. Programowanie obiektowe

function a(param) {
function b(theinput) {
return theinput * 2;
};
return 'Wynik wynosi ' + b(param);
};

Stosując drugą notację definiowania funkcji, możemy napisać:


var a = function(param) {
var b = function(theinput) {
return theinput * 2;
};
return 'Wynik wynosi ' + b(param);
};

Kiedy globalna funkcja a() zostanie wywołana, wywoła także lokalną funkcję b(). Jako że b()
jest lokalna, nie jest dostępna spoza a(), dlatego nazywamy ją funkcją prywatną.
>>> a(2);

"The result is 4"


>>> a(8);

"The result is 16"


>>> b(2);

b is not defined

Ze stosowania funkcji prywatnych płyną następujące korzyści:


Q Nie dochodzi do zaśmiecenia globalnej przestrzeni nazw (zmniejszone ryzyko
kolizji nazw).
Q Prywatność: na zewnątrz widoczne są tylko te funkcje, które programista chce
udostępnić. Funkcjonalności nieprzeznaczone dla reszty aplikacji są ukryte.

Funkcje, które zwracają funkcje


Wspominałem już, że funkcja zawsze zwraca wartość, a jeśli nie robi tego w sposób jawny, to
niejawnie zwracana jest wartość undefined. Funkcja zwraca dokładnie jedną wartość, która
z powodzeniem może być inną funkcją.
function a() {
alert('A!');
return function(){
alert('B!');
};
}

88
Rozdział 3. • Funkcje

Widoczna powyżej funkcja a() wykonuje swoją pracę (mówi 'A!') i zwraca inną funkcję, która
robi coś innego (mówi 'B!'). Wynik można przypisać jakiejś zmiennej i używać jej jako 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()();

Funkcjo, przepiszże się!


Ponieważ funkcje potrafią zwracać funkcje, możliwe jest zastąpienie oryginalnej funkcji tą
zwracaną. Wróćmy do poprzedniego przykładu. Wartość zwróconą przez wywołanie a()
można przypisać zmiennej a, nadpisując w ten sposób istniejącą funkcję:
>>> a = a();

Powyższa linia przy pierwszym uruchomieniu spowoduje wyświetlenie 'A!', jednak jej 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!');
};
}

Przy pierwszym wywołaniu funkcja:


Q Wyświetli 'A!' (załóżmy, że to właśnie jest nasze jednorazowe zadanie
inicjalizacyjne).
Q Zmieni definicję globalnej zmiennej a, przypisując jej nową funkcję.

Każde kolejne wywołanie będzie powodowało wyświetlenie 'B!'.

89
JavaScript. Programowanie obiektowe

Oto inny przykład, który łączy kilka technik omówionych na ostatnich kilku stronach:
var a = function() {
function inicjalizacja(){
var setup = 'już';
}
function normalnaPraca() {
alert('praca wre!');
}
inicjalizacja();
return normalnaPraca;
}();

W przykładzie:
Q Mamy funkcje prywatne: inicjalizacja() i normalnaPraca().
Q Mamy funkcję samowywołującą się: funkcja a() jest wywoływana dzięki nawiasowi
po jej definicji.
Q Pierwsze wywołanie a() polega na wywołaniu funkcji inicjalizacja() i zwróceniu
referencji do zmiennej normalnaPraca, która jest funkcją. Zwróć uwagę na brak
nawiasów przy zwracanej wartości — nie ma ich dlatego, że zwracamy do funkcji
referencję, a nie wynik wywołania tejże funkcji.
Q Jako że kod zaczyna się od var a =, wartość zwrócona przez samowywołującą się
funkcję zostanie przypisana zmiennej a.

Jeśli chcesz sprawdzić, czy poprawnie rozumiesz omówiony zakres materiału, spróbuj 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()?

Przedstawione mechanizmy okazują się bardzo przydatne w środowisku przeglądarki. Różne


przeglądarki mogą realizować konkretne zadania na różne sposoby. Przy założeniu, że właści-
wości przeglądarki nie zmienią się pomiędzy wywołaniami funkcji, możemy stworzyć funkcję,
która wybierze sposób działania najlepiej dopasowany do danej przeglądarki, po czym w od-
powiedni sposób zmieni swoją definicję, dzięki czemu tylko raz będzie musiała wykrywać typ
przeglądarki. Konkretne przykłady zastosowania tego scenariusza będzie można zobaczyć na
dalszych stronach książki.

Domknięcia
Pozostała część tego rozdziału jest poświęcona domknięciom (czyż istnieje lepszy sposób na
zamknięcie rozdziału?). Domknięcia początkowo mogą wydawać się trudne do zrozumienia,
dlatego nie zniechęcaj się, jeśli nie pojmiesz wszystkiego od razu. Postaraj się doczytać rozdział

90
Rozdział 3. • Funkcje

do końca i poeksperymentować z przykładami, a jeśli niektóre zagadnienia nadal nie będą 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

>>> function f1(){var a = 1; f2();}


>>> function f2(){return a;}
>>> f1();

a is not defined

Wewnątrz funkcji f1() wywołujemy funkcję f2(). Ponieważ zmienna lokalna a znajduje się
także wewnątrz f1(), ktoś mógłby się spodziewać, że f2() będzie miała dostęp do a, jednak
tak nie jest. W momencie definicji f2() (a nie w momencie wywołania) nigdzie nie było śladu
a. f2(), podobnie jak f1(), ma dostęp jedynie do własnego zakresu oraz do zakresu globalnego.
f1() i f2() nie współdzielą zakresów lokalnych.

Podczas definiowania funkcja zapamiętuje swoje środowisko, to znaczy swój łańcuch zakresów.
Nie znaczy to wcale, że funkcja pamięta każdą konkretną zmienną, która pojawiła się w tym
zakresie. Wręcz przeciwnie — zmienne można dodawać, usuwać i uaktualniać, a funkcja zawsze
będzie widziała najnowszy, aktualny stan zmiennych. Jeśli rozszerzymy przykład o deklarację
globalnej zmiennej a, stanie się ona widoczna dla f2(), ponieważ f2() zna ścieżkę do zmiennych
globalnych i ma dostęp do całości tego środowiska. Zwróć uwagę na to, że f1() zawiera wywo-
łanie f2(), które działa — mimo że f2() nie została jeszcze zdefiniowana. f1() musi tylko 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

Przedstawiony mechanizm sprawia, że JavaScript jest bardzo elastyczny — można dodawać


zmienne, usuwać je, a potem dodawać je ponownie. Możesz poeksperymentować, kasując
funkcję f2(), a potem definiując ją ponownie, ale z innym ciałem. Funkcja f1() nadal będzie
działać, ponieważ musi znać jedynie sposób dostępu do swojego zakresu — nie jest jej po-
trzebna wiedza o tym, co kiedyś do tego zakresu należało. Ciąg dalszy przykładu:

92
Rozdział 3. • Funkcje

true
>>> f1()

f2 is not defined
>>> var f2 = function(){return a * 2;}
>>> var a = 5;

5
>>> f1();

10

Przerwanie łańcucha za pomocą domknięcia


Zaczniemy od ilustracji.

Poniżej widzisz zakres globalny. Wyobraź go sobie jako wszechświat.

Może on zawierać zmienne, takie jak a, i funkcje, jak F.

Funkcje posiadają własną przestrzeń, którą mogą wykorzystywać do przechowywania innych


zmiennych (i funkcji). W pewnym momencie rysunek będzie wyglądał mniej więcej tak:

93
JavaScript. Programowanie obiektowe

Jeśli jesteś w punkcie a, jesteś w przestrzeni globalnej. Jeśli w punkcie b, który należy do
przestrzeni funkcji F, masz dostęp do przestrzeni globalnej oraz do przestrzeni F. Jeśli 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;
}
}

Co się stanie po wywołaniu f()?


>>> f();

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

Domknięcie 3. i jedna definicja


W oparciu o to, co zostało powiedziane do tej pory, możemy powiedzieć, że domknięcie jest
tworzone, gdy funkcja zachowuje dostęp do zakresu rodzica po tym, jak rodzic zwrócił ją do
globalnej przestrzeni nazw.

Argument przekazany funkcji wewnątrz niej jest dostępny jako zmienna globalna. Możesz 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;
}

Funkcję można wywołać w następujący sposób:


>>> var m = f(123);
>>> m();

124

Zauważ, że zmienna arg została zwiększona już po definicji funkcji, a pomimo tego m() zwróciła
aktualną wartość. Jest to kolejny dowód na to, że funkcje są związane ze swoimi zakresami, a nie
z przechowywanymi tam w danym momencie zmiennymi i ich wartościami.

Domknięcia w pętli
Pokażę teraz coś, co często prowadzi do bardzo trudnych do wykrycia błędów, ponieważ na
pierwszy rzut oka wydaje się, że nie ma tam miejsca na pomyłkę.

Napiszmy pętlę o trzech iteracjach, która za każdym przebiegiem zwraca numer pętli. 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

Wywołajmy ją teraz, przypisując wynikową tablicę zmiennej a.


>>> var a = f();

Mamy zatem tablicę z trzema funkcjami. Wywołajmy je, podając nawiasy po każdym 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;
}

Uzyskamy oczekiwany wynik:


>>> var a = f();
>>> a[0]();

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++];
};
}

Wywołanie funkcji setup() z parametrem będącym tablicą danych spowoduje automatyczne


utworzenie funkcji next().
>>> var next = setup(['a', 'b', 'c']);

Dalej czekają nas sam przyjemności: wywołując wciąż tę samą funkcję, przejdziemy przez
wszystkie elementy tablicy.

99
JavaScript. Programowanie obiektowe

>>> next();

"a"
>>> next();

"b"
>>> next();

"c"

Podsumowanie
Właśnie skończyliśmy podstawowy kurs pojęć związanych z funkcjami. Przejście do konceptów
programowania obiektowego oraz do wzorców wykorzystywanych w nowoczesnym 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

Q funkcje zmieniające swoją definicję.


Q Domknięcia.

Ćwiczenia
1. Napisz funkcję, która przekształca szesnastkową definicję koloru (np. niebieski
to "0000FF") na reprezentację RGB (np. "rgb(0, 0, 255)"). Nazwij funkcję getRGB()
i przetestuj ją za pomocą następującego kodu:

100
Rozdział 3. • Funkcje

>>> var a = getRGB("#00FF00");


>>> a;

"rgb(0, 255, 0)"

2. Co pojawi się w konsoli po uruchomieniu każdej z poniższych linii kodu?


>>> parseInt(1e1)
>>> parseInt('1e1')
>>> parseFloat('1e1')
>>> isFinite(0/10)
>>> isFinite(20/0)
>>> isNaN(parseInt(NaN));
3. Co pojawi się w okienku alert() po wykonaniu następującego kodu?
var a = 1;
function f() {
var a = 2;
function n() {
alert(a);
}
n();
}
f();
4. Wszystkie poniższe przykłady spowodują wyświetlenie "Uuu!". Czy potrafisz
powiedzieć dlaczego?
4.1
var f = alert;
eval('f("Uuu!")');
4.2
var e;
var f = alert;
eval('e=f')('Uuu!');
4.3
(
function(){
return alert;
}
)()('Uuu!');

101
JavaScript. Programowanie obiektowe

102
4

Obiekty

Skoro znasz już na wylot podstawowe typy danych, tablice oraz funkcje, przyszła pora na to,
co najciekawsze — obiekty. W tym rozdziale dowiesz się:
Q jak tworzyć obiekty i jak ich używać,
Q czym są funkcje nazywane konstruktorami,
Q jak korzystać z wbudowanych obiektów JavaScriptu.

Od tablic do obiektów
Jak już wiesz z rozdziału 2., tablica jest listą wartości. Każdej wartości odpowiada indeks, przy
czym indeksy kolejnych elementów zaczynają się od zera i są zwiększane o jeden dla każdej
kolejnej wartości.
>>>> var myarr = ['czerwony', 'niebieski', 'żółty', 'fioletowy'];
>>> myarr;

["czerwony", "niebieski", "żółty", "fioletowy"]


>>> myarr[0]

"czerwony"
>>> myarr[3]

"fioletowy"
JavaScript. Programowanie obiektowe

Jeśli wstawimy indeksy do jednej kolumny tablicy, a wartości do drugiej, otrzymamy nastę-
pującą tablicę par klucz – wartość:

Klucz Wartość
0 czerwony
1 niebieski
2 żółty
3 fioletowy

Obiekty różnią się od tablic między innymi tym, że programista samodzielnie definiuje 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.

Przeanalizujmy więc nasz pierwszy, prosty obiekt:


var bohater = {
gatunek: 'Żółw',
specjalizacja: 'Ninja'
};

Możesz zauważyć, że:


Q Zmienna, która przechowuje obiekt, nazywa się bohater.
Q Inaczej niż w przypadku tablic, do definiowania obiektów używa się nawiasów
klamrowych { i }, a nie kwadratowych [ i ].
Q Elementy obiektu (nazywane polami lub własnościami) oddziela się za pomocą
przecinków.
Q Pary klucz – wartość rozdziela się dwukropkiem.

Klucze (nazwy pól) można umieszczać w cudzysłowach. Poniższe instrukcje są równoważne:


var o = {prop: 1};
var o = {"prop": 1};
var o = {'prop': 1};

Nie zaleca się stosowania cudzysłowów (chociażby ze względu na oszczędność znaków), 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

Pokazany poniżej dziwaczny twór:


var o = {
pole: 1,
'tak lub nie': 'tak',
'!@#$%^&*': true
};

jest w pełni poprawnym obiektem. W przypadku drugiego i trzeciego pola cudzysłów1 jest
obowiązkowy — pominięcie go doprowadzi do błędu.

W dalszej części rozdziału poznasz inne niż []i {} sposoby definiowania obiektów i tablic. Ta-
blice zdefiniowane za pomocą [] określa się mianem literałów tablicowych, a obiekty zdefi-
niowane za pomocą {} to literały obiektowe.

Elementy, pola, metody


Mówimy, że tablice zawierają elementy. Obiekty, dla odmiany, mają pola. Dla JavaScriptu to
rozróżnienie nie ma znaczenia — jest ono czysto terminologiczne i pochodzi z innych języ-
ków programowania.

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

W języku JavaScript tablicom indeksowanym odpowiadają tablice, a tablicom asocjacyjnym


— obiekty.

Dostęp do własności obiektu


Dostęp do własności obiektu można uzyskać na dwa sposoby:
Q przy użyciu nawiasów kwadratowych, na przykład bohater['specjalizacja'];
Q przy użyciu kropki, na przykład bohater.specjalizacja.

Notacja z kropką jest wygodniejsza, ale nie zawsze można ją zastosować. Zasady są takie same
jak w przypadku nazw pól: jeśli nazwa nie jest poprawną nazwą zmiennej, nie można skorzy-
stać z notacji z kropką.

Weźmy następujący obiekt:


var bohater = {
gatunek: 'Żółw',
specjalizacja: 'Ninja'
};

Dostęp do własności obiektu za pomocą notacji z kropką:


>>> bohater.gatunek;

."Żółw"

Dostęp do własności obiektu za pomocą notacji nawiasowej:


>>> bohater['specjalizacja'];
"Ninja"

Próba dostępu do nieistniejącego pola kończy się zwróceniem wartości undefined:


>>> 'Kolor włosów bohatera to ' + bohater.kolor_wlosow;

"Kolor włosów bohatera to undefined"

Obiekty mogą zawierać dane, w tym także inne obiekty.


var ksiazka = {
tytul: 'Paragraf 22',
wydana: 1961,
autor: {
imie: 'Joseph',
nazwisko: 'Heller'
}
};

106
Rozdział 4. • Obiekty

W celu pobrania wartości pola imie obiektu będącego wartością pola autor obiektu ksiazka,
należy napisać:
>>> ksiazka.autor.imie

"Joseph"

lub, przy użyciu składni z nawiasami:


>>> ksiazka['autor']['nazwisko']

"Heller"

Można nawet łączyć notacje:


>>> ksiazka.autor['nazwisko']

"Heller"
>>>ksiazka['autor'].nazwisko

"Heller"

Istnieje jeszcze jedna sytuacja, w której konieczne jest użycie notacji nawiasowej. Jeśli nazwa
pola, do którego chcemy sięgnąć, nie jest znana w czasie pisania kodu, można przypisać jej
wartość zmiennej:
>>> var klucz = 'imie'
>>> ksiazka.autor[klucz];

Joseph

Wywoływanie metod obiektu


Skoro metoda jest po prostu polem klasy, które przypadkiem jest także funkcją, dostęp do
metod odbywa się tak samo jak dostęp do zwykłych pól: przy użyciu notacji z kropką lub no-
tacji nawiasowej. Metody wywołuje się jak wszystkie inne funkcje: należy po nazwie metody
dodać nawiasy, które wydadzą rozkaz „Wykonać!”.
var bohater = {
gatunek: 'Żółw',
specjalizacja: 'Ninja'
mow: function() {
return 'Moja specjalizacja to ' + bohater.specjalizacja;
}
}
>>> bohater.mow();

"Moja specjalizacja to Ninja"

107
JavaScript. Programowanie obiektowe

Jeśli metoda pobiera parametry, przekazujemy je dokładnie tak samo jak w przypadku 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']();

Dobra rada: żadnych cudzysłowów!


1. Podczas sięgania do pól i metod stosuj notację z kropką.
2. Nie używaj cudzysłowów w nazwach pól literałów obiektowych.

Modyfikacja pól i metod


JavaScript jest językiem dynamicznym: pozwala modyfikować składowe (czyli pola i metody)
istniejących obiektów w czasie wykonania. Można dodawać nowe składowe i usuwać stare.
Można utworzyć pusty obiekt, a pola i metody dodać do niego później. Zobaczmy, jak to zrobić.

Pusty obiekt:
>>> var bohater = {};

Dostęp do nieistniejącego pola:


>>> typeof bohater.gatunek

"undefined"

Dodanie pól i metod:


>>> bohater.gatunek = 'Żółw';
>>> bohater.imie = 'Leonardo';
>>> bohater.mowImie = function() {return bohater.imie;};

Wywołanie metody:
>>> bohater.mowImie();

"Leonardo"

Usuwanie własności:
>>> delete bohater.imie;

true

108
Rozdział 4. • Obiekty

Próba ponownego wywołania metody zwracającej imię zakończy się niepowodzeniem:


>>> bohater.mowImie();

reference to undefined property bohater.imie

Wartość this
W poprzednim przykładzie widzieliśmy metodę mowImie(), która do pola imie obiektu bohater
odwoływała się za pomocą składni bohater.imie. Istnieje jednak inny, bardziej ogólny sposób
dostępu z wnętrza metody do aktualnego obiektu (to znaczy do obiektu, do którego należy
metoda): poprzez specjalną wartość this.
var bohater = {
imie: 'Rafael',
mowImie: function() {
return this.imie;
}
}
>>> bohater.mowImie();

"Rafael"

Jak widać, this (z ang. „ten”) oznacza bieżący obiekt.

Konstruktory
Obiekty można tworzyć także przy użyciu funkcji nazywanych konstruktorami. Przykład:
function Bohater() {
this.specjalizacja = 'Ninja';
}

Konstruktor wywołujemy przy użyciu operatora new:


>>> var bohater = new Bohater();
>>> bohater.specjalizacja;

"Ninja"

Przewagą tego sposobu tworzenia obiektów jest to, że konstruktory mogą przyjmować 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;
}
}

Przy użyciu jednego konstruktora można utworzyć wiele różnych obiektów:


>>> var h1 = new Bohater('Michał Anioł');
>>> var h2 = new Bohater('Donatello');
>>> h1.kimJestes();

"Jestem Michał Anioł, a moja specjalizacja to Ninja"


>>> h2.kimJestes();

"Jestem Donatello, a moja specjalizacja to Ninja"

Konwencja nakazuje zaczynać nazwy konstruktorów wielką literą, dzięki czemu od razu moż-
na zorientować się, że nie mamy do czynienia z normalną funkcją. Wywołanie konstruktora bez
operatora new nie zostanie uznane za błąd, ale może prowadzić do nieoczekiwanych wyników:
>>> var h = Bohater('Leonardo');
>>> typeof h

"undefined"

Co tu zaszło? Ponieważ nie został użyty operator new, nie powstał nowy obiekt. Funkcja została
wywołana jako zwykła funkcja, a nie jako konstruktor, zatem h zawiera wartość zwracaną przez
funkcję. Ponieważ jednak funkcja nie zawiera instrukcji return, w rzeczywistości zwraca 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

Dostęp do niej uzyskasz na różne sposoby:


Q odwołując się do zmiennej a;
Q odwołując się do pola obiektu globalnego, na przykład window['a'] lub window.a.

Wróćmy do przykładu, w którym definiowaliśmy konstruktor i wywoływaliśmy go bez użycia


operatora new. Jak już mówiłem, w takim wypadku this odwołuje się do obiektu globalnego,
a wszystkie własności ustawione za pomocą this stają się własnościami obiektu globalnego
(w przypadku przeglądarki będzie to window).

Zadeklarowanie konstruktora i wywołanie go bez new zwróci undefined.


>>> function Bohater(imie) {this.imie = imie;}
>>> var h = Bohater('Leonardo');
>>> typeof h
"undefined"
>>> typeof h.imie
h has no properties

Ponieważ wewnątrz funkcji Bohater pojawiło się this, utworzona została zmienna globalna
(pole obiektu globalnego) o nazwie imie. Próba odwołania się do pola imie zmiennej h kończy
się niepowodzeniem (i komunikatem, że h nie posiada żadnych własności).
>>> imie
"Leonardo"
>>> window.imie

"Leonardo"

Jeśli konstruktor zostanie wywołany z użyciem new, zwrócony zostanie nowy obiekt, do któ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.

Funkcje zwracające obiekty


Obiekty można tworzyć nie tylko za pomocą konstruktorów i operatora new, ale także za po-
mocą zwykłych funkcji. Możliwe jest napisanie funkcji, która wykona pewne zadania przygo-
towawcze i na koniec zwróci wartość będącą obiektem.

Poniższy przykład przedstawia prostą funkcję o nazwie factory() („fabryka”), która produkuje
obiekty:
function factory(name) {
return {
name: name
};
}

Korzysta się z niej w następujący sposób:


>>> var o = factory('jeden');
>>> o.name
"jeden"
>>> o.constructor

Object()

Można także korzystać z konstruktorów i zwracać obiekty inne niż this, czyli zmieniać stan-
dardowe zachowanie konstruktora. Już tłumaczę, jak to zrobić.

Oto najczęstszy scenariusz wykorzystania konstruktora:


>>> function C() {this.a = 1;}
>>> var c = new C();
>>> c.a
1

A jednak można zrobić coś takiego:


>>> function C2() {this.a = 1; return {b: 2};}
>>> var c2 = new C2();
>>> typeof c2.a
"undefined"
>>> c2.b

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

Tak samo ma się sprawa z przekazywaniem obiektów funkcjom:


>>> var oryginal = {ile: 100};
>>> var zeruj = function(o) {o.ile = 0;}
>>> zeruj(oryginal);
>>> oryginal.ile

Porównywanie obiektów
Wynikiem porównania dwóch obiektów będzie true tylko wtedy, gdy porównywane będą
dwie referencje do tego samego obiektu. Jeśli porównamy dwa oddzielne obiekty, które akurat
mają ten sam zestaw pól i metod, to mimo wszystko otrzymamy false.

Utwórzmy dwa obiekty, które wyglądają tak samo:


>>> var azor = {gatunek: 'pies'};
>>> var burek = {gatunek: 'pies'};

114
Rozdział 4. • Obiekty

Wynikiem porównania będzie false:


>>> azor === burek

false
>>> azor == burek

false

Utwórzmy teraz nową zmienną mojPies i przypiszmy jej jeden z obiektów. W ten sposób
otrzymamy dwie zmienne wskazujące ten sam obiekt.
>>> var mojPies = burek;

Teraz mojPies i burek są referencjami do tego samego obiektu. Zmiana własności mojPies po-
ciągnie za sobą zmianę własności obiektu burek. Wynikiem porównania będzie true.
>>> mojPies === burek

true

Ponieważ azor jest innym obiektem, nie zostanie dopasowany do mojPies:


>>> mojPies === azor

false

Obiekty w konsoli Firebug


Zanim na poważnie zajmiemy się obiektami wbudowanymi, chcę powiedzieć kilka słów na
temat pracy z obiektami w konsoli Firebug.

Prawdopodobnie udało się już, na podstawie przykładów przedstawionych w tym rozdziale,


wyciągnąć pewne wnioski na temat sposobu wyświetlania obiektów w konsoli. Jeśli utworzysz
obiekt, a następnie wpiszesz jego nazwę, obiekt zostanie przedstawiony w postaci łańcucha
znaków, który uwzględni także własności obiektu. W przypadku gdy własności jest zbyt wiele,
pokazane zostanie tylko kilka pierwszych.

115
JavaScript. Programowanie obiektowe

Jeśli klikniesz na reprezentacji obiektu, Firebug przeniesie Cię do zakładki DOM, w której
pokazane są wszystkie własności obiektu. Jeśli dana własność sama jest obiektem, obok jej
nazwy pojawi się znak plus (+), za pomocą którego można wyświetlić szczegóły zagnieżdżonego
obiektu.

Konsola daje nam dostęp do obiektu o nazwie console, którego metody, takie jak console.log(),
console.error() i console.info(), pozwalają wypisać w konsoli dowolną wartość.

console.log() przydaje się, gdy trzeba szybko coś przetestować lub gdy skrypt ma wypisywać
informacje ułatwiające debugowanie. Poniższy przykład pokazuje zastosowanie tej metody
w pętli:
>>> for(var i = 0; i < 5; i++) { console.log(i); }

0
1
2
3
4

116
Rozdział 4. • Obiekty

Obiekty wbudowane
Wcześniej w tym rozdziale zetknęliśmy się już z konstruktorem Object(). Jest on zwracany
przez obiekty literałowe, gdy sięgnie się do ich pola constructor. Funkcja ta jest jednym
z konstruktorów wbudowanych. Takich konstruktorów jest więcej — z wszystkimi spośród
nich spotkasz się zaraz na kartach tego rozdziału.

Obiekty wbudowane można podzielić na trzy kategorie:


Q Obiekty opakowujące: Object, Array, Function, Boolean, Number i String. Odpowiadają
one różnym typom danych JavaScriptu. Zasadniczo każda wartość zwracana
przez operator typeof (omówiony w rozdziale 2.) posiada swój obiekt opakowujący.
Wyjątkami są "undefined" i "null".
Q Obiekty użytkowe. Są to Math, Date i RegExp — warto jest je poznać, ponieważ często
okazują się przydatne.
Q Obiekty błędów, czyli obiekt Error oraz inne, bardziej szczegółowe obiekty,
za pomocą których można przywrócić działanie programu po wystąpieniu
nieoczekiwanej sytuacji.

W rozdziale omawiam jedynie wybrane metody obiektów wbudowanych. Pełna lista znajduje
się w dodatku C.

Jeśli nie widzisz różnicy pomiędzy wbudowanym obiektem a wbudowanym konstruktorem —


nie martw się, w zasadzie są tym samym. Za chwilę wytłumaczę, że funkcje, wśród nich rów-
nież konstruktory, także są obiektami.

Object
Object jest rodzicem wszystkich obiektów w języku JavaScript — wszystkie inne obiekty z niego
dziedziczą. W celu utworzenia nowego obiektu możesz skorzystać z notacji literałowej albo
z konstruktora Object(). Następujące dwie linie są równoważne:
>>> var o = {};
>>> var o = new Object();

Pusty obiekt nie jest zupełnie bezużyteczny, ponieważ już na starcie jest wyposażony w kilka
pól i metod:
Q Własność o.constructor zwróci konstruktor.
Q o.toString() to metoda, która zwraca tekstową reprezentację obiektu.
Q o.valueOf() zwraca jednowartościową reprezentację obiektu, najczęściej sam
obiekt.

117
JavaScript. Programowanie obiektowe

Zobaczmy te metody w akcji:


>>> var o = new Object();

Wywołanie toString()zwróci tekstową reprezentację obiektu:


>>> o.toString()

"[object Object]"

Metoda toString() zostanie wewnętrznie wywołana przez JavaScript, jeśli obiekt zostanie
użyty w kontekście łańcucha znaków. Przykładowo alert() działa jedynie na łańcuchach,
dlatego jeśli zostanie jej przekazany obiekt, w tle zostanie wywołana metoda toString(). 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

"An object: [object Object]"

valueOf() to kolejna metoda, w którą wyposażone są wszystkie obiekty. W przypadku obiek-


tów prostych, których konstruktorem jest Object(), valueOf() zwróci dany obiekt:
>>> o.valueOf() === o

true

Podsumujmy:
Q Obiekty można tworzyć za pomocą var o = {}; (preferowana notacja literałowa)
lub za pomocą var o = new Object();.
Q Każdy, nawet najbardziej złożony obiekt dziedziczy z obiektu Object i dzięki temu
posiada metody takie jak toString() i pola takie jak constructor.

Array
Array() to funkcja wbudowana, której można używać jako konstruktora do tworzenia tablic:
>>> var a = new Array();

Powyższy fragment kodu odpowiada następującemu zapisowi literałowemu:


>>> var a = [];

118
Rozdział 4. • Obiekty

Niezależnie od tego, w jaki sposób została utworzona tablica, można dodawać do niej 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;

[undefined, undefined, undefined, undefined, undefined]

Skoro tablice można tworzyć przy użyciu konstruktora, czy są one obiektami? Tak — można
upewnić się za pomocą operatora typeof:
>>> typeof a;

"object"

Każdy obiekt dziedziczy pola i metody pochodzące od Object:


>>> a.toString();

"1,2,3,cztery"
>>> a.valueOf()

[1, 2, 3, "cztery"]
>>> a.constructor

Array()

Tablice są obiektami obdarzonymi pewnymi wyjątkowymi cechami:


Q Ich pola są nazywane automatycznie za pomocą liczb od zera w górę.
Q Posiadają pole length, które zawiera liczbę elementów tablicy.
Q Poza metodami odziedziczonymi z Object posiadają własne metody wbudowane.

Przyjrzymy się różnicom pomiędzy tablicą a obiektem. Na początek utwórzmy pusty obiekt
o i pustą tablicę a:
>>> var a = [], o = {};

119
JavaScript. Programowanie obiektowe

Tablice zawsze posiadają pole length określające ich długość, podczas gdy zwykłe obiekty nie:
>>> a.length

0
>>> typeof o.length

"undefined"

Zarówno do tablic, jak i do obiektów można dodawać pola liczbowe i nieliczbowe:


>>> a[0] = 1; o[0] = 1;
>>> a.prop = 2; o.prop = 2;

Pole length zawsze przechowuje liczbę pól numerycznych, ignorując pozostałe.


>>> a.length

Wartość pola length można zmieniać. Zwiększenie jego wartości powoduje dodanie do tablicy
pustych elementów (o wartości undefined).
>>> a.length = 5

5
>>> a

[1, undefined, undefined, undefined, undefined]

Zmniejszenie wartości pola length spowoduje usunięcie końcowych elementów.


>>> a.length = 2;

2
>>> a

[1, undefined]

Ciekawe metody obiektu Array


Poza metodami odziedziczonymi z Object obiekty tablicowe posiadają własne przydatne me-
tody, takie jak sort(), join() i slice() (pełna lista w dodatku C).

Poeksperymentujmy sobie z metodami tablic:


>>> var a = [3, 5, 1, 7, 'test'];

Metoda push() dodaje element na koniec tablicy, zaś pop() usuwa ostatni element. Wywoł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

[3, 5, 1, 7, "test", "new"]


>>> a.pop()

"new"
>>> a

[3, 5, 1, 7, "test"]

Metoda sort() sortuje elementy tablicy, a także zwraca wynik sortowania. W poniższym 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 ');

"1 to nie 3 to nie 5 to nie 7 to nie test"

slice()zwraca fragment tablicy bez wprowadzania modyfikacji do oryginalnego obiektu. Pierw-


szym parametrem jest indeks początkowy (indeks pierwszego elementu, który ma zostać zwróco-
ny) , drugim indeks końcowy — oba liczone od zera.
>>> b = a.slice(1, 3);

[3, 5]
>>> b = a.slice(0, 1);

[1]
>>> b = a.slice(0, 2);

[1, 3]

Oryginalna tablica nie uległa zmianie:


>>> a

[1, 3, 5, 7, "test"]

121
JavaScript. Programowanie obiektowe

Metoda splice()dla odmiany zmienia tablicę, na której jest wywoływana. Usuwa ona fragment
tablicy, zwraca go oraz, opcjonalnie, wypełnia powstałą lukę nowymi elementami. Pierwsze dwa
parametry to indeksy początkowy i końcowy, pozostałe to nowe wartości.
>>> b = a.splice(1, 2, 100, 101, 102);

[3, 5]
>>> a

[1, 100, 101, 102, 7, "test"]

Wypełnianie luki nowymi elementami nie jest obowiązkowe — można z niego zrezygnować:
>>> a.splice(1, 3)

[100, 101, 102]


>>> a

[1, 7, "test"]

Function
Wiesz już, że funkcje są pewnym specjalnym typem danych. Okazuje się jednak, że są czymś
więcej — są obiektami. Istnieje wbudowany konstruktor Function, który pozwala tworzyć
funkcje w odmienny od pokazanego wcześniej (aczkolwiek niezalecany) sposób.

Istnieją trzy równoważne sposoby definiowania funkcji:


>>> function suma(a, b) {return a + b;};
>>> suma(1, 2)

3
>>> var suma = function(a, b) {return a + b;};
>>> suma(1, 2)

3
>>> var suma = new Function('a', 'b', 'return a + b;');
>>> suma(1, 2)

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

>>> var pierwsza = new Function('a, b, c, d', 'return arguments;');


>>> pierwsza(1,2,3,4);

[1, 2, 3, 4]
>>> var druga = new Function('a, b, c', 'd', 'return arguments;');
>>> druga(1,2,3,4);

[1, 2, 3, 4]
>>> var trzecia = new Function('a', 'b', 'c', 'd', 'return arguments;');
>>> trzecia(1,2,3,4);

[1, 2, 3, 4]

Dobra rada
Nie używaj konstruktora Function(). Należy unikać wszelkich funkcji, które jako argument pobierają
kod w postaci łańcucha znaków. Do tej samej grupy należą funkcje setTimeout() (która jeszcze nie
pojawiła się w tej książce) oraz eval().

Własności obiektu Function


Jak wszystkie inne obiekty, funkcje posiadają pole constructor, które zawiera referencję do
konstruktora Function().
>>> function myfunc(a){return a;}
>>> myfunc.constructor

Function()

Funkcje posiadają także pole length, które określa liczbę parametrów przyjmowanych przez
funkcję.
>>> function myfunc(a, b, c){return true;}
>>> myfunc.length

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"

Więcej o polu prototype dowiesz się z następnego rozdziału.

124
Rozdział 4. • Obiekty

Metody obiektu Function


Obiekty będące funkcjami również są potomkami obiektu Object, dlatego posiadają metody
domyślne, takie jak toString(). toString() wywołana na obiekcie będącym funkcją zwróci jej
kod źródłowy.
>>> function myfunc(a, b, c) {return a + b + c;}
>>> myfunc.toString()

"function myfunc(a, b, c) {
return a + b + c;
}"

Jeśli spróbujesz zajrzeć w kod funkcji wbudowanych, otrzymasz niezbyt przydatny łańcuch
znaków [native code]:
>>> eval.toString()

"function eval() {
[native code]
}"

Ważnymi metodami obiektów funkcyjnych są call() i apply(). Dzięki nim obiekty mogą 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.

Załóżmy, że mamy obiekt obiekt, który posiada metodę mow():


var obiekt= {
imie: 'Ninja',
mow: function(kto){
return 'Siema ' + kto + ', jestem ' + this.imie;
}
}

Możesz wywołać metodę mow(), która sięga do this.name w celu pobrania wartości własnego
pola.
>>> some_obj.say('stary');

"Siema stary, jestem Ninja"

Utwórzmy teraz prosty obiekt moj_obiekt, który posiada jedynie pole imie:
>>> mój_obiekt = {imie: 'Programistyczny guru'};

Powiedzmy, że moj_obiekt tak bardzo lubi mow(), że chce wywołać ją jako swoją własną metodę.
Jest to możliwe przy użyciu metody call() obiektu funkcyjnego mow():

125
JavaScript. Programowanie obiektowe

>>> obiekt.mow.call(mój_obiekt, 'stary');

"Siema stary, jestem Programistyczny guru"

Działa! Co dokładnie zaszło? Wywołaliśmy metodę call() obiektu mow(), przekazując jej dwa
parametry: obiekt moj_obiekt oraz łańcuch 'stary'. W wyniku tego podczas wywołania mow()
wszystkie referencje do wartości this wskazywały mój_obiekt. Dzięki temu this.name nie
zwróciło 'Ninja', tylko 'Guru programistyczny'.

Jeśli dana funkcja pobiera więcej argumentów, po prostu je wymieniamy:


obiekt.metoda.call(mój_obiekt, 'a', 'b', 'c');

Jeśli jako pierwszy parametr call() nie zostanie przekazany obiekt lub jeśli przekaże się null,
funkcja zostanie wywołana na rzecz obiektu globalnego.

Metoda apply() działa tak jak call(), z tą różnicą, że wszystkie parametry przekazywane
metodzie innego obiektu umieszcza się w tablicy. Poniższe dwie linie są równoważne:
obiekt.metoda.apply(moj_obiekt, ['a', 'b', 'c']);
obiekt.metoda.call(mój_obiekt, 'a', 'b', 'c');

Kontynuując powyższy przykład, możesz spróbować wykonać następujący kod:


>>> obiekt.metoda.apply(mój obiekt, [stary']);

"Siema stary, jestem Programistyczny guru"

Nowe spojrzenie na obiekt arguments


W poprzednim rozdziale pokazałem, jak z wnętrza funkcji uzyskać dostęp do zmiennej o na-
zwie arguments, która przechowuje wartości wszystkich parametrów użytych podczas wywo-
łania funkcji:
>>> function f() {return arguments;}
>>> f(1,2,3)

[1, 2, 3]

arguments wygląda jak tablica, jednak w rzeczywistości jest to obiekt tablicopodobny. Przypomina
tablicę, ponieważ posiada indeksowane elementy oraz pole length. Na tym jednak podobień-
stwa się kończą — arguments nie posiada metod tablicowych, takich jak sort() czy slice().

Obiekt ten posiada jednak inną ciekawą własność: pole callee. Zawiera ono referencję do 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

Dzięki arguments.callee funkcje anonimowe mogą rekurencyjnie wywoływać same siebie.


Oto przykład:
(
function(count){
if (count < 5) {
alert(count);
arguments.callee(++count);
}
}
)(1)

Widać tu funkcję anonimową, która pobiera parametr count, wyświetla go, a następnie wywołuje
samą siebie ze zwiększoną wartością count. Cała funkcja została umieszczona w nawiasie, po
którym następuje pusty nawias powodujący, że funkcja od razu jest wykonywana z wartością
początkową 1. Uruchomienie kodu spowoduje wyświetlenie czterech okienek dialogowych,
prezentujących liczby 1, 2, 3 i 4.

Boolean
Kontynuując naszą podróż przez wbudowane obiekty JavaScriptu, dochodzimy do mało
skomplikowanej grupy, w której skład wchodzą obiekty opakowujące proste typy danych (typ
logiczny boolean, liczba, łańcuch znaków).

Typ boolean po raz pierwszy pojawił się w rozdziale 2. Teraz przyszła pora na spotkanie z 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

Obiekty utworzone za pomocą konstruktora Boolean() nie są specjalnie przydatne, jako że


obiekt ten zawiera jedynie odziedziczone metody.

127
JavaScript. Programowanie obiektowe

Inaczej ma się sprawa z Boolean() wywoływanym jako normalna funkcja, a nie konstruktor —
czyli bez użycia new. W ten sposób można zamienić na typ logiczny wartość należącą do 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

>>> var n = Number('12.12');


>>> n

12.12
>>> typeof n

"number"
>>> var n = new Number('12.12');
>>> typeof n

"object"

Skoro funkcje są obiektami, mogą posiadać pola i metody. Funkcja Number() ma kilka 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

Obiekt liczbowy posiada trzy metody: toFixed(), toPrecision() i toExponential() (szczegóły


w dodatku C).
>>> var n = new Number(123.456)
>>> n.toFixed(1)

"123.5"

Warto wiedzieć, że do korzystania z tych metod nie jest konieczne jawne utworzenie obiektu
liczbowego. Można wywołać je na rzecz wartości liczbowej, która zostanie automatycznie 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

>>> var n = new Number(255);


>>> n.toString();
"255"
>>> n.toString(10);
"255"
>>> n.toString(16);
"ff"
>>> (3).toString(2);

"11"
>>> (3).toString(10);
"3"

String
Konstruktor String() służy do tworzenia obiektów przechowujących łańcuchy znaków. 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.

W poniższym przykładzie widzimy automatyczną zamianę łańcucha na odpowiedni obiekt:


>>> "ziemniak".length

8
>>> "pomidor"[0]

"p"
>>> "ziemniak"["ziemniak".length - 1]

"k"

Ostatni przykład prezentujący różnicę pomiędzy obiektem przechowującym łańcuch znaków


a łańcuchem będącym prostym typem danych: zamieńmy oba na boolean. Pusty łańcuch jest
wartością fałszywą, natomiast wszystkie obiekty zostaną zamienione na true.
>>> Boolean("")

false
>>> Boolean(new String(""))

true

Podobnie jak w przypadku Number() i Boolean(), funkcja String() użyta bez operatora new
zamieni parametr na typ prosty. Jeśli wartość wejściowa będzie obiektem, zostanie wywołana
funkcja toString().
>>> String(1)

"1"

131
JavaScript. Programowanie obiektowe

>>> String({p: 1})

"[object Object]"
>>> String([1,2,3])

"1,2,3"

Ciekawe metody obiektu String


Poeksperymentujmy sobie z metodami, które można wywołać na obiektach reprezentujących
łańcuchy znaków (pełna lista tych metod znajduje się w dodatku C).
Zacznijmy od utworzenia obiektu:
>>> var s = new String("Guru programowania");

Metody toUpperCase() i toLowerCase() pozwalają zmienić wielkość liter:


>>> s.toUpperCase()

"GURU PROGRAMOWANIA"
>>> s.toLowerCase()

"guru programowania"

Metoda charAt() zwraca znak znajdujący się na określonej pozycji (ten sam efekt da użycie
nawiasu kwadratowego, ponieważ obiekt przechowujący łańcuch znaków przypomina tablicę
znaków).
>>> s.charAt(0);

"G"
>>> s[0]

"G"

Odwołanie się do nieistniejącej pozycji zwróci pusty łańcuch:


>>> s.charAt(101)

""

Metoda indexof() pozwala na przeszukiwanie łańcuchów. Jeśli istnieje chociaż jedno 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')

Jeśli fragment nie zostanie znaleziony, zwracana jest wartość -1:


>>> s.indexOf('guru')

-1

Jeśli chcesz pominąć kwestię wielkości liter, możesz przed rozpoczęciem wyszukiwania 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) {...}

Metody slice() i substring() zwracają fragment łańcucha ograniczony za pomocą argumen-


tów rozumianych jako pozycje początkowa i końcowa.
>>> s.slice(5, 12)

"program"

133
JavaScript. Programowanie obiektowe

>>> s.substring(5, 12)

"program"

Należy pamiętać, że drugi parametr to pozycja końcowa, a nie długość fragment łańcucha.
Pokazane powyżej dwie metody różnią się sposobem interpretacji argumentów ujemnych.
substring() potraktuje je jak zera, podczas gdy slice() doda je do długości łańcucha. Zatem
przekazanie wartości (1, -1) zostanie zrozumiane jako substring(1, 0) i slice(1, s.length-1):
>>> s.slice(1, -1)

"uru programowani"
>>> s.substring(1, -1)

"G"

Metoda split() zamienia obiekt na tablicę, traktując parametr jako wartość rozdzielającą:
>>> s.split(" ")

["Guru", "programowania"]

Przeciwieństwem split() jest metoda join(), która zamienia tablicę w obiekt typu String:
>>> s.split(' ').join(' ');

"Guru programowania"

Metoda concat() skleja łańcuchy, podobnie jak operator + dla typu prostego:
>>> s.concat(" w języku JavaScript")

"Guru programowania w języku JavaScript"

Zwróć uwagę na to, że wszystkie omawiane powyżej metody związane z łańcuchami zwracają
wartości proste i nie modyfikują obiektu źródłowego. Po wywołaniu wszystkich tych metod
łańcuch znaków przechowywany przez obiekt nie uległ zmianie:
>>> s.valueOf()

"Guru programowania"

Do przeszukiwania łańcuchów używaliśmy indexOf() oraz lastIndexOf(). Istnieją bardziej


zaawansowane metody (search(), match() i replace()), które jako parametry pobierają wyra-
żenia regularne. Opowiem o nich później, gdy dojdziemy do konstruktora RegExp().

To już wszystkie obiekty opakowujące dane. Zajmiemy się teraz użytkowymi obiektami Math,
Date i RegExp.

134
Rozdział 4. • Obiekty

Math
Math różni się nieco od innych wbudowanych obiektów globalnych. Jest zwykłą funkcją i w zwią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

Logarytm naturalny z 10:


>>> Math.LN10

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

Obiekt, któremu przypisana zostanie aktualna data i godzina:


>>> new Date()

Tue Jan 08 2008 01:10:42 GMT+0100

Jak zwykle w przypadku obiektów konsola Firebug wyświetla wynik wywołania metody
toString().

Poniżej przedstawiam kilka przykładów użycia łańcuchów znaków do inicjalizacji obiektu prze-
chowującego datę. Można wybierać spomiędzy wielu różnych formatów:
>>> new Date('2009 11 12')

Thu Nov 12 2009 00:00:00 GMT+0100


>>> new Date('1 1 2012')

Sun Jan 01 2012 00:00:00 GMT+0100


>>> new Date('1 mar 2012 5:30')

Thu Mar 01 2012 05:30:00 GMT+0100

JavaScript potrafi odczytać datę z łańcuchów znaków w różnym formacie, jednak nie jest to
najskuteczniejszy sposób precyzyjnego definiowania daty. Lepiej jest przekazać 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.

Podanie wszystkich wspomnianych parametrów:


>>> new Date(2008, 0, 1, 17, 05, 03, 120)

Tue Jan 01 2008 17:05:03 GMT+0100

Podanie tylko daty i godziny:


>>> new Date(2008, 0, 1, 17)

Tue Jan 01 2008 17:00:00 GMT+0100

137
JavaScript. Programowanie obiektowe

Postaraj się zapamiętać, że miesiące liczone są od 0, zatem 1 oznacza luty:


>>> new Date(2008, 1, 28)

Thu Feb 28 2008 00:00:00 GMT+0100

Jeśli podasz zbyt dużą wartość, zostanie ona przetłumaczona na odpowiednią datę w przyszło-
ści. Przykładowo ponieważ w roku 2008 nie było dnia 30 lutego, taka wartość zostanie przetłuma-
czona na 1 marca (2008 był rokiem przestępnym).
>>> new Date(2008, 1, 29)

Fri Feb 29 2008 00:00:00 GMT+0100


>>> new Date(2008, 1, 30)

Sat Mar 01 2008 00:00:00 GMT+0100

W analogiczny sposób 32 grudnia zostanie zamieniony na 1 stycznia następnego roku:


>>> new Date(2008, 11, 31)

Wed Dec 31 2008 00:00:00 GMT+0100


>>> new Date(2008, 11, 32)

Thu Jan 01 2009 00:00:00 GMT+0100

Obiekt reprezentujący datę można jeszcze zainicjalizować za pomocą znacznika czasu, czyli
liczby milisekund od początku ery Uniksa (gdzie 0 milisekund oznacza 1 stycznia 1970).
>>> new Date(1199865795109)

Wed Jan 09 2008 00:03:15 GMT+0100

Jeśli funkcja Date() zostanie wywołana bez operatora new, zwróci łańcuch znaków 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()

"Thu Jan 17 2008 23:11:32 GMT+0100"


>>> Date(1, 2, 3, "bez znaczenia");

"Thu Jan 17 2008 23:11:35 GMT+0100"

Metody działające na obiektach Date


Istnieje wiele metod, które można wywołać na obiektach Date. Większość z nich to metody
dostępowe get*() (ustawiające wartość atrybutu) lub set*() (pobierające wartość). Mamy na
przykład getMonth() (pobierz miesiąc), setMonth() (ustaw miesiąc), getHours() (pobierz godzinę),
setHours() (ustaw godzinę) itd.

138
Rozdział 4. • Obiekty

Utwórzmy obiekt:
>>> var d = new Date();
>>> d.toString();

"Wed Jan 09 2008 00:26:39 GMT+0100"

Ustawienie miesiąca na marzec (miesiące liczymy od 0):


>>> d.setMonth(2);

1205051199562
>>> d.toString();

"Sun Mar 09 2008 00:26:39 GMT+0100"

Pobranie miesiąca:
>>> d.getMonth();

Poza metodami należącymi do instancji obiektu Date istnieją jeszcze dwie metody będące
polami funkcji/obiektu Date(). Do ich funkcjonowania nie jest potrzebna instancja danych —
działają tak jak metody Math. W językach, w których istnieje pojęcie klasy, tego typu metody
nazywa się statycznymi.

Metoda Date.parse() zamienia tekst na znacznik czasu:


>>> Date.parse('Jan 1, 2008')

1199174400000

Date.UTC() pobiera parametry określające rok, miesiąc, dzień itd. i zamienia je na znacznik
czasu uniwersalnego:
>>> Date.UTC(2008, 0, 1)

1199145600000

Skoro konstruktor new Date() przyjmuje znaczniki czasu, można przekazać mu wynik wywo-
łania Date.UTC. Poniższy przykład dowodzi, że UTC() podaje czas uniwersalny, a Date() czas
lokalny:
>>> new Date(Date.UTC(2008, 0, 1));

Mon Dec 31 2007 16:00:00 GMT+0100


>>> new Date(2008, 0, 1);

Tue Jan 01 2008 00:00:00 GMT+0100

139
JavaScript. Programowanie obiektowe

Spójrzmy jeszcze na ostatni przykład działań na obiekcie typu Date. Ciekawiło mnie, w jaki
dzień tygodnia wypadną moje urodziny w roku 2012:
>>> var d = new Date(2012, 5, 20);
>>> d.getDay();

Dni tygodnia liczymy od 0 (niedziela), zatem 3 powinno oznaczać środę:


>>> d.toDateString();

"Wed Jun 20 2012"

Zgadza się, będzie to środa (ang. Wednesday) — nie jest to najlepszy dzień na imprezę. W 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++) {

stats[new Date(i, 5, 20).getDay()]++;


}

Wynik:
>>> stats;

[139, 145, 139, 146, 143, 143, 145]

143 piątki i 145 sobót. Tak jest!

RegExp
Wyrażenia regularne (ang. regular expressions) są niezwykle potężnym mechanizmem 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.

Wyrażenia regularne w skrócie określa się mianem „regex” lub „regexp”.

Wyrażenie regularne składa się z:


Q wzorca, do którego ma zostać dopasowany tekst;
Q nieobowiązkowych modyfikatorów (nazywanych także flagami), które wpływają na
sposób stosowania wzorca.

Wzorzec może być zwykłym fragmentem tekstu, który ma zostać dokładnie dopasowany, ale
takie zastosowania wyrażeń regularnych spotyka się rzadko, tym bardziej że do osiągnięcia tego
celu wystarczy zastosować indexOf(). W większości przypadków wzorzec jest dość złożony i 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.

Konstruktor RegExp() pozwala tworzyć obiekty reprezentujące wyrażenia regularne.


>>> var re = new RegExp("j.*t");

Obiekty te można w nieco wygodniejszy sposób tworzyć za pomocą literałów:


>>> var re = /j.*t/;

j.*t w powyższym przykładzie jest wzorcem wyrażenia regularnego. Oznacza „znajdź takie
łańcuchy, które zaczynają się od j i kończą się na t, a pomiędzy nimi występuje 0 lub więcej
dowolnych znaków”. Kropka (.) oznacza dowolny znak. Jeśli wzorzec jest przekazywany kon-
struktorowi RegExp(), należy umieścić go w cudzysłowie.

Pola obiektów RegExp


Obiekty reprezentujące wyrażenia regularne posiadają następujące pola:
Q global: jeśli to pole ma wartość false (domyślną), w wyniku wyszukiwania
zwrócony zostanie tylko pierwszy odnaleziony wynik. Jeśli chcesz otrzymać
wszystkie dopasowania, zmień wartość pola na true.
Q ignoreCase: true oznacza, że nie są rozróżniane wielkie i małe litery. Wartość
domyślna tego pola to false.
Q multiline: wartość true pozwala na wyszukiwanie dopasowań, które zajmują
więcej niż jedną linię. Wartością domyślną jest false.
Q lastIndex: pozycja, od której ma się rozpocząć wyszukiwanie — domyślnie 0.
Q source: przechowuje wzorzec wyrażenia regularnego.

141
JavaScript. Programowanie obiektowe

Pole lastIndex jest jedynym, którego wartość można zmieniać po utworzeniu obiektu.

Pierwsze trzy parametry to modyfikatory wyrażenia regularnego. Podczas tworzenia obiektu


wyrażenia za pomocą konstruktora można jako drugi parametr przekazać dowolną kombinację
poniższych znaków:
Q "g" dla global,
Q "i" dla ignoreCase,
Q "m" dla multiline.

Kolejność liter nie ma znaczenia. Przekazanie danej litery powoduje ustawienie wartości 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

Po utworzeniu obiektu nie można już zmienić wartości modyfikatora:


>>> re.global = false;
>>> re.global

true

Jeśli obiekt jest tworzony za pomocą literału, modyfikatory podaje się po końcowym ukośniku.
>>> var re = /j.*t/ig;
>>> re.global

true

Metody obiektów RegExp


Obiekty RegExp oferują dwie metody służące do znajdowania fragmentów tekstu: test() i exec().
Obie przyjmują parametr tekstowy. test() zwraca wartość logiczną (true, jeśli znaleziono do-
pasowanie, i false w przeciwnym przypadku), natomiast exec() — tablicę dopasowanych
łańcuchów znaków. Oczywiście exec() wykonuje bardziej skomplikowane obliczenia, dlatego
o ile nie są Ci potrzebne konkretne dopasowania, korzystaj z test(). Najczęstsze zastosowa-
nie wyrażeń regularnych to walidacja formularzy — do tego test w zupełności wystarczy.

Brak dopasowania z powodu różnicy w wielkości liter:


>>> /j.*t/.test("Javascript")

false

142
Rozdział 4. • Obiekty

Ustawienie wartości ignoreCase na true powoduje, że uda się odnaleźć pasujący tekst:
>>> /j.*t/i.test("Javascript")

true

To samo zapytanie zadane za pomocą exec() zwraca tablicę. Sprawdźmy wartość jej pierw-
szego elementu:
>>> /j.*t/i.exec("Javascript")[0]

"Javascript"

Metody obiektu String, których parametrami mogą być wyrażenia regularne


Wcześniej w tym rozdziale opowiadałem o obiekcie String i o wykorzystaniu jego metod
indexOf() i lastIndexOf() do przeszukiwania tekstu. Przy ich użyciu można odnajdować w tekście
fragmenty przekazane w postaci parametrów. Więcej możliwości daje przeszukiwanie tekstu
przy użyciu wyrażeń regularnych. Obiekty String umożliwiają również to.

Obiekty tekstowe posiadają następujące metody przyjmujące jako parametry wyrażenia 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');

match() zwróci tablicę zawierającą tylko pierwsze dopasowanie:


>>> s.match(/a/);

["a"]

Jeśli skorzystamy z modyfikatora g, wyszukiwanie będzie miało zasięg globalny i otrzymamy


tablicę zawierającą dwa elementy:
>>> s.match(/a/g);

["a", "a"]

143
JavaScript. Programowanie obiektowe

Pominięcie różnic w wielkości liter:


>>> s.match(/j.*a/i);

["Java"]

Metoda search() zwraca pozycję dopasowanego łańcucha:


>>> s.search(/j.*a/i);

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"

Jeśli wyrażenie regularne zawiera grupy (oznaczone nawiasami), do poszczególnych grup


można się dostać dzięki sekwencjom: $1 dla pierwszej grupy, $2 dla drugiej itd.
>>> s.replace(/([A-Z])/g, "_$1");

"_Hello_Java_Script_World"

Wyobraź sobie, że na Twojej stronie znajduje się formularz, w który użytkownik powinien wpisać
swój adres e-mail, nazwę użytkownika i hasło. Gdy tylko wpisze e-mail, nasz skrypt zasugeruje
nazwę użytkownika w oparciu o ten adres:
>>> var email = "[email protected]";
>>> var nazwa_uzytkownika = email.replace(/(.*)@.*/, "$1");
>>> nazwa_uzytkownika;

"stoyan"

144
Rozdział 4. • Obiekty

Wywołania zwrotne replace


Podczas zamiany fragmentów tekstu na inne można zamiast konkretnego tekstu podać funkcję
zwracającą łańcuch, która w odpowiedni sposób przetworzy odnaleziony tekst.
>>> function replaceCallback(match){return "_" +
match.toLowerCase();}
>>> s.replace(/[A-Z]/g, replaceCallback);

"_hello_java_script_world"

W rzeczywistości funkcja otrzymuje kilka parametrów (w powyższym przykładzie zignorowa-


liśmy wszystkie oprócz pierwszego):
Q Pierwszy parametr to dopasowany tekst.
Q Drugi to przeszukiwany łańcuch.
Q Przedostatni informuje o pozycji dopasowania.
Q Pozostałe parametry (jeśli jest ich więcej niż jeden, część z nich pojawi się w tablicy
przed informacją o pozycji dopasowania) zawierają fragmenty dopasowane
do poszczególnych grup z wzorca.

Przetestujmy to. Po pierwsze, utwórzmy zmienną, która będzie przechowywała tablicę argu-
mentów przekazanych podczas wywołania funkcji:
>>> var glob;

Następnie zdefiniujmy wyrażenie regularne z trzema grupami, które ma pasować do adresów


e-mail postaci [email protected]:
>>> var re = /(.*)@(.*)\.(.*)/;

Teraz napiszmy funkcję, która przechowa wartości i zwróci postać adresu nieczytelną dla botów:
var callback = function(){
glob = arguments;
return arguments[1] + ' na serwerze: ' + arguments[2] + ' kropka ' + arguments[3];
}

Działanie funkcji jest następujące:


>>> "[email protected]".replace(re, callback);

"stoyan na serwerze phpied kropka com"

Oto argumenty, które odebrała funkcja:


>>> glob

["[email protected]", "stoyan", "phpied", "com", 0, "[email protected]"]

145
JavaScript. Programowanie obiektowe

split()
Zapoznałem Cię już z metodą split(), która tworzy tablicę w oparciu o tekst wejściowy i łań-
cuch znaków pełniący funkcję separatora. Rozetnijmy łańcuch składający się z wartości od-
dzielonych przecinkami:
>>> var csv = 'raz, dwa,trzy ,cztery';
>>> csv.split(',');

["raz", " dwa", "trzy ", "cztery"]

Ponieważ w wejściowym tekście spacje nie są stosowane konsekwentnie, w wyniku podziału


otrzymaliśmy tablicę, która także zawiera spacje. Można to naprawić, stosując wyrażenie \s*,
które oznacza „zero lub więcej spacji”:
>>> csv.split(/\s*,\s*/)

["raz", "dwa", "trzy", "cztery"]

Przekazanie zwykłego tekstu zamiast wyrażenia regularnego


Warto zapamiętać, że omówione przed chwilą cztery metody (split(), match(), search()
i replace()) mogą zamiast wyrażeń regularnych pobierać zwykły tekst. W takim wypadku argu-
ment zostanie wykorzystany do utworzenia nowego obiektu wyrażenia regularnego, tak jakby
został przekazany do konstruktora RegExp().

Przykład:
>>> "test".replace('e', 'o')

"tost"

Powyższe wywołanie jest równoważne:


>>> "test".replace(new RegExp('e'), 'o')

"tost"

Jeśli parametr jest łańcuchem, nie można ustawić wartości modyfikatorów. Na określenie ich
wartości pozwala konstruktor RegExp() oraz notacja literałowa.

Obsługa błędów za pomocą obiektów Error


Nie da się całkowicie wyeliminować błędów, dlatego potrzebny jest mechanizm ich wykrywania,
dzięki któremu program będzie mógł wykryć, że coś poszło nie tak, i w elegancki sposób odzyskać
sprawność. Do obsługi błędów w języku JavaScript służą instrukcje try, catch i finally. Kiedy
pojawia się błąd, „rzucany” jest obiekt błędu. Obiekty te tworzy się za pomocą wbudowanych

146
Rozdział 4. • Obiekty

konstruktorów: EvalError, RangeError, ReferenceError, SyntaxError, TypeError i URIError3.


Wszystkie konstruktory dziedziczą z obiektu Error.

Spowodujmy błąd i zobaczmy, co się stanie. Nasz przykład będzie próbował wywołać nieist-
niejącą funkcję. Wpisz w konsoli Firebug następujący kod:
>>> nieMaMnie ();

Wynik będzie mniej więcej taki:

Jeśli strona zawiera błąd, w prawym dolnym rogu przeglądarki zamiast normalnej ikonki Fi-
rebug pojawi się:

Informacje o błędach można przeglądać w konsoli błędów (Narzędzia/Konsola błędów):

Sposób wyświetlania informacji o błędach jest różny w różnych przeglądarkach. Internet


Explorer w lewym dolnym rogu wyświetla następujący komunikat:

Po dwukrotnym kliknięciu otrzymamy więcej informacji (patrz rysunek na następnej stronie).

W zależności od konfiguracji przeglądarki możesz nawet nie zauważyć, że wystąpił błąd. Nigdy
jednak nie będziesz mieć pewności, że wszyscy użytkownicy wyłączyli informowanie o błędach.
Uwolnienie ich od konieczności oglądania komunikatów o błędach na Twojej stronie należy
tylko do Ciebie. Błąd z naszego przykładu został wyświetlony użytkownikowi, ponieważ kod
nie próbował go „przechwycić” i nie był przygotowany na jego obsługę. Na szczęście łapanie
błędów jest naprawdę proste. Potrzeba do tego instrukcji try („spróbuj”), po której nastąpi
instrukcja catch („przechwyć”).

3
Odpowiednio: błąd wykonania, błąd zakresu, błąd referencji, błąd składniowy, błąd typu, błąd adresu
URI — przyp. tłum.

147
JavaScript. Programowanie obiektowe

Poniższy kod nie spowoduje wystąpienia błędów:


try {
nieMaMnie();
} catch (e){
// nic nie rób
}

Mamy tu:
Q Instrukcję try, po której następuje blok kodu.
Q Instrukcję catch, po której następuje nazwa zmiennej w nawiasie i kolejny blok kodu.

Istnieje jeszcze nieobowiązkowa instrukcja finally. Towarzyszący jej blok kodu jest 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).

Uruchom teraz następujący kod:


try {
nieMaMnie();
} catch (e){
alert(e.name + ': ' + e.message);
} finally {
alert('Wreszcie!');
}

148
Rozdział 4. • Obiekty

Pojawi się okienko alert() pokazujące nazwę błędu i komunikat, a potem drugie okienko
o treści „Wreszcie!”.

W Firefoksie pierwsze okienko wyświetli tekst ReferenceError: nieMaMnie is not defined.


W Internet Explorerze będzie to TypeError: Oczekiwano obiektu. Na tej podstawie możemy
wywnioskować dwa fakty:
Q e.name przechowuje nazwę konstruktora, który został użyty podczas tworzenia obiektu
błędu.
Q Skoro w różnych przeglądarkach ten sam błąd w kodzie jest wiązany z różnymi
obiektami błędów, nie jest dobrym pomysłem podejmowanie decyzji na temat
zachowania kodu na podstawie typu błędu (tzn. wartości e.name).

Nowe obiekty błędów można tworzyć samodzielnie za pomocą konstruktora new Error() lub
dowolnego z dziedziczących z niego konstruktorów. Wystąpienie nowego błędu w kodzie 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.

Niezależnie od istnienia funkcji mozeIstnieje() i od zwracanej przez nią wartości, na końcu


pojawi się okno dialogowe z komunikatem Wreszcie!.

149
JavaScript. Programowanie obiektowe

Zamiast rzucania ogólnego błędu throw new Error('Dzielenie przez zero! ') możesz 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.

Number(), String() i Boolean() można wywołać:


Q z operatorem new — w celu utworzenia nowego obiektu;
Q bez new — w celu przekształcenia dowolnej wartości na odpowiadającą jej wartość
prostą.

Inne omówione w tym rozdziale wbudowane konstruktory to: Object(), Array(), Function(),
Date(), RegExp()i Error(). Opisałem także funkcję Math, która jednak nie jest konstruktorem.

Wiesz już, że obiekty odgrywają w języku JavaScript podstawową rolę. Prawie wszystko albo
jest obiektem, albo może zostać opakowane w obiekt.

150
Rozdział 4. • Obiekty

Podsumujmy jeszcze sposoby tworzenia obiektów za pomocą notacji literałowej:

Nazwa Literał Konstruktor Przykład


Object {} new Object() {prop:1}
Array [] new Array() [1,2,3,'test']
wyrażenie /wzorzec/modyfikatory new RegExp('wzorzec', 'modyfikatory') /java.*/img
regularne

Ćwiczenia
1. Do którego z obiektów (globalnego czy obiektu o) odnosi się wartość this w poniższym
kodzie?
function F() {
function C() {
return this;
}
return C();
}
var o = new F();
2. Jaki będzie wynik wykonania poniższego fragmentu kodu?
function C(){
this.a = 1;
return false;
}
console.log(typeof new C());
3. A jaki będzie wynik wykonania tego fragmentu?
>>> c = [1,. 2, [1, 2]];
>>> c.sort();
>>> c.join('--');
>>> console.log(c);
4. Wyobraź sobie, że nie istnieje konstruktor String(). Utwórz konstruktor MojString(),
którego działanie będzie tak bliskie działaniu String(), jak to tylko możliwe. Nie wolno
Ci używać wbudowanych pól i metod obiektu String i pamiętaj, że nie istnieje
String(). Sprawdź działanie kodu przy użyciu następującego testu:
>>> var s = new MojString('hello');
>>> s.length;

5
>>> s[0];

"h"

151
JavaScript. Programowanie obiektowe

>>> s.toString();

"hello"
>>> s.valueOf();

"hello"
>>> s.charAt(1);

"e"
>>> s.charAt('2');

"l"
>>> s.charAt('e');

"h"
>>> s.concat(' world!');

"hello world!"
>>> s.slice(1,3);

"el"
>>> s.slice(0,-1);

"hell"
>>> s.split('e');

["h", "llo"]
>>> s.split('l');

["he", "", "o"]

Możesz przejść przez wszystkie znaki łańcucha wejściowego za pomocą pętli for…in, traktując go jak
tablicę.

5. Dodaj do konstruktora MojString() metodę reverse() („odwróć”).

Wykorzystaj fakt, że tablice posiadają metodę reverse().

6. Wyobraź sobie, że nie istnieje ani Array(), ani literałowy sposób tworzenia tablic.
Napisz konstruktor MojArray(), który zachowuje się w prawie taki sam sposób jak
Array(). Przeprowadź następujące testy:
>>> var a = new MojArray(1,2,3,"test");
>>> a.toString();

"1,2,3,test"

152
Rozdział 4. • Obiekty

>>> a.length;

4
>>> a[a.length - 1]

"test"
>>> a.push('boo');

5
>>> a.toString();

"1,2,3,test,boo"
>>> a.pop();

[1, 2, 3, "test"]
>>> a.toString();

"1,2,3,test"
>>> a.join(',')

"1,2,3,test"
>>> a.join(' to nie ')

"1 to nie 2 to nie 3 to nie test"


Jeśli podoba Ci się to ćwiczenie, nie poprzestawaj na join(), ale zaimplementuj
również inne metody.

7. Wyobraź sobie, że nie istnieje Math. Utwórz obiekt MojMath, który posiada
następujące metody:
Q MojMath.rand(min, max, wlacznie) — losuje liczbę z przedziału od min do max,
włącznie, jeśli wlacznie ma wartość true.
Q MojMath.min(tablica) — zwraca najmniejszy element tablicy.
Q MojMath.max(tablica) — zwraca największy element tablicy.

153
JavaScript. Programowanie obiektowe

154
5

Prototypy

W tym rozdziale omówię pole prototype obiektów funkcyjnych. JavaScript jest prototypowym
językiem obiektowym, więc zrozumienie idei prototypów jest bardzo ważne. Sam prototyp
nie jest niczym specjalnie skomplikowanym, jednak jest to nowe pojęcie, z którym po prostu
trzeba się oswoić. Prototypy to kolejny (po domknięciach) element JavaScriptu, który — 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.

W rozdziale poruszone zostały następujące tematy:


Q pole prototype i przechowywany w nim obiekt;
Q dodawanie pól do obiektu prototype;
Q korzystanie z pól dodanych do prototypu;
Q różnica pomiędzy własnymi polami obiektu a polami prototypu;
Q __proto__, czyli ukryte powiązanie obiektu z jego prototypem;
Q metody isPrototypeOf(), hasOwnProperty() oraz propertyIsEnumerable();
Q rozszerzanie obiektów wbudowanych, takich jak tablice i łańcuchy znaków.

Pole prototype
Funkcje w języku JavaScript są obiektami posiadającymi pola i metody. Niektóre z nich już
znasz, na przykład metody apply() i call()oraz pola length i constructor. prototype jest 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"

Jawne ustawienie wartości pola da ten sam efekt:


>>> foo.prototype = {}

Możesz rozszerzać obiekt przechowywany w prototype o funkcje i metody. Nie będą one
miały żadnego wpływu na działanie funkcji foo() — zostaną użyte tylko podczas wywołania
foo() jako konstruktora.

Dodawanie pól i metod przy użyciu prototypu


W poprzednim rozdziale pokazałem, jak tworzyć funkcje będące konstruktorami nowych
obiektów. Wewnątrz takiej funkcji, wywoływanej z operatorem new, programista ma dostęp do
wartości this zawierającej obiekt, który zostanie zwrócony przez konstruktor. Rozszerzając
ten obiekt (czyli dodając do niego pola i metody), dodajemy funkcjonalności do nowo tworzo-
nego obiektu.

Spójrzmy na konstruktor Gadget(), który przy użyciu this dodaje dwa pola i jedną metodę do
tworzonego obiektu.
function Gadget(name, color) {
this.nazwa = nazwa;
this.kolor = kolor;
this.ktosTy = funkcja(){
return 'Jam ' + this.kolor + ' ' + this.nazwa;
}
}

Innym sposobem rozszerzania nowo tworzonych obiektów jest dodawanie pól i metod do pola
prototype konstruktora. Dołóżmy do obiektu jeszcze dwa pola, cena i ocena_uzytkownikow,
oraz metodę informuj(). Ponieważ prototype zawiera obiekt, można dodać własności klasy
w następujący sposób:

156
Rozdział 5. • Prototypy

Gadget.prototype.cena = 100;
Gadget.prototype.ocena_uzytkownikow = 3;
Gadget.prototype.informuj = function() {
return 'Ocena_uzytkownikow: ' + this.ocena_uzytkownikow + ', cena: ' +
this.cena;
};

Można także zrezygnować z dodawania własności do obiektu prototype i zamiast tego całko-
wicie go nadpisać innym obiektem:
Gadget.prototype = {
cena: 100,
ocena_uzytkownikow: 3,
informuj: function() {
return 'Ocena_uzytkownikow: ' + this.ocena_uzytkownikow + ', cena: ' +
this.cena;
}
};

Korzystanie z pól i metod obiektu prototype


Pola i metody dodane do prototypu stają się dostępne zaraz po utworzeniu nowego obiektu
przy użyciu danego konstruktora. Jeśli przy użyciu konstruktora Gadget() stworzysz obiekt
nowaZabawka, możliwe będzie sięgnięcie do wszystkich zdefiniowanych wcześniej pól i metod.
>>> var nowaZabawka = new Gadget('kamera', 'czarna');
>>> nowaZabawka.nazwa;

"kamera"
>>> nowaZabawka.kolor;

"czarna"
>>> nowaZabawka.ktosTy();

"Jam czarna kamera"


>>> nowaZabawka.cena;

100
>>> nowaZabawka.ocena_uzytkownikow;

3
>>> nowaZabawka.informuj();

"Ocena użytkownków: 3, cena: 100"

157
JavaScript. Programowanie obiektowe

Nie wolno zapominać o tym, że obiekty są przekazywane przez referencję, zatem zmiana prototy-
pu pociąga za sobą zmiany we wszystkich dziedziczących z niego obiektach, nawet tych utwo-
rzonych wcześniej.

Dodajmy do prototypu nową metodę:


Gadget.prototype.pobierz = function(co) {
return this[co];
};

Pomimo tego, że nowaZabawka została utworzona przed zdefiniowaniem metody pobierz(),


obiekt ma do niej dostęp::
>>> nowaZabawka.pobierz('cena');

100
>>> nowaZabawka.pobierz('kolor');

"czarna"

Własne pola obiektu a pola prototypu


W poprzednim przykładzie metoda informuj() korzystała z this w celu uzyskania dostępu do
obiektu. Ten sam wynik dałoby odwołanie się do Gadget.prototype:
Gadget.prototype.informuj = function() {
return 'Ocena użytkowników: ' + Gadget.prototype.ocena_uzytkownikow + ',
cena: ' + Gadget.prototype.cena;
};

Czy to rozwiązanie różni się czymś od poprzedniego? By móc poprawnie odpowiedzieć na to


pytanie, przyjrzymy się, jak dokładnie działa prototyp.

Jeszcze raz utwórzmy obiekt nowaZabawka:


>>> var nowaZabawka = new Gadget('kamera', 'czarna');

Jeśli spróbujesz pobrać wartość któregoś z pól obiektu nowaZabawka, na przykład nowaZabawka.
´nazwa, JavaScript przejdzie przez wszystkie pola obiektu w poszukiwaniu pola o nazwie nazwa.
Jeśli znajdzie takie pole, zwróci jego wartość.
>>> nowaZabawka.nazwa

"kamera"

Co stanie się, jeśli zapragniesz sięgnąć do pola ocena_uzytkownikow? Pola nie uda się 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

>>> nowaZabawka. ocena_uzytkownikow


3

Ten sam efekt dałoby bezpośrednie sięgnięcie do prototypu. Każdy obiekt posiada pole constructor
będące referencją do funkcji, która utworzyła obiekt. W naszym przypadku:
>>> nowaZabawka.constructor

Gadget(nazwa, kolor)
>>> nowaZabawka.constructor.prototype.ocena_uzytkownikow
3

Przejdźmy teraz na wyższy poziom. Każdy obiekt posiada konstruktor. Prototyp jest 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]"

Nadpisywanie pól prototypu własnymi polami obiektu


Jednym z wniosków płynących z powyższego wykładu jest to, że jeśli obiekt nie posiada pew-
nego pola, może skorzystać z pola o tej samej nazwie pochodzącego z łańcucha prototypów
(o ile istnieje). Co dzieje się w sytuacji, gdy i obiekt, i prototyp posiadają pole o danej nazwie?
Pierwszeństwo będzie miało własne pole obiektu.

Utwórzmy obiekt pasujący do opisanego powyżej scenariusza:


function Gadget(nazwa) {
this.nazwa = nazwa;
}
Gadget.prototype.nazwa = 'foo';
"foo"

159
JavaScript. Programowanie obiektowe

Po utworzeniu nowego obiektu i sięgnięciu do pola nazwa otrzymasz wartość jego własnego pola:
>>> var zabawka = new Gadget('aparat fotograficzny');
>>> zabawka.nazwa;

"aparat fotograficzny"

Jeśli usuniesz to pole, do głosu dojdzie analogiczne pole prototypu:


>>> delete zabawka.nazwa;

true
>>> zabawka.nazwa;

"foo"

Oczywiście zawsze możesz odtworzyć własne pole obiektu:


>>> zabawka.nazwa = 'aparat fotograficzny';
>>> zabawka.nazwa;

"aparat fotograficzny"

Pobieranie listy pól


Jeśli chcesz wypisać wszystkie pola danego obiektu, możesz skorzystać z pętli for…in. W roz-
dziale 2. pokazałem, jak wykorzystać tę pętlę do iteracji po elementach tablicy:
var a = [1, 2, 3];
for (var i in a) {
console.log(a[i]);
}

Tablice są obiektami, zatem można podejrzewać, że pętla for…in działa także na obiektach:
var o = {p1: 1, p2: 2};
for (var i in o) {
console.log(i + '=' + o[i]);
}

Wynikiem będzie:
p1=1
p2=2

Warto pamiętać, że:


Q Pętla for…in pominie niektóre pola. Nie pojawią się na przykład pole length
(dla tablic) ani constructor. Niepominięte pola określa się mianem wyliczalnych
(ang. enumerable). To, czy pole jest wyliczalne, można sprawdzić za pomocą
metody propertyIsEnumerable(), oferowanej przez wszystkie obiekty.

160
Rozdział 5. • Prototypy

Q Uwzględnione zostaną pola pochodzące z łańcucha prototypów, o ile są wyliczalne.


Można sprawdzić, czy dane pole jest własnym polem obiektu, czy polem pochodzącym
z prototypu, za pomocą metody hasOwnProperty().
Q propertyIsEnumerable() zwróci false dla wszystkich pól prototypu, nawet jeśli są
wyliczalne i pojawią się w pętli for…in.

Sprawdźmy, jak działają te metody. Zacznijmy od uproszczonej wersji konstruktora Gadget():


function Gadget(nazwa, kolor) {
this.nazwa = nazwa;
this.kolor = kolor;
this.metoda = function(){return 1;}
}
Gadget.prototype.cena = 100;
Gadget.prototype.ocena_uzytkownikow = 3;

Nowy obiekt:
var nowaZabawka = new Gadget('kamera', 'czarna');

Za pomocą pętli for…in możesz wypisać wszystkie pola obiektu, także te pochodzące z 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

Tym razem pętla wypisze tylko własne pola obiektu:


for (var pole in nowaZabawka) {
if (nowaZabawka.hasOwnProperty(pole)) {
console.log(pole + '=' + nowaZabawka [pole]);
}
}

Wynik:
nazwa=kamera
kolor=czarna
metoda=function () { return 1; }

Przejdźmy teraz do propertyIsEnumerable(). Dla własnych pól metoda zwraca true:


>>> nowaZabawka.propertyIsEnumerable('nazwa')

true

Większość pól i metod wbudowanych nie jest wyliczalna:


>>> nowaZabawka.propertyIsEnumerable('constructor')

false

Nie są wyliczalne pola pochodzące z łańcucha prototypów:


>>> nowaZabawka.propertyIsEnumerable('cena')

false

Takie pola staną się wyliczalne, jeśli dojdziemy do obiektu zawartego w polu prototype i to na
nim wywołamy metodę:
>>> nowaZabawka.constructor.prototype.propertyIsEnumerable('cena')

true

isPrototypeOf()
Każdy obiekt posiada metodę isPrototypeOf(). Zwraca on informację o tym, czy konkretny
obiekt jest prototypem innego.

Weźmy prosty obiekt małpa.


var małpa = {
owłosiona: true,
je: 'banany',
oddycha: 'powietrzem'
};

162
Rozdział 5. • Prototypy

Utwórzmy teraz konstruktor Człowiek(), którego pole prototype ustawimy na małpa.


function Człowiek(nazwisko) {
this.nazwisko = nazwisko;
}
Człowiek.prototype = małpa;

Jeśli stworzysz teraz nowy obiekt typu Człowiek i nazwiesz go jurek, a następnie spytasz „Czy
małpa jest prototypem Jurka?”, otrzymasz wartość true.
>>> var jurek = new Człowiek('Jurek');
>>> małpa.isPrototypeOf(jurek)

true

Ukryte powiązanie __proto__


Wiesz już, że jeśli obiekt nie posiada pola o podanej nazwie, sprawdzone zostanie pole
prototype.

Jeszcze raz utwórzmy obiekt małpa i wykorzystajmy go jako prototyp podczas tworzenia obiektów
za pomocą konstruktora Człowiek().
var małpa = {
je: 'banany',
oddycha: 'powietrzem'
};
function Człowiek() {}
Człowiek.prototype = małpa;

Utwórzmy teraz obiekt programista i przypiszmy mu kilka pól:


var programista = new Człowiek();
programista.je = 'pizzę';
programista.wymiata_w = 'JavaScript';

Pobierzmy teraz wartości pól. wymiata_w jest polem obiektu programista.


>>> programista.wymiata_w

"JavaScript"

Innym polem jest je:


>>> programista.je

"pizzę"

oddycha nie jest własnym polem obiektu programista, dlatego sprawdzony zostanie prototyp,
jak gdyby istniało tajemne powiązanie pomiędzy obiektem a prototypem.

163
JavaScript. Programowanie obiektowe

>>> programista.oddycha

"powietrzem"

Czy z obiektu programista możesz dostać się do jego prototypu? Cóż, w zasadzie tak — jeśli
wykorzystasz konstruktor jako pośrednika, dostaniesz się do obiektu małpa, pisząc programista.
´constructor.prototype. Rozwiązanie to nie jest jednak godne polecenia. Pole constructor pełni
przede wszystkim funkcję informacyjną, a jego wartość może zostać zmieniona w dowolnej
chwili. Możesz nawet podstawić tam dane niebędące obiektem — nie zaburzy to działania
łańcucha prototypów.

Przypiszmy polu constructor prosty łańcuch znaków:


>>> programista.constructor = 'śmieć'

"śmieć"

Wydaje się, że zepsuliśmy prototyp:


>>> typeof programista.constructor.prototype

"undefined"

…a jednak tak nie jest, skoro programista nadal oddycha "powietrzem":


>>> programista.oddycha

"powietrzem"

Dowodzi to, że nadal istnieje sekretne powiązanie pomiędzy obiektem a jego prototypem. Firefox
widzi to powiązanie jako pole __proto__ (na początku i na końcu słowa proto znajdują się po
dwa podkreślniki).
>>> programista.__proto__

Object je=banany oddycha=powietrzem

Możesz pobawić się tym polem dla celów nauki, ale nie warto korzystać z niego w skryptach.
__proto__ nie istnieje w przeglądarce IE, więc Twoje skrypty nie byłyby przenośne. Spróbuj
na przykład utworzyć kilka obiektów, dla których małpa będzie prototypem, a potem wprowadzić
zmianę w prototypie, która wpłynie na istniejące już obiekty:
>>> małpa.test = 1

1
>>> programista.test

__proto__ i prototype to nie to samo. Różnica polega na tym, że __proto__ jest polem instancji,
podczas gdy prototype jest polem konstruktorów.

164
Rozdział 5. • Prototypy

>>> typeof programista.__proto__

"object"
>>> typeof programista.prototype

"undefined"

Powiem to jeszcze raz: nie odwołuj się bezpośrednio do __proto__, chyba że robisz to dla celów
nauki, lub szukając błędów.

Rozszerzanie obiektów wbudowanych


Obiekty wbudowane, takie jak konstruktory Array, String, Object czy Function, mogą zostać
rozszerzone za pomocą prototypów. Oznacza to, że możesz dodać, na przykład do prototypu
Array, nowe metody, które staną się dostępne dla wszystkich tablic. Przećwiczmy to.

Język PHP posiada funkcję o nazwie in_array(), która sprawdza, czy podana wartość jest
elementem tablicy. JavaScript nie ma takiej metody. Zaimplementujmy ją pod nazwą inArray()
i dodajmy do Array.prototype.
Array.prototype.inArray = function(needle) {
for (var i = 0, len = this.length; i < len; i++) {
if (this[i] === needle) {
return true;
}
}
return false;
}

Każda tablica będzie miała tę metodę. Sprawdźmy:


>>> var a = [czerwony', 'zielony', 'niebieski'];
>>> a.inArray('czerwony');

true
>>> a.inArray('żółty');

false

Działa! Zatem zróbmy to jeszcze raz. Wyobraź sobie, że Twoja aplikacja często musi odwracać
kolejność liter w łańcuchach znaków, w związku z czym czujesz, że przydałaby się 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('');
}

Metoda split() została wykorzystana do zamiany łańcucha na tablicę, na której następnie


wywołujemy metodę reverse(). Wynik jest z powrotem zamieniany na napis przy użyciu join().
Przetestujmy nową metodę:
>>> "Stoyan".reverse();
"nayotS"

Rozszerzanie obiektów wbudowanych — kontrowersje


Rozszerzając obiekty wbudowane za pomocą prototypów, możesz w dowolny sposób kształ-
tować kod. Możliwości tego mechanizmu są niemalże nieograniczone, dlatego przed jego za-
stosowaniem należy zastanowić się, czy nie istnieją inne, mniej inwazyjne rozwiązania.

Przykładem biblioteki, w którym prototypy stosowane są na każdym kroku, jest Prototype. Jej
twórca tak bardzo lubił prototypy, że nazwał swoje dzieło na ich cześć! Biblioteka ta pozwala
używać metod JavaScriptu w sposób zbliżony do języka Ruby.

Biblioteka YUI (Yahoo! User Interface) jest całkowitym przeciwieństwem Prototype. Jej twó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

Pułapki związane z prototypami


Istnieją dwa nie do końca intuicyjne zachowania programów związane z prototypami:
Q Zmiana prototypu pociąga za sobą jego zmianę we wszystkich utworzonych
przy jego pomocy obiektach, z wyjątkiem sytuacji, gdy obiekt prototype zostaje
całkowicie zastąpiony innym obiektem.
Q Nie można ufać zawartości prototype.constructor.

Dwa proste konstruktory i dwa obiekty:


>>> function Pies(){this.ogon = true;}
>>> var azor = new Pies();
>>> var burek = new Pies();

Nawet po utworzeniu obiektów możliwe jest dodawanie do prototypu nowych własności, do


których obiekty będą miały dostęp. Dodajmy metodę szczekaj():
>>> Pies.prototype.szczekaj = function(){return 'Hau!';}

Oba obiekty mają do niej dostęp:


>>> azor.szczekaj();

"Hau!"
>>> burek.szczekaj();

"Hau!"

Jeśli teraz spytasz obiekty o konstruktor, za pomocą którego zostały utworzone, odpowiedzą
poprawnie.
>>> azor.constructor;

Pies()
>>> burek.constructor;

Pies()

Ciekawostką jest to, że jeśli spytasz o konstruktor prototypu, w odpowiedzi również otrzymasz
Pies(), co nie jest do końca zgodne z prawdą. Prototyp to w rzeczywistości zwykły obiekt
utworzony za pomocą Object(). Nie posiada on żadnych własności obiektu utworzonego za
pomocą konstruktora Pies().
>>> azor.constructor.prototype.constructor

Pies()
>>> typeof azor.constructor.prototype.ogon

"undefined"

167
JavaScript. Programowanie obiektowe

Nadpiszmy teraz obiekt prototypu zupełnie innym obiektem:


>>> Pies.prototype = {łapy: 4, owłosiony: true};

Okazuje się, że stare obiekty nie mają dostępu do nowych pól. Nadal są one powiązane ze starym
obiektem prototypu:
>>> typeof azor.łapy

"undefined"
>>> azor.szczekaj()

"Hau!"
>>> typeof azor.__proto__.szczekaj

"function"
>>> typeof azor.__proto__.łapy

"undefined"

Nowe obiekty będą korzystały z poprawionej wersji prototypu:


>>> var lessi = new Pies();
>>> lessi.szczekaj()

TypeError: lessi.szczekaj is not a function


>>> lessi.łapy

__proto__ wskazuje nowy obiekt prototypu:


>>> typeof lessi.__proto__.szczekaj

"undefined"
>>> typeof lessi.__proto__.łapy

"number"

W tej chwili pole constructor przestaje zwracać poprawną informację. Powinno wskazywać
na Pies(), ale zamiast niego jest to Object():
>>> lessi.constructor

Object()
>>> azor.constructor

Dog()

168
Rozdział 5. • Prototypy

Najbardziej dezorientującym elementem jest informacja o prototypie konstruktora:


>>> typeof lessi.constructor.prototype.łapy

"undefined"
>>> typeof azor.constructor.prototype.łapy

"number"

Poniższe linie powinny przywrócić oczekiwane zachowanie kodu:


>>> Pies.prototype = {łapy: 4, owłosiony: true};
>>> Pies.prototype.constructor = Pies;

Dobra rada
Jeśli nadpisujesz prototyp, nadaj odpowiednią wartość polu constructor.

Podsumowanie
Podsumujmy krótko treść rozdziału:
Q Wszystkie funkcje posiadają pole o nazwie prototype. Początkowo zawiera ono
pusty obiekt.
Q Do obiektu prototype można dodawać pola i metody. Można także zastąpić go
innym obiektem.
Q Obiekt utworzony za pomocą konstruktora (z operatorem new) przechowuje ukryte
powiązanie z prototypem oraz może odwoływać się do pól prototypu jak do swoich
własnych.
Q Własne pola obiektu nadpisują pola prototypu o tej samej nazwie.
Q Pola własne od pól prototypu można odróżnić za pomocą metody hasOwnProperty().
Q Istnieje łańcuch prototypów: jeśli obiekt foo nie posiada pola bar, w wyniku odwołania
się do foo.bar zostanie przeszukany prototyp. Jeśli prototyp nie ma tego pola,
sprawdzony zostanie jego prototyp — aż do najwyższego poziomu, czyli do Object.
Q Możesz rozszerzać konstruktory wbudowane. Wprowadzone w nich zmiany będą
widoczne dla wszystkich obiektów. Jeśli przypiszesz funkcję do Array.prototype.flip,
wszystkie tablice będą miały metodę flip (np. [1,2,3].flip). Sprawdź (w kodzie),
czy metoda na pewno nie istnieje — uchroni Cię to przed błędami w przyszłości.

169
JavaScript. Programowanie obiektowe

Ćwiczenia
1. Utwórz obiekt kształt, który posiada pole typ oraz metodę dostępową pobierzTyp()
(lub, zgodnie z angielskojęzyczną konwencją, pole type oraz metodę getType()).
2. Zdefiniuj konstruktor Trójkąt(), którego prototypem jest Figura. Obiekty tworzone
za pomocą Trójkąt() powinny mieć trzy własne pola: a, b i c, przechowujące
długość boków trójkąta.
3. Dodaj do prototypu nową metodę o nazwie pobierzObwód().
4. Przetestuj poprawność implementacji za pomocą następującego kodu:
>>> var t = new Trójkąt(1, 2, 3);
>>> t.constructor

Trójkąt(a, b, c)
>>> kształt.isPrototypeOf(t)

true
>>> t.pobierzObwód()

6
>>> t.pobierzTyp()

"trójkąt"

5. Napisz pętlę, która wypisze wszystkie własne pola i metody obiektu t, pomijając
pola i metody prototypu.
6. Spraw, by działał poniższy kod:
>>> [1,2,3,4,5,6,7,8,9].potasuj()

[2, 4, 1, 8, 9, 6, 5, 3, 7]

170
6

Dziedziczenie

W rozdziale 1. przedstawiłem różne zagadnienia i terminy związane z programowaniem obiekto-


wym. W kolejnym rozdziałach pokazywałem, jakie jest ich zastosowanie w języku JavaScript.
W tej chwili wiesz już, czym są obiekty, pola i metody. Wiesz, że JavaScript nie ma klas, ale
ich funkcjonalności są dostępne dzięki konstruktorom. Kapsułkowanie? Jak najbardziej:
obiekty posiadają zarówno dane, jak i sposoby działania na tych danych (czyli metody). Agre-
gacja? Tak, obiekty mogą zawierać inne obiekty, a nawet zawierają je prawie zawsze — skoro
metody to funkcje, a funkcje to obiekty. Skoro posiadasz już cały ten ogrom wiedzy, przyszła
pora, by skoncentrować się na dziedziczeniu. Jest to jedna z najciekawszych cech programo-
wania obiektowego. Dzięki dziedziczeniu ten sam kod można wykorzystywać wielokrotnie.
Sprzyja to lenistwu, ale to właśnie lenistwo doprowadziło do powstania języków programo-
wania, czyż nie?

JavaScript jest językiem dynamicznym, przez co większość zadań programistycznych można


wykonać na więcej niż jeden sposób. Dziedziczenie nie jest tu wyjątkiem. Zamierzam przed-
stawić kilka popularnych wzorców implementacji dziedziczenia, zaczynając od sposobu opi-
sanego w standardzie ECMAScript. Poświęć chwilę na zrozumienie tych wzorców i świadomy
wybór tego, który najlepiej pasuje do Twojego projektu i stylu pracy.

W rozdziale kilka razy pojawi się nazwisko Douglasa Crockforda. Nie sposób mówić o 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.

Przykładowy łańcuch prototypów


Tworzenie łańcuchów prototypów jest standardowym sposobem implementacji dziedziczenia,
opisanym w standardzie ECMAScript. Stwórzmy teraz przykładową hierarchię, zaczynając od
trzech konstruktorów.

172
Rozdział 6. • Dziedziczenie

function Figura(){
this.nazwa = 'figura';
this.toString = function() {return this.nazwa;};
}

function Figura2D(){
this.nazwa = 'figura 2D';
}

function Trójkąt(bok, wysokość) {


this.nazwa = 'trójkąt';
this.bok = bok;
this.wysokość = wysokość;
this.pobierzPole = function(){return this.bok * this.wysokość / 2;};
}

Dziedziczenie odbywa się w następujący sposób:


Figura2D.prototype = new Figura();
Trójkąt.prototype = new Figura2D();

Co się dzieje? Zamiast dodawać pola do obiektu przechowywanego w polu prototype obiektu
Figura2D, nadpisaliśmy obiekt czymś zupełnie nowym — obiektem powstałym w wyniku wywo-
łania konstruktora new Figura(). Tak samo w przypadku obiektu Trójkąt, którego prototyp 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.

Pewnie pamiętasz z poprzedniego rozdziału, że całkowite nadpisanie prototypu (w odróżnieniu od


jego rozszerzenia o nowe pola) ma pewne efekty uboczne związane z polem constructor.
Dlatego po implementacji dziedziczenia warto na nowo ustawić wartość constructor:
Figura2D.prototype.constructor = Figura2D;
Trójkąt.prototype.constructor = Trójkąt;

Przetestujmy kod. Stwórzmy obiekt Trójkąt i wywołajmy jego metodę pobierzPole():


>>> var my = new Trójkąt(5, 10);
>>> my.pobierzPole();

25

Chociaż obiekt my nie ma własnej metody toString(), dziedziczy ją z innego obiektu i dzięki
temu może ją wywołać. Zwróć uwagę, że this wewnątrz metody toString odnosi się do
obiektu my.

173
JavaScript. Programowanie obiektowe

>>> my.toString()

"trójkąt"

Oto, co dzieje się po wywołaniu my.toString():


Q Interpreter JavaScriptu sprawdza pola obiektu my i nie znajduje metody o nazwie
toString().
Q Sprawdza obiekt wskazywany przez my.__proto__. Obiekt ten jest instancją
Figura2D utworzoną w procesie dziedziczenia.
Q Interpreter próbuje znaleźć metodę toString() w instancji Figura2D. Ponieważ
przeszukiwanie kończy się niepowodzeniem, sprawdza wskaźnik __proto__, który
tym razem prowadzi do instancji utworzonej wcześniej jako new Shape().
Q Udaje się odnaleźć metodę toString() wewnątrz instancji Shape().
Q Metoda zostaje wywołana w kontekście obiektu my, co oznacza, że this wskazuje my.

Jeśli spytamy my „kto cię stworzył?”, odpowie poprawnie, ponieważ podczas dziedziczenia
jawnie zmieniliśmy wartość constructor.
>>> my.constructor

Trójkąt(bok, wysokość)

Przy pomocy operatora instanceof możesz upewnić się, że my jest instancją wszystkich trzech
konstruktorów.
>>> my instanceof Figura

true
>>> my instanceof Figura2D

true
>>> my instanceof Trójkąt

true
>>> my instanceof Array

false

Ten sam efekt da wywołanie metody isPropertyOf() („czy jest polem obiektu”) konstruktorów
z parametrem my:
>>> Figura.prototype.isPrototypeOf(my)

true
>>> Figura2D.prototype.isPrototypeOf(my)

true

174
Rozdział 6. • Dziedziczenie

>>> Trójkąt.prototype.isPrototypeOf(my)

true
>>> String.prototype.isPrototypeOf(my)

false

Możesz również tworzyć obiekty przy użyciu dwóch pozostałych konstruktorów. Obiekty
utworzone za pomocą new Figura2D() także mają metodę toString(), odziedziczoną z Figura().
>>> var td = new Figura2D();
>>> td.constructor

Figura2D()
>>> td.toString()

"figura 2D"
>>> var s = new Figura();
>>> s.constructor

Figura()

Przenoszenie wspólnych pól do prototypu


Kiedy tworzysz obiekty przy użyciu konstruktora, możesz dodawać nowe pola przy użyciu
this. Nie jest to najlepsze rozwiązanie, jeśli wszystkie instancje mają te same pola. W powyższym
przykładzie konstruktor Figura() został zdefiniowany w następujący sposób:
function Figura(){
this.nazwa= 'Figura';
}

Oznacza to, że za każdym razem gdy nowy obiekt powstaje w wyniku wywołania new Figura(),
tworzone jest nowe pole nazwa, które musi zostać przechowane w pamięci. Zamiast tego moż-
na dodać pole nazwa do prototypu — wówczas będzie ono dzielone przez wszystkie instancje.
function Figura(){}
Figura.prototype.nazwa = 'Figura';

Od tej pory nowe obiekty tworzone za pomocą Figura() nie będą posiadały własnego pola
nazwa, tylko będą korzystały z pola dodanego do prototypu. To rozwiązanie jest bardziej 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;};

Kod można testować w taki sam sposób jak wcześniej:


>>> var my = new Trójkąt(5, 10);
>>> my.getArea()

25
>>> my.toString()

"Trójkąt"

Działanie kodu jest takie same. Różnią się tylko operacje wykonywane w tle podczas wywo-
łania my.toString(). Konieczne jest jedno sprawdzenie więcej, zanim metoda zostanie odna-
leziona w Figura.prototype, a nie w instancji new Figura(), co miało miejsce w poprzednim
przykładzie.

Możesz także poeksperymentować z metodą hasOwnProperty(), która pozwoli Ci sprawdzić,


czy dane pole jest własnym polem obiektu, czy polem pochodzącym z łańcucha prototypów.

176
Rozdział 6. • Dziedziczenie

>>> my.hasOwnProperty('bok')

true
>>> my.hasOwnProperty('nazwa')

false

Wywołania isPrototypeOf() oraz operator instanceof zadziałają dokładnie tak samo jak we
wcześniejszym przykładzie:
>>> Figura2D.prototype.isPrototypeOf(my)

true
>>> my instanceof Figura

true

Dziedziczenie samego prototypu


Wyjaśniłem już, że dla zwiększenia wydajności warto rozważyć dodanie współdzielonych pól
i metod do prototypu. Jeśli zdecydujesz się na to rozwiązanie, dobrym pomysłem może okazać się
dziedziczenie samego prototypu, skoro to w nim znajduje się interesujący Cię kod wielokrotnego
użytku. Innymi słowy, lepsze będzie dziedziczenie obiektu osadzonego w Figura.prototype
niż całego obiektu utworzonego za pomocą new Figura() — przecież i tak nie skorzystasz
z własnych pól obiektu Figura (inaczej trafiłyby one do prototypu). Rozwiązanie to pociąga za
sobą zwiększenie efektywności, ponieważ:
Q Nie jest tworzony nowy obiekt potrzebny tylko podczas dziedziczenia.
Q Przeszukiwanie łańcucha prototypów (na przykład w celu odnalezienia toString())
jest krótsze.

Oto kod w poprawionej wersji (zmiany zostały wytłuszczone):


function Figura(){}
// rozszerzenie prototypu
Figura.prototype.nazwa = 'figura';
Figura.prototype.toString = function() {return this.nazwa;};

function Figura2D(){}
// obsługa dziedziczenia
Figura2D.prototype = Figura.prototype;
Figura2D.prototype.constructor = Figura2D;
// rozszerzenie prototypu
Figura2D.prototype.nazwa = 'figura 2D';

177
JavaScript. Programowanie obiektowe

function Trójkąt(bok, wysokość) {


this.bok = bok;
this.wysokość = wysokość;
}
// obsługa dziedziczenia
Trójkąt.prototype = Figura2D.prototype;
Trójkąt.prototype.constructor = Trójkąt;
// rozszerzenie prototypu
Trójkąt.prototype.nazwa = 'trójkąt';
Trójkąt.prototype.pobierzPole = function(){return this.bok * this.wysokość / 2;}

Kod można przetestować w taki sam sposób jak wcześniej:


>>> var my = new Trójkąt(5, 10);
>>> my.pobierzPole()

25
>>> my.toString()

"trójkąt"

Na czym polega różnica w wyszukiwaniu podczas wywołania my.toString()? Po pierwsze,


interpreter jak zwykle szuka metody toString() wewnątrz obiektu. Nie znajduje go, dlatego
sprawdza prototyp. Prototyp zawiera wskaźnik na ten sam obiekt, który wskazują pola proto-
type obiektu Figura2D oraz obiektu Figura. Pamiętaj, że obiekty nie są kopiowane przez war-
tość, tylko przez referencję. Dlatego sprawdzenie odbywa się w dwóch krokach, a nie w czterech
(jak w poprzednim przykładzie) ani trzech (jak na początku).

Kopiowanie samego prototypu zwiększa efektywność, ale ma pewien efekt uboczny: ponieważ
wszystkie dzieci i wszyscy rodzice wskazują ten sam obiekt, jeśli dziecko zmieni prototyp, zmiana
będzie widoczna dla wszystkich rodziców i całego rodzeństwa. Przeanalizuj następującą linię:
Trójkąt.prototype.nazwa = 'trójkąt;

Zmieniane jest pole nazwa. Zmiana wartości jest widziana także na ścieżce Figura.prototype.name.
Jeśli utworzysz egzemplarz obiektu przy użyciu new Figura(), pole nazwa będzie zawierało war-
tość "trójkąt":
>>> var f = new Figura()
>>> f.nazwa

"trójkąt"

Konstruktor tymczasowy — new F()


Rozwiązaniem opisanego powyżej problemu, kiedy to wszystkie pola prototype wskazują ten
obiekt, przez co obiekty-rodzice odczytują zmiany wprowadzone przez obiekty-dzieci, jest zasto-
sowanie pośrednika, który przerwie łańcuch. Pośrednik ma formę tymczasowego konstruktora.

178
Rozdział 6. • Dziedziczenie

Tworząc pustą funkcję F() i ustawiając jej wartość prototype na prototype konstruktora-rodzica,
możesz wywoływać new f() i tworzyć obiekty, które nie mają własnych pól, ale dziedziczą
wszystko z prototype rodzica.

Zmodyfikowany kod wygląda tak:


function Figura(){}
// rozszerzenie prototypu
Figura.prototype.nazwa = 'figura';
Figura.prototype.toString = function() {return this.nazwa;};

function Figura2D(){}
// obsługa dziedziczenia
var F = function(){};
F.prototype = Figura.prototype;
Figura2D.prototype = new F();
Figura2D.prototype.constructor = Figura2D;
// rozszerzenie prototypu
Figura2D.prototype.name = '2D Figura';

function Trójkąt(bok, wysokość) {


this.bok = bok;
this.wysokość = wysokość;
}

// obsługa dziedziczenia
var F = function(){};
F.prototype = Figura2D.prototype;
Trójkąt.prototype = new F();
Trójkąt.prototype.constructor = Trójkąt;
// rozszerzenie prototypu
Trójkąt.prototype.name = 'Trójkąt';
Trójkąt.prototype.getArea = function(){return this.bok * this.wysokość / 2;};

Testy:
>>> var my = new Trójkąt(5, 10);
>>> my.pobierzPole()

25
>>> my.toString()

"trójkąt"

W ten sposób zachowywany jest łańcuch prototypów, ale pola rodziców nie są nadpisywane
polami dzieci:
>> my.__proto__.__proto__.__proto__.constructor

Figura()

179
JavaScript. Programowanie obiektowe

>>> var s = new Figura();


>>> s.name

"figura"

Podejście opisane w tym podrozdziale jest zgodne z poglądem, że dziedziczone powinny być
tylko pola i metody dodane do prototypu, zaś własne pola obiektów powinny być pomijane,
ponieważ z reguły są zbyt szczegółowe i zbyt silnie związane z instancją, by można je było
ponownie wykorzystać.

Uber: dostęp do obiektu-rodzica


Większość klasycznych języków obiektowych posiada specjalną składnię umożliwiającą dostęp do
klasy-rodzica, określanej mianem nadklasy (ang. superclass) lub klasy bazowej. Przydaje się to,
gdy dziecko chce implementować metodę, która wykonuje wszystkie czynności wykonywane
przez metodę rodzica, a na koniec dodaje coś od siebie. W takich sytuacjach dziecko wywołuje
metodę rodzica o tej samej nazwie, a następnie przetwarza zwracany przez nią wynik.

JavaScript nie posiada takiej składni, ale osiągnięcie opisanej funkcjonalności i tak jest możliwe.
Jeszcze raz przepiszmy poprzedni przykład, rozszerzając go o pole uber, które będzie 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

function Trójkąt(bok, wysokość) {


this.bok = bok;
this.wysokość = wysokość;
}

// obsługa dziedziczenia
var F = function(){};
F.prototype = Figura2D.prototype;
Trójkąt.prototype = new F();
Trójkąt.prototype.constructor = Trójkąt;
Trójkąt.uber = Figura2D.prototype;
// rozszerzenie prototypu
Trójkąt.prototype.name = 'Trójkąt';
Trójkąt.prototype.getArea = function(){return this.bok * this.wysokość / 2;};

Pojawiły się następujące nowości:


Q Nowe pole uber wskazuje prototyp rodzica.
Q Zmieniła się definicja toString().

Wcześniej metoda toString() zwracała jedynie wartość this.nazwa. Teraz metoda dodatkowo
sprawdza, czy istnieje this.constructor.uber. Jeśli tak, najpierw wywołuje metodę this.
´constructor.uber.toString(). this.constructor to funkcja (konstruktor), a this.constructor.
´uber to wskaźnik na prototype rodzica. Efekt jest taki, że wywołanie toString() na instancji
Trójkąt zwraca połączone wyniki wywołania tej metody na elementach łańcucha prototypów:
>>> var my = new Trójkąt(5, 10);
>>> my.toString()

"figura, figura 2D, Trójkąt"

Mógłbym zamiast uber nazwać pole „superclass”, ale sugerowałoby to, że JavaScript ma klasy.
Chciałbym nazwać je po prostu „super” (jak w Javie), ale niestety super jest słowem 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).

Zamknięcie dziedziczenia wewnątrz funkcji


Umieśćmy kod obsługujący dziedziczenie wewnątrz funkcji o nazwie extend() („rozszerz”):
function extend(Dziecko, Rodzic) {
var F = function(){};
F.prototype = Rodzic.prototype;
Dziecko.prototype = new F();
Dziecko.prototype.constructor = Dziecko;
Dziecko.uber = Rodzic.prototype;
}

181
JavaScript. Programowanie obiektowe

Dzięki tej funkcji (lub podobnej, lepiej dopasowanej do konkretnego programu) kod będzie
krótszy i bardziej czytelny. Obiekty mogą dziedziczyć z innych w następujący sposób:
extend(Figura2D, Figura);
extend(Trójkąt, Figura);

W podobny sposób realizowane jest dziedziczenie w bibliotece YUI (Yahoo! User Interface).
Odpowiedni fragment kodu korzystający z tej biblioteki wyglądałby tak:
YAHOO.lang.extend(Trójkąt, Figura)

Kopiowanie pól
Inny sposób realizacji dziedziczenia to kopiowanie pól. Można zmniejszyć ilość potrzebnego
kodu (a przecież o to właśnie chodzi w dziedziczeniu), kopiując pola obiektu-rodzica do 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ę.

Przeanalizujmy to na przykładzie konstruktorów Figura() i Figura2D(). Prototyp konstruktora


Figura() zawiera jedno pole typu prostego (nazwa) i jedno będące obiektem (metoda toString()):
var Figura = function(){};
var Figura2D = function(){};
Figura.prototype.nazwa = 'figura';
Figura.prototype.toString = function(){return this.nazwa;};

182
Rozdział 6. • Dziedziczenie

Jeśli do realizacji dziedziczenia zostanie użyta funkcja extend(), ani instancja Figura2D(), ani
jej prototyp nie będą miały pola nazwa. Będą za to miały dostęp do pola nazwa odziedziczonego
z Figura().
>>> extend(Figura2D, Figura);
>>> var f2 = new Figura2D();
>>> f2.nazwa

"figura"
>>> Figura2D.prototype.name

"figura"
>>> f2.__proto__.name

"figura"
>>> f2.hasOwnProperty('nazwa')

false
>>> f2.__proto__.hasOwnProperty('nazwa')

false

Jeśli wykorzystana zostanie funkcja extend2(), prototyp konstruktora Figura2D() otrzyma wł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

Uwaga na kopiowanie przez referencję!


To, że obiekty (w tym funkcje i tablice) są kopiowane przez referencję, może niekiedy prowadzić
do nieoczekiwanych rezultatów.

Stwórzmy dwa konstruktory i dodajmy kilka pól do prototypu pierwszego z nich.


>>> var A = function(){}, B = function(){};
>>> A.prototype.costam = [1,2,3];

[1, 2, 3]
>>> A.prototype.nazwa = 'a';

"a"

Niech B dziedziczy z A (nie ma znaczenia, czy za pomocą extend(), czy extend2()):


>>> extend2(B, A);

Jeśli wykorzystane zostało extend2(), prototyp B odziedziczył pola prototypu A jako własne.
>>> B.prototype.hasOwnProperty('nazwa')

true
>>> B.prototype.hasOwnProperty('costam')

true

Pole nazwa jest typu prostego, dlatego tworzona jest jego kopia. Pole costam jest tablicą (obiektem),
dlatego jest kopiowane przez referencję:
>>> B.prototype.costam

[1, 2, 3]
>>> B.prototype.costam === A.prototype.costam

true

Zmiana pola nazwa w B nie będzie miała żadnego wpływu na A:


>>> B.prototype.nazwa += 'b'

"ab"
>>> A.prototype.nazwa

"a"

Jeśli jednak w B zostanie zmienione pole costam, zmiana będzie widoczna w A, ponieważ oba
prototypy posiadają wskaźnik na tę samą tablicę.

184
Rozdział 6. • Dziedziczenie

>>> B.prototype.costam.push(4,5,6);

6
>>> A.prototype,costam

[1, 2, 3, 4, 5, 6]

Co innego, jeśli kopia costam w B zostanie całkowicie nadpisana innym obiektem. W takim
wypadku A zachowa wskaźnik na stary obiekt.
>>> B.prototype.costam = ['a', 'b', 'c'];

["a", "b", "c"]


>>> A.prototype.costam

[1, 2, 3, 4, 5, 6]

Wyobraź sobie obiekt jako rzecz tworzoną i przechowywaną w pewnym miejscu w pamięci.
Zmienne i pola jedynie wskazują dane miejsce. Przypisanie polu B.prototype.costam nowego
obiektu jest jak wydanie rozkazu: „Zapomnij o starym obiekcie i przesuń wskaźnik w miejsce
nowego”.

185
JavaScript. Programowanie obiektowe

Obiekty dziedziczą z obiektów


Wszystkie przedstawione do tej pory przykłady dziedziczenia dotyczyły sytuacji, w których
obiekty są tworzone za pomocą konstruktorów, a obiekty tworzone za pomocą jednego kon-
struktora mają dziedziczyć pola pochodzącego z innego. Prawdopodobnie zastanawiasz się, co
z obiektami, które tworzone są bez użycia konstruktorów, za pomocą notacji literałowej (która,
przy okazji, zajmuje mniej miejsca).

W Javie czy PHP definiuje się klasy, które mogą dziedziczyć z innych klas (łatwo zapamiętać,
że klasyczne programowanie obiektowe oparte jest na klasach). JavaScript nie ma klas. Programi-
ści, którzy wcześniej pracowali w którymś z języków z klasami, uciekają się do konstruktorów,
ponieważ to rozwiązanie najbardziej przypomina znane im mechanizmy. Na dodatek 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

Zobaczmy, jak działa funkcja. Zaczniemy od utworzenia obiektu bazowego:


var figura = {
nazwa: 'figura',
toString: function() {return this.nazwa;}
}

W celu utworzenia obiektu dziedziczącego z figura wywołamy funkcję extendCopy(). Zwróci


ona nowy obiekt, do którego będzie można dodawać nowe funkcjonalności.
var dwaDe = extendCopy(figura);
dwaDe.name = 'figura 2D;
dwaDe.toString = function(){return this.uber.toString() + ', ' + this.nazwa;};

Obiekt trójkąt, dziedziczący z dwaDe:


var trójkąt = extendCopy(dwaDe);
trójkąt.name = 'trójkąt ';
trójkąt.pobierzPole = function(){return this.bok * this.wysokość / 2;}

Wykorzystanie obiektu:
>>> trójkąt.bok = 5; trójkąt.wysokość = 10; trójkąt.pobierzPole();

25
>>> trójkąt.toString();

"figura, figura 2D, trójkąt"

Pewnym minusem tej metody dziedziczenia jest to, że trzeba jawnie przypisać wartości 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.

Głębokie kopiowanie, które zaimplementujemy w postaci funkcji deepCopy(), przebiega po-


dobnie jak płytkie, z tą różnicą, że jeśli pętla przechodząca po polach obiektu napotka pole,

187
JavaScript. Programowanie obiektowe

które samo jest obiektem, zostanie na nim rekurencyjnie wywołana funkcja głębokiego 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;
}

Stwórzmy obiekt, którego polami będą tablice i inne obiekty.


var rodzic = {
liczby: [1, 2, 3],
litery: ['a', 'b', 'c'],
obj: {
pole: 1
},
bool: true
};

Sprawdźmy, czy wszystko działa jak należy. Zmiana pola liczby w głębokiej kopii nie powinna
mieć wpływu na pole liczby oryginalnego obiektu.
>>> var głęboka = deepCopy(rodzic);
>>> var płytka = extendCopy(rodzic);
>>> głęboka.liczby.push(4,5,6);

6
>>> głęboka.liczby

[1, 2, 3, 4, 5, 6]
>>> rodzic.liczby

[1, 2, 3]
>>> płytka.liczby.push(10)

4
>>> płytka.liczby

[1, 2, 3, 10]

188
Rozdział 6. • Dziedziczenie

>>> rodzic.liczby

[1, 2, 3, 10]
>>> głęboka.liczby

[1, 2, 3, 4, 5, 6]

Dziedziczenie za pomocą głębokiej kopii zostało zaimplementowane w nowszych wersjach


jQuery.

object()
W związku z tym, że obiekty dziedziczą z innych obiektów, Douglas Crockford zaproponował
wykorzystanie funkcji object(), która jako parametr przyjmuje pewien obiekt i zwraca nowy
obiekt, którego prototypem jest obiekt przekazany jako parametr.
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

Jeśli potrzebny jest Ci dostęp do pola uber, możesz nieco zmienić funkcję object():
function object(o) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
return n;
}

Zasada jest taka sama jak w przypadku extendCopy(): pobierany jest obiekt (jak dwaDe we wcze-
śniejszym przykładzie), po czym tworzona jest jego kopia, którą następnie można rozszerzać.
var trójkąt = object(dwaDe);
trójkąt.nazwa = 'trójkąt';
trójkąt.pobierzPole = function(){return this.bok * this.wysokość / 2;};

Zachowanie obiektu nie zmieniło się:


>>> trójkąt.toString()

"figura, figura 2D, trójkąt"

Ten wzorzec dziedziczenia nazywa się prototypowym, ponieważ obiekt-rodzic staje się pro-
totypem obiektu-dziecka.

189
JavaScript. Programowanie obiektowe

Połączenie dziedziczenia prototypowego


z kopiowaniem pól
Z dziedziczenia najczęściej korzysta się po to, by wykorzystać pewne istniejące funkcjonalności
i, być może, rozszerzyć je. Tworzony jest nowy obiekt, który dziedziczy z już istniejącego i do
którego dodaje się nowe pola i metody. Wszystkie te operacje można wykonać za pomocą po-
jedynczego wywołania funkcji, która łączy dwa omówione wcześniej podejścia.

Możesz:
Q wykorzystać dziedziczenie prototypowe w celu sklonowania istniejącego obiektu,
Q skopiować wszystkie pola innego obiektu.
function objectPlus(o, dodatki) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
for (var i in dodatki) {
n[i] = dodatki[i];
}
return n;
}

Funkcja pobiera obiekt o (z którego będzie dziedziczył nowy obiekt) oraz obiekt dodatki (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
});

Przetestujmy działanie programu na przykładzie trójkąta my o zdefiniowanej długości boku


i wysokości.
>>> var my = objectPlus(triangle, {bok: 4, wysokość: 4});
>>> my.pobierzPole()

8
>>> my.toString()

"figura, figura 2D, trójkąt, trójkąt"

Gołym okiem widać jedną różnicę: toString() zwraca ciąg znaków, w którym wyraz "trójkąt"
pojawia się dwa razy. Jest tak dlatego, że nasza konkretna instancja dziedziczy z trójkąt — 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()

"figura, figura 2D, trójkąt, trójkąciątko"

Dziedziczenie wielokrotne
Jeśli dziecko dziedziczy z więcej niż jednego rodzica, mamy do czynienia z dziedziczeniem
wielokrotnym. Niektóre języki obiektowe wspierają dziedziczenie wielokrotne, inne nie.
Zwolennicy obu rozwiązań mają w zanadrzu mocne argumenty: że dziedziczenie wielokrotne
jest bardzo wygodne, albo że to niepotrzebne komplikowanie architektury aplikacji, które
spokojnie można zastąpić odpowiednim łańcuchem pojedynczego dziedziczenia. Tak czy
inaczej, w językach dynamicznych (do których należy JavaScript) dziedziczenie wielokrotne
bardzo łatwo zaimplementować, nawet jeśli język nie został wyposażony w specjalną składnię
do obsługi tego mechanizmu. Przełóżmy dyskusję na temat wad i zalet dziedziczenia 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
};

var trójkąt = multi(figura, dwaDe, {


nazwa: 'trójkąt',
pobierzPole: function(){return this.bok * this.wysokość / 2;},
bok: 5,
wysokość: 10
});

Sprawdźmy, czy kod działa zgodnie z oczekiwaniami:


>>> trójkąt.pobierzPole()

25
>>> trójkąt.wymiary

2
>>> trójkąt.toString()

"trójkąt"

Zwróć uwagę, że multi() przechodzi przez parametry zgodnie z kolejnością, w jakiej zostały
podane. Jeśli dwa obiekty przekazane jako parametry będą miały pole o tej samej nazwie, 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
};

Funkcja tworząca obiekt trójkąt może:


Q sklonować obiekt dwaDe do obiektu o nazwie that (ang. „tamten”);
Q dodać do that nowe pola;
Q zwrócić that.
function trójkąt(b, w) {
var that = object(dwaDe);
that.nazwa ='trójkąt';
that.pobierzPole = function(){return this.bok * this.wysokość / 2;};
that.bok = b;
that.wysokość = w;
return that;
}

W związku z tym, że trójkąt() jest normalną funkcją, a nie konstruktorem, nie wymaga użycia
operatora new. Ponieważ jednak zwraca obiekt, przypadkowe użycie new nie zmieni jej działania.

193
JavaScript. Programowanie obiektowe

>>> var t = trójkąt(5, 10);


>>> t.wymiary

2
>>> var t2 = new trójkąt(5,5);
>>> t2.pobierzPole();

12.5

Oczywiście that to tylko nazwa, pozbawiona specjalnego znaczenia, jakie posiada this.

Wypożyczanie konstruktora
Kolejny (ostatni w tym rozdziale, przysięgam!) sposób implementacji dziedziczenia jest zwią-
zany z konstruktorami, a nie z samymi obiektami. Wzorzec polega na tym, że konstruktor
obiektu-dziecka wywołuje konstruktor obiektu-rodzica za pomocą metody call() lub apply().
Można to nazwać kradzieżą konstruktora lub, eufemistycznie, wypożyczeniem.

Metody call() i apply() zostały omówione w rozdziale 4. Krótkie przypomnienie: pozwalają


one wywoływać metody na zewnętrznym obiekcie, przekazywanym jako parametr, w taki sposób,
że wartość this danej metody zostaje ustawiona na przekazany obiekt. Dziedziczenie przez
wypożyczanie konstruktora polega na tym, że konstruktor dziecka wywołuje konstruktor rodzica,
przypisując do this nowo utworzony obiekt-dziecko.

Oto konstruktor rodzica:


function Figura(id) {
this.id = id;
}
Shape.prototype.nazwa = 'figura';
Shape.prototype.toString = function(){return this.nazwa;};

Zdefiniujmy teraz konstruktor Trójkąt(), który wywoła konstruktor Figura() za pomocą apply(),
przekazując this oraz wszelkie dodatkowe argumenty.
function Trójkąt() {
Figura.apply(this, arguments);
}
Trójkąt.prototype.nazwa = 'trójkąt';

Zwróć uwagę, że zarówno Trójkąt(), jak i Figura() dodają pewne dodatkowe pola do swoich
prototypów.

194
Rozdział 6. • Dziedziczenie

Utwórzmy nowy obiekt reprezentujący trójkąt:


>>> var t = new Trójkąt(101);
>>> t.nazwa

"Trójkąt"

Nowy obiekt dziedziczy pole id rodzica, ale nie dziedziczy niczego, co zostało dodane do 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);

Utwórzmy nowy egzemplarz:


>>> var t = new Trójkąt(202);
>>> t.id

202

195
JavaScript. Programowanie obiektowe

Obiekt ma własne pole id, jednak istnieje także druga wersja tego pola pochodząca z łańcu-
cha prototypów:
>>> t.__proto__.id

101
>>> delete t.id

true
>>> t.id

101

Pożycz konstruktor i skopiuj jego prototyp


Problem wynikający ze zdublowania pracy wykonywanej podczas wywoływania konstruktora
łatwo naprawić. Możesz wywołać apply() na konstruktorze rodzica w celu pobrania wszystkich
własnych pól, a następnie skopiować pola prototypu poprzez prostą iterację (lub za pomocą
omówionego wcześniej extend2()).
function Figura(id) {
this.id = id;
}
Figura.prototype.nazwa = 'figura';
Figura.prototype.toString = function(){return this.nazwa;};

function Trójkąt() {
Figura.apply(this, arguments);
}
extend2(Trójkąt, Figura);
Trójkąt.prototype.nazwa = 'trójkąt';

Sprawdzenie:
>>> var t = new Trójkąt(101);
>>> t.toString();

"trójkąt"
>>> t.id

101

Ani śladu podwójnego dziedziczenia:


>>> typeof t.__proto__.id

"undefined"

196
Rozdział 6. • Dziedziczenie

Dodatkowo extend2() umożliwia dostęp do pola uber:


>>> t.uber.name

"figura"

Podsumowanie
W tym rozdziale przedstawiłem kilka sposobów (wzorców) implementacji dziedziczenia. Można je
z grubsza podzielić na:
Q wzorce związane z konstruktorami,
Q wzorce związane z obiektami.

Można także rozróżnić wzorce w zależności od tego, czy:


Q korzystają z prototypu,
Q kopiują pola,
Q łączą obie możliwości.

Nr Nazwa wzorca Przykład Klasyfikacja Uwagi


1. Łańcuchy Dziecko.prototype = new Rodzic(); Związany Mechanizm domyślny,
prototypów z konstruktorami. opisany w standardzie
(wzorzec Korzysta ECMA.
pseudoklasowy) z prototypu. Wskazówka: pola
i metody wspólne dla
wszystkich instancji
należy przenieść do
prototypu, a pozostałe
przechowywać jako
własne pola obiektu.
2. Dziedziczenie Dziecko.prototype = Związany Większa efektywność,
samego ´Rodzic.prototype; z konstruktorami. ponieważ nie tworzy
prototypu Kopiuje prototyp się nowych instancji
(brak łańcucha tylko w celu
prototypów, obiekty dziedziczenia.
dzielą ten sam Szybkie przeszukanie
obiekt prototypu). prototypu, ponieważ
nie ma łańcucha.
Wada: dzieci mogą
zmienić funkcjonalności
rodziców.

197
JavaScript. Programowanie obiektowe

Nr Nazwa wzorca Przykład Klasyfikacja Uwagi


3. Konstruktor function extend(Dziecko, Rodzic) { Związany Inaczej niż w 1.,
tymczasowy var F = function(){}; z konstruktorami. dziedziczone są tylko
F.prototype = Rodzic.prototype; pola prototypu.
Dziecko.prototype = new F(); Korzysta z łańcucha
prototypów. Nie są dziedziczone
Dziecko.prototype.constructor =
Dziecko; własne pola (utworzone
Dziecko.uber = Rodzic.prototype; za pomocą this
} wewnątrz
konstruktora).
Wykorzystywany
w bibliotekach
Ext.js i YUI.
4. Kopiowanie function extend2(Dziecko, Rodzic) { Związany Wszystkie pola
pól prototypu var p = Rodzic.prototype; z konstruktorami. prototypu rodzica
var c = Dziecko.prototype; stają się polami
for (var i in p) { Kopiuje pola.
prototypu dziecka.
c[i] = p[i]; Korzysta z łańcucha
}
prototypów. Nie tworzy się nowych
c.uber = p; instancji jedynie na
} potrzeby dziedziczenia.
Krótsze łańcuchy
prototypów.
5. Kopiowanie function extendCopy(p) { Związany Bardzo prosty.
wszystkich pól var c = {}; z obiektami.
for (var i in p) { Wykorzystywany
(płytkie) Kopiuje pola. w Firebug oraz
c[i] = p[i];
} wczesnych wersjach
c.uber = p; jQuery i Prototype.js.
return c; Nie korzysta
} z prototypów.
6. Głębokie Jak wyżej, ale rekurencyjnie dla pól Związany Jak 5., ale obiekty są
kopiowanie będących obiektami. z obiektami. kopiowane przez
Kopiuje pola. wartość, a nie
przez referencję.
Stosowany w nowszych
wersjach jQuery.
7. Dziedziczenie function object(o){ Związany Brak pseudoklas:
prototypowe function F() {} z obiektami. obiekty dziedziczą
F.prototype = o; z obiektów.
return new F(); Korzysta z łańcucha
} prototypów. Wykorzystanie
właściwości prototypu.

198
Rozdział 6. • Dziedziczenie

Nr Nazwa wzorca Przykład Klasyfikacja Uwagi


8. Połączenie function objectPlus(o, dodatki) { Związany Połączenie
dziedziczenia var n; z obiektami. dziedziczenia
prototypowego function F() {} prototypowego (7.)
F.prototype = o; Korzysta z łańcucha
z kopiowaniem prototypów. i kopiowania pól (5.).
n = new F();
pól n.uber = o; Jedno wywołanie
(dziedziczenie for (var i in dodatki) {
Kopiuje pola.
umożliwia realizację
i rozszerzenie) n[i] = dodatki[i]; dziedziczenia
}
return n;
i rozszerzenie obiektu.
}
9. Dziedziczenie function multi() { Związany Implementacja zbliżona
wielokrotne var n = {}, dodatki, j = 0, z obiektami. do idei miksinów.
len = arguments.length;
for (j = 0; j < len; j++) { Kopiuje pola. Kopiowanie wszystkich
dodatki = arguments[j]; pól wszystkich
for (var i in dodatki) { obiektów-rodziców
n[i] = dodatki[i]; w kolejności ich
} pojawienia się na liście
}
return n;
parametrów.
}
10 Dziedziczenie function pasożyt(ofiara) { Związany Funkcja podobna
. pasożytnicze var that = object(ofiara); z obiektami. do konstruktora
that.more = 1; tworzy obiekty.
return that; Korzysta z łańcucha
} prototypów. Kopiuje obiekt,
rozszerza go
i zwraca kopię.
11 Wypożyczanie function Dziecko() { Związany Dziedziczy tylko
. konstruktorów Rodzic.apply(this, arguments); z konstruktorami. własne pola.
}
Można połączyć z 1.
w celu dziedziczenia
także prototypu.
Pozwala łatwo obejść
problem związany
z dziedziczeniem
przez dziecko pól
będących obiektami
(przekazywanych
przez referencję).
12 Wypożyczenie function Dziecko() { Związany Połączenie 11. i 4.
. konstruktora Rodzic.apply(this, arguments); z konstruktorami. Pozwala dziedziczyć
i skopiowanie } zarówno własne pola,
extend2(Dziecko, Rodzic); Korzysta z łańcucha
jego prototypu prototypów. jak i pola prototypu
bez podwójnego
Kopiuje pola. wywoływania
konstruktora rodzica.

199
JavaScript. Programowanie obiektowe

Którą z opcji wybrać? Wszystko zależy od Twojego stylu programowania, a także od projektu,
konkretnego zadania i specyfiki zespołu. Łatwiej Ci projektować aplikację w oparciu o klasy?
W takim wypadku wybierz któryś z wzorców korzystających z konstruktorów. Twoja „klasa”
będzie miała tylko jedną lub kilka instancji? Wybierz wzorzec korzystający z obiektów.

Czy to już wszystkie metody implementacji dziedziczenia? Bynajmniej. Możesz wybrać 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.

Studium przypadku: rysujemy kształty


Chciałbym zakończyć ten rozdział praktycznym przykładem dziedziczenia. Zadanie jest na-
stępujące: napisać zawierający jak najmniej kodu (i powtórzeń) program obliczający pola i obwody
różnych figur i potrafiący je rysować.

Analiza
Konstruktor Shape (figura) będzie zawierać wszystkie wspólne elementy. Oprócz niego 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 definicji figur będziemy używać punktów o współrzędnych x i y. Figura może posiadać


dowolnie wiele punktów. Trójkąt jest określany przez trzy punkty, a prostokąt (dla ułatwienia)
przez jeden punkt oraz długości boków. Obwód dowolnej figury to suma długości jej boków.
Pole zależy od kształtu, dlatego będzie implementowane osobno dla różnych figur.
Wspólne funkcjonalności, które trafią do Shape, to:
Q metoda draw() („rysuj”) potrafiąca narysować każdą figurę w oparciu o punkty;
Q metoda getPerimeter() („pobierz obwód”);
Q pole o nazwie points zawierające tablicę punktów;
Q potrzebne metody pomocnicze.

Do rysowania wykorzystamy znacznik <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

Zachęcam do eksperymentowania z działającym przykładem, który umieściłem tutaj: http://www.


phpied.com/files/canvas/. Wystarczy otworzyć konsolę Firebug i zacząć tworzyć nowe figury
w sposób, który przedstawię za chwilę.

Implementacja
Na początek dodajmy do pustej strony HTML znacznik <canvas> („płótno”):
<canvas height="600" width="800" id="canvas" />

Kod naszego skryptu musi znaleźć się pomiędzy znacznikami <script>:


<script type="text/javascript">
// ... tutaj należy umieścić kod
</script>

Przejdźmy wreszcie do samego skryptu.

Zaczniemy od konstruktora Point. Łatwiej się nie da:


function Point(x, y) {
this.x = x;
this.y = y;
}

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();
}

Teraz najtrudniejsze: metody Shape.prototype. Zdefiniujmy je przy użyciu notacji literałowej.


Działanie metod zostało objaśnione za pomocą komentarzy w kodzie:
Shape.prototype = {
// ustawienie wskaźnika na konstruktor
constructor: Shape,
// inicjalizacja, ustawia wskaźnik this.context na obiekt canvas
init: function() {
if (typeof this.context === 'undefined') {
var canvas = document.getElementById('canvas');
Shape.prototype.context = canvas.getContext('2d');
}
},
// metoda rysująca figurę za pomocą pętli przechodzącej przez points
draw: function() {
var ctx = this.context;
ctx.strokeStyle = this.getColor();
ctx.beginPath();
ctx.moveTo(this.points[0].x, this.points[0].y);
for(var i = 1; i < this.points.length; i++) {
ctx.lineTo(this.points[i].x, this.points[i].y);
}
ctx.closePath();
ctx.stroke();
},
// metoda generująca losowy kolor
getColor: function() {
var rgb = [];
for (var i = 0; i < 3; i++) {
rgb[i] = Math.round(255 * Math.random());
}
return 'rgb(' + rgb.join(',') + ')';
},
// metoda, która przechodzi przez tablicę punktów,
// tworzy instancje Line i dodaje je do this.lines
getLines: function() {
if (this.lines.length > 0) {
return this.lines;
}

202
Rozdział 6. • Dziedziczenie

var lines = [];


for(var i = 0; i < this.points.length; i++) {
lines[i] = new Line(this.points[i], (this.points[i+1]) ? this.points[i+1]
: this.points[0]);
}
this.lines = lines;
return lines;
},
// metoda obliczająca pole powierzchni, implementowana przez poszczególne dzieci
getArea: function(){},
// metoda obliczająca obwód poprzez sumowanie długości wszystkich boków (linii)
getPerimeter: function(){
var lines = this.getLines();
.var perim = 0;
for (var i = 0; i < lines.length; i++) {
perim += lines[i].length;
}
return perim;
}
}

Konstruktory dzieci — najpierw trójkąt:


function Triangle(a, b, c){
this.points = [a, b, c];
this.getArea = function(){
var p = this.getPerimeter();
var s = p / 2;
return Math.sqrt(
* (s - this.lines[0].length)
* (s - this.lines[1].length)
* (s - this.lines[2].length)
);
};
}

Konstruktor Triangle pobiera trzy obiekty reprezentujące punkty i zapamiętuje je w this.points


(swojej własnej kolekcji punktów). Następnie implementuje metodę getArea(), obliczającą
pole powierzchni przy użyciu wzoru Herona:
Pole = s(s-a)(s-b)(s-c)

s stanowi połowę obwodu trójkąta.

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

new Point(p.x + side_a, p.y), // prawy górny


new Point(p.x + side_a, p.y + side_b), // prawy dolny
new Point(p.x, p.y + side_b) // lewy dolny
];
this.getArea = function() {return side_a * side_b;};
}

Ostatni z konstruktorów dzieci to Square — kwadrat. Jest on szczególnym przypadkiem prosto-


kąta, dlatego skorzystamy z konstruktora Rectangle. Najwygodniej będzie wypożyczyć kon-
struktor.
function Square(p, side){
Rectangle.call(this, p, side, side);
}

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);

Przekażmy te punkty konstruktorowi Triangle:


>>> var t = new Triangle(p1, p2, p3);

Narysowanie trójkąta na płótnie oraz obliczenie obwodu i pola powierzchni:


>>> t.draw();
>>> t.getPerimeter()

482.842712474619

204
Rozdział 6. • Dziedziczenie

>>> t.getArea()

10000.000000000002

Teraz poeksperymentujmy z instancją Rectangle:


>>> var r = new Rectangle(new Point(200, 200), 50, 100);
>>> r.draw();
>>> r.getArea()

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()

Wynik powinien być mniej więcej taki:

Ć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.

Łączenie JavaScriptu z kodem HTML


JavaScript można osadzić w kodzie strony HTML przy użyciu znacznika <script>:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>JS test</title>
<script type="text/javascript" src="plik.js"></script>
</head>
JavaScript. Programowanie obiektowe

<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>.

BOM i DOM — przegląd


Kod w języku JavaScript osadzony wewnątrz strony HTML ma dostęp do obiektów, które można
podzielić na:
Q obiekty związane z obecnie załadowaną stroną (dokumentem);
Q obiekty zewnętrzne, takie jak ekran i okno przeglądarki.

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

Q informacje o Firefoksie w dokumentacji DOM Mozilli


(https://developer.mozilla.org/en/Gecko_DOM_Reference),
Q informacje o Internet Explorerze w dokumentacji Microsoftu
(http://msdn.microsoft.com/en-us/library/ms533050.aspx),
Q specyfikacje DOM na stronie W3C (http://www.w3.org/DOM/DOMTR).

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).

Ponownie odkrywamy obiekt window


Wiesz już, że każde środowisko JavaScriptu zapewnia obiekt globalny. W środowisku prze-
glądarki obiektem tym jest window. Wszystkie zmienne globalne stają się polami (własnościami)
tego obiektu.
>>> window.zmienna = 1;

1
>>> zmienna

Podobnie wszystkie wbudowane funkcje JavaScriptu (omówione w rozdziale 2.) są metodami


obiektu window.
>>> parseInt('123a456')

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

"Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.0.6) Gecko/2009011913


Firefox/3.0.6"

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.

Firebug jako ściąga


Firebug pozwala w wygodny sposób sprawdzić zawartość obiektu — dotyczy to także pól
BOM i DOM. Wpisz:
>>> navigator

210
Rozdział 7. • Środowisko przeglądarki

i kliknij wynik. Ten sam efekt da wpisanie


>>> console.dir(navigator)

Otrzymasz w ten sposób listę pól i ich wartości.

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.

Wyobraź sobie, że jesteś na stronie o następującym adresie URL: http://search.phpied.com:8080/


search?p=javascript#results.
>>> for(var i in location) {console.log(i + ' = "' + location[i] + '"')}
href = "http://search.phpied.com:8080/search?p=javascript#results"
hash = "#results"
host = "search.phpied.com:8080"
hostname = "search.phpied.com"
pathname = "/search"
port = "8080"
protocol = "http:"
search = "?p=javascript"

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')

Przeładować stronę można w następujący sposób:


>>> location.reload()

Ten sam efekt da „zmiana” wartości location.href na location.href.


>>> window.location.href = window.location.href

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

Możesz “skakać” po stronach w historii za pomocą metody history.go(). Poniższy fragment


kodu jest równoważny history.back()(przejście na ostatnią odwiedzoną stronę):
>>> history.go(-1);

Dwie strony wstecz:


>>> history.go(-2);

Przeładowanie bieżącej strony:


>>> history.go(0);

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()

W analogiczny sposób możliwe jest sięgnięcie z ramki do strony-rodzica:


>>> frames[0].parent === window

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

Pole self jest tym samym co window.


>>> self === window

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

Możesz sprawdzić ustawienia rozdzielczości:


>>> screen.width //szerokość

1440
>>> screen.availWidth //dostępna szerokość

1440

214
Rozdział 7. • Środowisko przeglądarki

>>> screen.height //wysokość

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.

window.open() pobiera następujące parametry:


Q URL strony, która ma się pojawić w nowym oknie;
Q nazwę nowego okna, której można użyć jako wartości atrybutu target formularza;
Q rozdzieloną przecinkami listę właściwości, między innymi:
Q resizable: informacja o tym, czy użytkownik ma prawo zmienić rozmiar okna;
Q width, height: odpowiednio szerokość i wysokość okna;
Q status: informacja o tym, czy pasek stanu ma być wyświetlony.

window.open() zwraca referencję do nowo utworzonego obiektu. Oto przykład:


var win = window.open('http://helion.pl', 'helion',
'width=300,height=300,resizable=yes');

win jest referencją do obiektu window nowego okienka. Jeśli okienko zostało zablokowane, win
otrzyma wartość fałszywą.

win.close() zamknie nowe okno.

Z powodów związanych z wygodą użytkowników lepiej trzymać się z dala od wyskakujących


okienek. Nie czyń drugiemu, co tobie niemiło! Oczywiście istnieją uzasadnione zastosowania
wyskakujących okienek, takie jak prezentacja informacji ułatwiających wypełnianie formularzy,
jednak z reguły ten sam cel można osiągnąć za pomocą innych, mniej inwazyjnych metod, na
przykład odpowiednio rozmieszczonych na stronie znaczników <div>.

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.

window.alert(), window.prompt(), window.confirm()


W rozdziale 2. pojawiła się funkcja alert(). Teraz już wiesz, że funkcje globalne są w rzeczy-
wistości metodami obiektu globalnego, zatem alert('Uwaga!') i window.alert('Uwaga!') to
dokładnie to samo.

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.

Można w ten sposób potwierdzać czynności użytkownika:


if (confirm('Czy na pewno chcesz usunąć ten element?')) {
// usuń
} else {
// zostaw
}

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.

window.prompt() umożliwia użytkownikowi wprowadzenie tekstu:


>>> var info = prompt('Jak masz na imię?'); console.log(info);

Pojawi się następujące okienko (na rysunku wersja Windows/Firefox):

Wartością zmiennej info będzie:


Q null, jeśli użytkownik kliknie Anuluj lub X albo wciśnie Esc.
Q "" (pusty łańcuch znaków), jeśli kliknie OK (lub wciśnie Enter), nie wprowadziwszy
żadnego tekstu.
Q Łańcuch znaków przechowujący wprowadzony tekst, jeśli użytkownik wpisze coś
w pole edycji, a następnie wybierze OK lub Enter.

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.

Poniższy kod jest poprawny, ale niezalecany:


var id = setInterval("alert('Uuu, uuu')", 2000);

218
Rozdział 7. • Środowisko przeglądarki

Preferowana jest następująca wersja:


var id = setInterval(
function(){
alert('uuu, uuu')
}, 2000
);

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).

Przykładowa strona HTML (umieściłem ją pod adresem http://phpied.com/files/jsoop/ch7.html):


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>My page</title>
</head>
<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<p id="closer">final</p>
<!-- koniec -->
</body>
</html>

Spójrz na drugi akapit (<p><em>second</em> paragraph</p>). Widzisz tam p zawarty wewnątrz


znacznika body — body jest rodzicem p, a p jest dzieckiem. Pierwszy i trzeci akapit są dziećmi
znacznika body i rodzeństwem drugiego akapitu. Znacznik em jest dzieckiem drugiego p, zatem p

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.

Core DOM i HTML DOM


Ostatnia dygresja przed przejściem do bardziej praktycznych przykładów. DOM może repre-
zentować zarówno dokumenty XML, jak i HTML. Właściwie to dokumenty HTML są szcze-
gólnym przypadkiem dokumentów XML. DOM poziomu pierwszego składa się ze specyfikacji
Core DOM (podstawowa specyfikacja DOM), obejmującej wszystkie dokumenty XML, oraz
z rozszerzającej ją specyfikacji HTML DOM. Oczywiście HTML DOM można stosować tylko do
dokumentów HTML. W tabeli poniżej umieściłem przykłady konstruktorów z Core DOM oraz
HTML DOM.

Konstruktor Dziedziczy z… Core czy HTML? Uwagi


Node Core Dowolny węzeł drzewa.
Document Node Core Obiekt document — główny punkt dostępu
do dowolnego dokumentu XML.
HTMLDocument Document HTML Może to być window.document lub po prostu
document — bardzo często używana wersja HTML
poprzedniego obiektu.
Element Node Core Każdy znacznik z kodu źródłowego jest
reprezentowany za pomocą elementu. „Element P”
odpowiada parze znaczników <p> i </p>.
HTMLElement Element HTML Ogólny konstruktor, z którego dziedziczą wszystkie
elementy HTML.
HTMLBodyElement HTMLElement HTML Element odpowiadający znacznikowi <body>.
HTMLLinkElement HTMLElement HTML Element A, odpowiadający parze znaczników
<a href="..."> i </a>.
.... HTMLElement HTML Masa innych elementów reprezentujących
pozostałe znaczniki HTML.

221
JavaScript. Programowanie obiektowe

Konstruktor Dziedziczy z… Core czy HTML? Uwagi


CharacterData Node Core Ogólny konstruktor obsługujący dane tekstowe.
Text CharacterData Core Tekst wewnątrz znacznika. Przykładowo fragmentowi
kodu <em>second</em> odpowiada węzeł elementu
EM oraz węzeł tekstowy o wartości "second".
Comment CharacterData Core <!-- dowolny komentarz -->
Attr Node Core Reprezentuje atrybut znacznika. W przypadku
<p id="closer"> atrybut id to obiekt DOM
utworzony za pomocą konstruktora Attr().
NodeList Core Lista węzłów — obiekt tablicopodobny posiadający
długość zdefiniowaną w polu length.
NamedNodeMap Core Jak wyżej, ale dostęp do węzłów można uzyskać
za pomocą ich nazw, a nie tylko za pomocą
liczbowego indeksu.
HTMLCollection HTML Podobny do dwóch powyższych, ale przeznaczony
dla HTML.

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.

Dostęp do węzłów DOM


Aby móc sprawdzić poprawność formularza lub podmienić obrazek na stronie internetowej,
musisz uzyskać dostęp do elementu, którego wartość Cię interesuje. Istnieje wiele sposobów
pobrania wartości elementu. Część z nich wymaga przejścia przez cały dokument i odnalezienia
konkretnego elementu, część pozwala korzystać ze skrótów.

Zachęcam Cię do eksperymentowania z wszystkimi omawianymi przeze mnie obiektami i meto-


dami. Przykłady, które zaraz nastąpią, są oparte na tym samym prostym dokumencie, który
pojawił się na początku podrozdziału poświęconego DOM (dokument jest dostępny pod ad-
resem http://www.phpied.com/files/jsoop/ch7.html). Otwórz konsolę Firebug… Zaczynamy!

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>

Jest to węzeł typu 1 (węzeł elementu):


>>> document.documentElement.nodeType

W przypadku węzłów elementu pola nodeName i tagName zawierają nazwę znacznika.


>>> document.documentElement.nodeName

"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>

Każde dziecko ma dostęp do rodzica za pośrednictwem pola parentNode:


>>> document.documentElement.childNodes[1].parentNode

<html>

Przypiszmy zmiennej referencję do elementu body:


>>> var bd = document.documentElement.childNodes[1];

Ile dzieci ma ten element?


>>> bd.childNodes.length

Dla przypomnienia, chodzi o element body następującego dokumentu:


<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<p id="closer">final</p>
<!-- koniec -->
</body>

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">

Przy użyciu hasAttributes() możesz sprawdzić, czy element posiada atrybuty:


>>> bd.childNodes[1].hasAttributes()

true

Ile atrybutów? W naszym przykładzie tylko jeden, atrybut class.

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"

Dostęp do zawartości znacznika


Jeszcze raz odwołajmy się do pierwszego akapitu:
>>> bd.childNodes[1].nodeName

"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"

Uproszczone metody dostępowe DOM


Przy użyciu childNodes, parentNode, nodeName, nodeValue oraz atrybutów możesz poruszać się
w górę i dół drzewa dokumentu i dowolnie go przekształcać. Jednak to, że białe znaki również są
uznawane za węzły tekstowe, nieco utrudnia przetwarzanie dokumentu w ten sposób. Mała
zmiana na stronie może uniemożliwić poprawne działanie skryptu. Poza tym przejście o jeden
poziom głębiej w strukturze drzewa może wymagać dość dużej ilości kodu. Dlatego właśnie
powstały uproszczone metody dostępowe DOM: getElementsByTagName(), getElementsByName(),
oraz getElementById().

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">

Pobranie zawartości pierwszego p:


>>> document.getElementsByTagName('p')[0].innerHTML

"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"

getElementsByTagName() zwraca wszystkie elementy na stronie:


>>> document.getElementsByTagName('*').length

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.

Kolejna metoda uproszczonego dostępu to getElementById(). Jest ona prawdopodobnie naj-


częściej stosowanym sposobem sięgania do elementów. Wystarczy przypisać identyfikatory
(atrybuty id) elementom, które mają być przetwarzane przez skrypt:
>>> document.getElementById('closer')

<p id="closer">

Rówieśnicy, body, pierwsze i ostatnie dziecko


nextSibling (następny rówieśnik2) oraz previousSibling (poprzedni rówieśnik) to kolejne pola
umożliwiające wygodną nawigację przez drzewo DOM w sytuacji, gdy mamy w ręku referen-
cję do jednego elementu:

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

>>> var akapit = document.getElementById('closer')


>>> akapit.nextSibling

"\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

Comment length=21 nodeName=#comment


>>> document.body.lastChild.previousSibling.nodeValue

" and that's about it "

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.

Spacer przez węzły DOM


Oto kod funkcji, która pobiera dowolny węzeł i rekurencyjnie przechodzi przez drzewo
DOM, począwszy od tego węzła.
function spacerDOM(n) {
do {
console.log(n);
if (n.hasChildNodes()) {
walkDOM(n.firstChild)
}
} while (n = n.nextSibling)
}

Przetestuj ją na następujących przykładach:


>>> walkDOM(document.documentElement)
>>> walkDOM(document.body)

Modyfikacja węzłów DOM


Znasz już wiele metod dających dostęp do węzłów drzewa DOM i do ich pól. Teraz dowiesz
się, w jaki sposób modyfikować węzły.

Niech zmienna my będzie wskaźnikiem na ostatni akapit naszego dokumentu.


>>> var my = document.getElementById('closer');

230
Rozdział 7. • Środowisko przeglądarki

Zmiana tekstu akapitu sprowadza się do zmiany wartości innerHTML:


>>> my.innerHTML = 'final!!!';

"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"

Nowy węzeł od razu staje się częścią drzewa:


>>> my.firstChild

<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";

"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"

Spójrzmy, jak wygląda znacznik <p> po wprowadzeniu zmian:


>>> my

<p id="further" align="right" style="border: 1px solid red; font-weight: bold;">

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.

Pobranie wszystkich pól wejściowych input:


>>> var inputs = document.getElementsByTagName('input');
>>> inputs.length;

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.

Pole przeznaczone na zapytanie:


>>> inputs[1].name;

"q"

232
Rozdział 7. • Środowisko przeglądarki

Możesz zmienić tekst zapytania, nadając odpowiednią wartość atrybutowi value:


>>> inputs[1].value = 'moje zapytanie';

"moje zapytanie"

Dla odprężenia zmieńmy napis na przycisku Szczęśliwy traf:


>>> inputs[3].value = inputs[3].value.replace(/Szczęśliwy/, 'Pechowy');

"Pechowy traf"

Możemy teraz zaimplementować naprawdę „pechową” funkcjonalność: niech przycisk poja-


wia się, a potem znika na sekundę. Realizująca to funkcja będzie się nazywała przełącz(). Przy
każdym wywołaniu funkcja sprawdzi wartość pola CSS visibility (widoczność), po czym ją
zmieni (z "visible" na "hidden" i odwrotnie).
function przełącz(){
var st = document.getElementsByTagName('input')[3].style;
st.visibility = (st.visibility === 'hidden') ? 'visible': 'hidden';
}

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)

Tworzenie nowych węzłów


Do tworzenia nowych węzłów służą metody createElement() (utwórz element) oraz create-
TextNode() (utwórz węzeł tekstowy). Nowe węzły dodaje się do drzewa dokumentu za pomocą
appendChild() (dodaj dziecko).

233
JavaScript. Programowanie obiektowe

Utworzenie nowego elementu p i ustawienie jego pola innerHTML:


>>> var myp = document.createElement('p');
>>> myp.innerHTML = 'jeszcze jeden';

"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'

"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)

<p style="border: 2px dotted blue;">

Oto porównanie wyglądu strony (http://www.phpied.com/files/jsoop/ch7.html) przed i po do-


daniu nowego węzła:

Metoda w pełni zgodna z DOM


Użycie innerHTML jest w pewnym sensie drogą na skróty. Ten sam efekt możesz uzyskać, sto-
sując czystą specyfikację DOM w następujący sposób:
1. Utwórz nowy węzeł tekstowy zawierający tekst "jeszcze jeden".
2. Utwórz nowy węzeł p (akapit).
3. Dodaj węzeł tekstowy jako dziecko akapitu.
4. Dodaj akapit jako dziecko body.

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>

Odpowiednia struktura drzewa wygląda mniej więcej tak:


element P
węzeł tekstowy o wartości "jeszcze jeden akapit "
element STRONG
węzeł tekstowy o wartości "pogrubiony"

Przedstawioną powyżej strukturę możesz utworzyć za pomocą następującego kodu:


// utwórz element P
var myp = document.createElement('p');
// utwórz węzeł tekstowy i dodaj do P
var myt = document.createTextNode('jeszcze jeden akapit ')
myp.appendChild(myt);
// utwórz element STRONG i dodaj do niego węzeł tekstowy
var str = document.createElement('strong');
str.appendChild(document.createTextNode('pogrubiony'));
// dodaj STRONG do P
myp.appendChild(str);
// dodaj P do BODY
document.body.appendChild(myp);

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ę.

Pobranie referencji do obiektu, który ma zostać sklonowany:


>>> var el = document.getElementsByTagName('p')[1];

el jest teraz wskaźnikiem do drugiego akapitu na stronie, który wygląda tak:


<p><em>second</em> paragraph</p>

Utwórzmy płytkę kopię el i dodajmy ją do węzła body:


>>> document.body.appendChild(el.cloneNode(false))

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))

Możesz skopiować sam element EM:


>>> document.body.appendChild(el.firstChild.cloneNode(true))

<em>

Kopiowanie samego węzła tekstowego zawierającego łańcuch znaków "second":


>>> document.body.appendChild(el.firstChild.firstChild.cloneNode(false))

"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>

Drugi akapit można usunąć w następujący sposób:


>>> var myp = document.getElementsByTagName('p')[1];
>>> var removed = document.body.removeChild(myp);

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>

Element P o identyfikatorze "closer" jest teraz drugim akapitem dokumentu.


>>> var p = document.getElementsByTagName('p')[1];
>>> p

<p id="closer">

Zastąpmy ten akapit akapitem przechowywanym w zmiennej removed:


>>> var replaced = document.body.replaceChild(removed, p);

Tak samo jak removeChild(), metoda replaceChild() zwraca referencję do węzła, który został
usunięty z drzewa.
>>> replaced

<p id="closer">

Znacznik <body> wygląda teraz tak:


<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<!-- and that's about it -->
</body>

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);

Obiekty DOM istniejące tylko w HTML


Jak już wspominałem, obiektowy model dokumentu można stosować w przypadku doku-
mentów XML i HTML. Wszystkie omówione powyżej sposoby przechodzenia przez drzewo
dokumentu oraz dodawania, usuwania i modyfikacji elementów stosują się zarówno do doku-
mentów XML, jak i HTML. Istnieją jednak pewne obiekty i pola, które mają zastosowanie tylko
w przypadku dokumentów HTML.

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].

document.body jest przykładem obiektu pochodzącego z prehistorycznej wersji DOM pozio-


mu zero, który został następnie przeniesiony do HTML-owego rozszerzenia specyfikacji
DOM. Obiektów tego typu jest więcej. Niektóre z nich nie posiadają odpowiedników w Core
DOM, inne owszem, ale zostały przeniesione z DOM 0 do specyfikacji HTML dla uproszczenia
i zachowania spójności. Opowiem teraz o tych obiektach.

238
Rozdział 7. • Środowisko przeglądarki

Starsze sposoby dostępu do dokumentu


Inaczej niż w przypadku DOM, który pozwala sięgnąć do dowolnego elementu (nawet do
komentarzy i białych znaków), wczesny JavaScript posiadał bardzo ograniczony dostęp do ele-
mentów dokumentu HTML. Był on możliwy głównie poprzez kolekcje:
Q document.images — kolekcja wszystkich obrazków na stronie, ten sam zbiór jest zwracany
przy zgodnym z Core DOM wywołaniu document.getElementsByTagName('img');
Q document.applets — odpowiada document.getElementsByTagName('applets');
Q document.links;
Q document.anchors;
Q document.forms.

document.links zawiera listę wszystkich znaczników <a href="..."></a> na stronie, czyli


wszystkich elementów A, które posiadają atrybut href. Kolekcja document.anchors zawiera
wszystkie znaczniki A z atrybutem name (<a name="..."></a>).

document.forms jest jedną z najczęściej stosowanych kolekcji, zawierającą wszystkie znaczniki


<form>. Dostęp do pierwszego formularza na stronie zapewnia następujący kod:
>>> document.forms[0]

Jest on równoważny następującej linii:


>>> document.getElementsByTagName('forms')[0]

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]'

"[email protected]"

Dynamiczne zablokowanie pola:


>>> document.forms[0].elements[0].disabled = true;

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.forms[0].elements['wyszukaj']; // notacja tablicowa


>>> document.forms[0].elements.wyszukaj; // pole obiektu

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>

Jest jeszcze document.writeln(), czyli wersja document.write(), która automatycznie dodaje


znak nowej linii na końcu tekstu. Poniższe dwie linie są równoważne:
>>> document.write('Uuu!\n');
>>> document.writeln('Uuu!');

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.

Pola cookies, title, referrer i domain


Cztery dodatkowe pola dokumentu, które zostaną omówione w poniższych akapitach, rów-
nież zostały przeniesione z DOM poziomu zerowego do HTML-owego rozszerzenia DOM
poziomu pierwszego. Jednak w odróżnieniu od wcześniej omówionych pól te cztery nie po-
siadają odpowiedników w Core DOM.

document.cookie to pole przechowujące łańcuch znaków będący treścią ciasteczek przesyła-


nych pomiędzy klientem a serwerem. Strona przesłana przez serwer może zawierać nagłówek
HTTP Set-Cookie (ustaw ciasteczko). Wysyłane do serwera żądanie klienta może zawierać infor-
macje z ciasteczka. Przy pomocy pola document.cookie możesz zmieniać treść ciasteczek przesy-
łanych do serwera. Jeśli wejdziesz na stronę cnn.com i wpiszesz w konsoli document.cookie,
zobaczysz tekst zbliżony do pokazanego poniżej:
>>> document.cookie

"CNNid=Ga50a0c6f-14404-1198821758-6; SelectedEdition=www;
s_sess=%20s_dslv%...

240
Rozdział 7. • Środowisko przeglądarki

document.title pozwala zmienić tytuł strony wyświetlany w oknie przeglądarki. Przykładowo


na stronie cnn.com może uruchomić kod:
>>> document.title = 'Mój tytuł'

"Mój tytuł"

Efekt będzie taki:

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].

document.referrer przechowuje URL poprzednio odwiedzonej strony. Jest to ta sama wartość,


którą przeglądarka przesyła w nagłówku HTTP Referer podczas żądania strony (przy okazji,
pojedyncze „r” w Referer jest błędem językowym, poprawna jest wersja z JavaScriptu, czyli
document.referrer, przez dwa „r”). Jeśli użytkownik przeszedł na stronę CNN ze strony Yahoo!,
otrzyma wynik zbliżony do pokazanego poniżej:
>>> document.referrer

"http://search.yahoo.com/search?p=cnn&vc=&fr=yfp-t-
105&toggle=1&cop=mss&ei=UTF-8&fp_ip=PL"

document.domain pozwala odczytać domenę aktualnie załadowanej strony. Jest to przydatne


podczas procesu zwanego domain relaxation. Załóżmy, że Twoja strona to www.yahoo.com i że
zawiera ona ramkę, która tak naprawdę znajduje się pod adresem music.yahoo.com. Są to dwie
różne domeny, zatem zabezpieczenia serwera nie pozwolą stronie komunikować się z ramką.
Możesz rozwiązać ten problem, ustawiając document.domain obu stron na yahoo.com, dzięki
czemu strony będą mogły wymieniać informacje.

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'

Illegal document.domain value" code: "1009

241
JavaScript. Programowanie obiektowe

>>> document.domain = 'www.example.org'

Illegal document.domain value" code: "1009

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.

Kod obsługi zdarzeń wpleciony w atrybuty HTML


Najprostszym sposobem reakcji na zdarzenie jest dodanie do znacznika odpowiedniego atry-
butu, na przykład:
<div onclick="alert('Ałka!')">kliknij!</div>

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.

Obserwatorzy zdarzeń DOM


Najlepszym sposobem programowania opartego o zdarzenia przeglądarki jest zastosowanie
podejścia związanego z obserwatorami zdarzeń (ang. event listeners), opisanego w specyfikacji
DOM poziomu drugiego. W tym modelu wiele funkcji może obserwować to samo zdarzenie.
Gdy zdarzenie pojawi się w systemie, zostaną wywołane wszystkie funkcje. Obserwatorzy nie
muszą wiedzieć o sobie nawzajem i mogą działać całkowicie niezależnie. Mogą podłączać się
i odłączać w dowolnym momencie bez wpływania na pozostałych obserwatorów.

Wróćmy do przykładu ze strony http://www.phpied.com/files/jsoop/ch7.html i skoncentrujmy


się na następującym fragmencie kodu HTML:
<p id="closer">final</p>

Kod w języku JavaScript może przypisać obserwatorów do zdarzenia kliknięcia za pomocą


metody addEventListener():
>>> var mypara = document.getElementById('closer');
>>> mypara.addEventListener('click', function() {alert('Uuu!')}, false);
>>> mypara.addEventListener('click', console.log, false);

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

Kliknięcie obiektu zdarzenia spowoduje wyświetlenie jego pól.

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

Załóżmy, że nieuporządkowana lista (znacznik <ul>) na stronie HTML zawiera link:


<body>
<ul>
<li><a href="http://phpied.com">mój blog</a></li>
</ul>
</body>

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.

Początkowo przeglądarki IE i Netscape implementowały (z powodu braku ustalonych stan-


dardów) dokładnie przeciwne rozwiązania. W IE istniało tylko bąbelkowanie, w Netscape
tylko przechwytywanie. Po wprowadzeniu specyfikacji DOM Firefox, Opera i Safari rozróżniają
wszystkie trzy fazy, ale IE nadal pozwala tylko bąbelkować.

245
JavaScript. Programowanie obiektowe

Jakie praktyczne konsekwencje mają różne sposoby propagacji zdarzeń?


Q Trzeci parametr metody addEventListener() pozwala zdecydować, czy powinno
zostać użyte przechwytywanie. Jeśli kod ma działać w różnych przeglądarkach
(a taka jest standardowa sytuacja), najlepiej ustawić wartość parametru na false
i korzystać jedynie z bąbelkowania.
Q Możesz przerwać propagację zdarzenia z wnętrza obserwatorów, dzięki czemu
zdarzenie nie będzie bąbelkowało i nigdy nie dojdzie do poziomu dokumentu.
W tym celu należy wywołać metodę stopPropagation() na obiekcie zdarzenia
(przykład wkrótce).
Q Można jeszcze delegować zdarzenia. Jeśli wewnątrz <div> znajduje się dziesięć
przycisków, możesz przypisać im dziesięć obserwatorów, po jednym dla przycisku.
Rozsądniej jednak będzie dodać jednego obserwatora do otaczającego przyciski
znacznika <div>, a po wystąpieniu zdarzenia sprawdzać, który z przycisków był
celem kliknięcia.

Ż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>

Zdefiniujmy funkcję, która będzie obsługiwała kliknięcia tego akapitu:


>>> function paraHandler(){alert('akapit kliknięty');}

Teraz przypiszmy tę funkcję jako obserwatora kliknięć:


>>> var para = document.getElementById('closer');
>>> para.addEventListener('click', paraHandler, false);

Przypiszmy obserwatorów kliknięć również do innych obiektów: do body, dokumentu oraz do


okna przeglądarki:
>>> document.body.addEventListener('click', function(){alert('body kliknięte')},
´false);
>>> document.addEventListener('click', function(){alert('dokument kliknięty')},
´false);
>>> window.addEventListener('click', function(){alert('okno kliknięte')}, false);

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

Jest to dowód na propagację (bąbelkowanie) zdarzeń od elementu będącego celem aż na sam


szczyt, do okna przeglądarki.

Przeciwieństwem addEventListener() jest metoda removeEventListener(), akceptująca te sa-


me parametry. Usuńmy obserwatora przypisanego akapitowi.
>>> para.removeEventListener('click', paraHandler, false);

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();
}

Dodanie zmienionego obserwatora:


>>> para.addEventListener('click', paraHandler, false);

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

Anulowanie zachowania domyślnego


Niektóre spośród zdarzeń przeglądarki posiadają zachowania domyślne. Przykładowo kliknię-
cie linku powoduje załadowanie strony, do której on prowadzi. Możliwe jest dołączenie ob-
serwatorów do kliknięć linków i anulowanie zachowania domyślnego. Jeśli chcesz uzyskać ten
efekt, musisz wywołać metodę preventDefault() (anuluj domyślne) na obiekcie zdarzenia.

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.

Obsługa zdarzeń w różnych przeglądarkach


Wiesz już, że większość współczesnych przeglądarek niemalże w pełni implementuje DOM
poziomu pierwszego. Niestety zdarzenia zostały objęte standaryzacją dopiero w DOM poziomu
drugiego. Z tego powodu istnieje kilka istotnych różnic pomiędzy Internet Explorerem a Fi-
refoksem, Operą i Safari.

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

W IE sprawy mają się nieco inaczej:


Q W IE nie ma metody addEventListener(), chociaż od wersji 5 istnieje odpowiadająca
jej metoda attachEvent(). We wcześniejszych wersjach możliwe jest jedynie
bezpośrednie odwołanie do pola (np. onclick).
Q Metodzie attachEvent() zamiast click trzeba przekazać onclick.
Q Jeśli obserwujesz zdarzenia w staromodny sposób (na przykład przy pomocy
przypisania funkcji do pola onclick), podczas wywołania funkcji nie zostanie jej
przekazany obiekt zdarzenia. Niezależnie od sposobu przypisania obserwatora,
w IE można pobrać zdarzenie z obiektu globalnego jako window.event.
Q W IE obiekt zdarzenia nie posiada pola target z informacją o elemencie, który
zapoczątkował zdarzenie. Zamiast tego posiada analogiczne pole srcElement.
Q Jak już wspominałem, przechwytywanie zdarzeń jest możliwe tylko dla niektórych
typów zdarzeń, dlatego należy stosować jedynie bębelkowanie.
Q Nie istnieje metoda stopPropagation(). Odpowiadające jej zachowanie można
wymusić, ustawiając wartość pola cancelBubble (istniejącego tylko w IE) na true.
Q Zamiast wywoływania metody preventDefault() należy ustawić pole returnValue
(znów: istniejące tylko w IE) na false.
Q W celu zakończenia obserwacji zdarzenia należy wywołać detachEvent(),
a nie removeEventListener().

Oto poprawiona wersja powyższego kodu, która zadziała w każdej przeglądarce:


function callback(evt) {
// przygotowanie
evt = evt || window.event;
.var target = (typeof evt.target !== 'undefined') ? evt.target :
evt.srcElement;
// wywołanie zwrotne
console.log(target.nodeName);
}
// rozpocznij obserwację (nasłuchiwanie) kliknięć
if (document.addEventListener){ // FF
document.addEventListener('click', callback, false);
} else if (document.attachEvent){ // IE
document.attachEvent('onclick', callback);
} else {
document.onclick = callback;
}

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ę.

Z XMLHttpRequest korzysta się dwuetapowo:


Q Pierwszy etap to wysłanie żądania: należy utworzyć obiekt XMLHttpRequest
i przydzielić mu obserwatora.
Q Drugi etap to przetworzenie odpowiedzi: obserwator jest informowany o nadejściu
odpowiedzi, po czym wykonuje odpowiednią czynność.

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();

Następnym krokiem jest przydzielenie obserwatora do zdarzenia readystatechange, urucha-


mianego przez ten obiekt:
xhr.onreadystatechange = myCallback;

Kolejny krok to wywołanie metody open():


xhr.open('GET', 'plik.txt', true);

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).

Ostatni krok to wysłanie przygotowanego żądania.


xhr.send('');

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

Wartość 4 oznacza, że nadeszła odpowiedź i można rozpocząć jej przetwarzanie. Wewnątrz


funkcji myCallback, oprócz upewnienia się, że readyState ma wartość 4, należy sprawdzić kod
statusu żądania HTTP. Przykładowo mogliśmy odwołać się do nieistniejącego adresu URL
i otrzymać błąd 404 (plik nie istnieje). Z naszego punktu widzenia szczególnie interesujący
jest kod 200 (OK). Kod statusu można odczytać w polu status obiektu XHR.

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.

Tworzenie obiektów XHR w IE w wersjach starszych niż 7


W wersjach Internet Explorera wcześniejszych niż wersja 7 XMLHttpRequest był obiektem
ActiveX, przez to jego instancje tworzyło się w nieco inny sposób:
var xhr = new ActiveXObject('MSXML2.XMLHTTP.3.0');

MSXML2.XMLHTTP.3.0 to identyfikator tworzonego obiektu. Istnieje kilka wersji obiektu XMLHttp


´Request. Jeśli osoba odwiedzająca Twoją stronę nie zainstalowała najnowszej wersji, możesz
spróbować z dwiema starszymi wersjami, zanim machniesz na to wszystko ręką.

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>.

Pliki do wgrania to:


Q content.txt: zwykły plik tekstowy, zawierający treść "I am a text file"
(„Jestem plikiem tekstowym”);
Q content.html: plik zawierający fragment kodu HTML (z informacją „Jestem
sformatowanym kodem HTML”):
"I am <strong>formatted</strong> <em>HTML</em>"
Q content.xml: plik XML („Jestem informacją w formacie XML”):
<?xml version="1.0" ?>
<root>
I'm XML data.
</root>

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.

Zacznijmy od utworzenia funkcji obsługującej operacje żądanie-odpowiedź:


function request(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = (function(myxhr){
return function(){
callback(myxhr);
}
})(xhr);
xhr.open('GET', url, true);
xhr.send('');
}

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;
}
);

Funkcje, która mają zostać wywołane po odebraniu odpowiedzi, są funkcjami anonimowymi.


Pierwsze dwie są prawie identyczne — wstawiają one zawartość pliku pomiędzy odpowiednią
parę znaczników <div>. Trzecia działa nieco inaczej, ponieważ musi obsłużyć dokument
XML. Pobiera ona dokument XML DOM z pola o.responseXML. Następnie przy użyciu
metody getElementsByTagName() pobiera wszystkie znaczniki <root> z dokumentu (wiemy, że
jest tylko jeden). Pierwszym dzieckiem tego elementu jest węzeł tekstowy, którego zawartość
("I'm XML data") funkcja pobiera za pośrednictwem pola nodeValue. Pozyskana w ten sposób
treść jest wstawiana do elementu <div> o identyfikatorze "xml". Efekt jest widoczny na zamiesz-
czonym poniżej zrzucie.

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().

Następnie przeszedłem do DOM (Document Object Model, obiektowy model dokumentu),


pozwalającego reprezentować dokumenty HTML (i XML) w postaci drzewa, w którym każdy
znacznik lub fragment tekstu staje się węzłem. Nauczyłem Cię:
Q metod dostępu do węzłów:
Q przy użyciu pól zawierających informację o relacjach rodzic – dziecko pomiędzy
elementami: parentNode, childNodes, firstChild, lastChild, nextSibling,
previousSibling;
Q przy użyciu metod getElementsById(), getElementsByTagName(),
getElementsByName().
Q sposobów modyfikacji węzłów:
Q przy użyciu innerHTML lub innerText bądź textContent;
Q przy użyciu nodeValue lub setAttribute(), albo poprzez odwoływanie się
do atrybutów jako do pól obiektu.
Q jak usuwać węzły, korzystając z removeChild() lub replaceChild();
Q jak dodawać nowe węzły przy użyciu appendChild(), cloneNode() i insertBefore().

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

Na koniec opowiedziałem o obiekcie XMLHttpRequest, pozwalającym tworzyć interaktywne


strony, które:
Q pobierają dane z serwera za pomocą żądań HTTP,
Q przetwarzają odpowiedź serwera w celu aktualizacji fragmentów strony.

Ć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

2.3. Napisz funkcję o nazwie include(), która na żądanie dołączy do kodu


zewnętrzne skrypty, czyli dynamicznie utworzy nowy znacznik <script>
i nada odpowiednią wartość jego atrybutowi src. Przetestuj funkcję,
uruchamiając następujący kod:
>>> include('skrypt.js');
2.4. Korzystając z funkcji z punktu 2.3, skonsumuj za pomocą JavaScriptu usługę
wyszukiwania Yahoo!. Dokumentacja znajduje się na stronie http://developer.
yahoo.com/search/web/V1/webSearch.html. Podczas konstruowania URL
żądania musisz ustawić output=json i callback=console.log. W ten sposób
wynik wywołania usługi zostanie wypisany w konsoli. Następnie zamień
console.log na wybraną przez siebie funkcję w celu uzyskania ciekawszych
efektów.
3. Zdarzenia
3.1. Utwórz obiekt użytkowy o nazwie myevent, który będzie posiadał następujące
metody, działające we wszystkich przeglądarkach:
Q addListener(element, nazwa_zdarzenia, funkcja): element może być
także tablicą elementów;
Q removeListener(element, nazwa_zdarzenia, funkcja);

Q getEvent(zdarzenie): zwraca window.event w starszych wersjach IE;


Q getTarget(zdarzenie): pobranie celu zdarzenia;
Q stopPropagation(zdarzenie);
Q preventDefault(zdarzenie).
Przykład użycia:
function myCallback(e) {
e = myevent.getEvent(e);
alert(myevent.getTarget(e).href);
myevent.stopPropagation(e);
myevent.preventDefault(e);
}
myevent.addListener(document.links, 'click', myCallback);
Efekt działania powyższego kodu ma być taki, że wszystkie linki w dokumencie
wyświetlają (w okienku alert()) wartość atrybutu href, ale nie powodują
przejścia na inną stronę.
3.2. Utwórz znacznik <div> o ustalonej pozycji bezwzględnej, na przykład
x=100px, y=100px. Napisz kod, który umożliwi przesuwanie elementu
po całej stronie przy użyciu klawiszy J (lewo), K (prawo), M (dół) i I (góra).
Wykorzystaj obiekt z zadania 3.1.

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.

W tym rozdziale omówione zostały dwa typy wzorców:


Q wzorce kodowania, z których większość jest bezpośrednio związana z JavaScriptem;
Q wzorce projektowe, niezależne od języka, które zostały rozpowszechnione przede
wszystkim dzięki Księdze Czterech.
JavaScript. Programowanie obiektowe

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

Q Należy w miarę możliwości zrezygnować ze stosowania atrybutu style w kodzie


HTML.
Q Znaczniki prezentacyjne, takie jak <font>, w ogóle nie powinny być stosowane.
Q Powinno się stosować znaczniki ze względu na ich znaczenie, a nie na to, jak
domyślnie wyświetla je przeglądarka. Zdarza się, że programiści stosują znacznik <div>
w miejscach, w których o wiele bardziej naturalne byłoby użycie <p>. Przykładem
dobrej praktyki kodowania jest rezygnacja ze znaczników <b> (pogrubienie) i <i>
(kursywa) na rzecz <strong> (ważne) i <em> (szczególny nacisk), ponieważ pierwsza
wspomniana para opisuje wygląd, a nie znaczenie.

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.

Oto wytyczne, które ułatwią odseparowanie warstwy zachowania od treści strony:


Q Staraj się możliwie ograniczyć liczbę znaczników <script>.
Q Unikaj osadzonych funkcji obsługi zdarzeń.
Q Nie umieszczaj wyrażeń CSS w kodzie strony.
Q Dynamicznie dodawaj znaczniki, których użycie nie ma sensu, gdy obsługa
JavaScriptu w przeglądarce użytkownika jest wyłączona.
Q Pod koniec opisu treści strony, tuż przed zamknięciem znacznika <body>, załącz
zewnętrzny plik z kodem w języku JavaScript.

Przykład wydzielenia warstwy zachowania


Wyobraź sobie, że Twoja strona zawiera formularz wyszukiwania, a poprawność wprowadzo-
nych do niego danych ma zostać sprawdzona za pomocą JavaScriptu. Znaczniki definiujące
formularz nie powinny zawierać żadnego kodu w języku JavaScript. Tuż przed zamykającym
znacznikiem <body> możesz umieścić znacznik <script>, załączający kod z zewnętrznego pliku.

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>

W pliku zachowanie.js do zdarzenia submit (wysłanie zapytania) dołączany jest obserwator,


który sprawdza, czy pole tekstowe nie jest puste — jeśli tak, to zawartość formularza nie zo-
stanie wysłana. Oto zawartość pliku zachowanie.js. Kod korzysta z obiektu użytkowego myevent,
opisanego w zadaniu 3.1 do rozdziału 7.
// inicjalizacja
myevent.addListener('myform', 'submit', function(e){
// nie ma potrzeby propagacji zdarzenia
e = myevent.getEvent(e);
myevent.stopPropagation(e);
// walidacja
var el = document.getElementById('szukaj');
if (!el.value) { // niestety, pole jest puste
myevent.preventDefault(e); // powstrzymaj wysyłanie formularza
alert('Proszę wprowadzić zapytanie. ');
}
});

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.

Obiekt w roli przestrzeni nazw


Stwórzmy obiekt globalny o nazwie MYAPP:
// globalna przestrzeń nazw
var MYAPP = MYAPP || {};

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 = {};

Dodanie metod do obiektu event odbywa się w normalny sposób:


// obiekt wraz z deklaracjami metod
MYAPP.event = {
addListener: function(el, type, fn) {
// ... ciało metody
},
removeListener: function(el, type, fn) {
// ...
},
getEvent: function(e) {
// ...
}
// ... inne pola i metody
};

Konstruktory w przestrzeniach nazw


Przestrzenie nazw nie uniemożliwiają tworzenia konstruktorów. Poniższy fragment kodu po-
kazuje, jak utworzyć obiekt użytkowy dom z konstruktorem Element, pozwalającym w wygod-
niejszy niż zwykle sposób tworzyć elementy DOM.
MYAPP.dom = {};
MYAPP.dom.Element = function(type, prop){
var tmp = document.createElement(type);
for (var i in prop) {
tmp.setAttribute(i, prop[i]);
}
return tmp;
}

W analogiczny sposób można dodać konstruktor Text, tworzący węzły tekstowe:


MYAPP.dom.Text = function(txt){
return document.createTextNode(txt);
}

Wykorzystanie konstruktora w celu utworzenia linku na dole strony:


var el1 = new MYAPP.dom.Element(
'a',
{href:'http://phpied.com'}
);

265
JavaScript. Programowanie obiektowe

var el2 = new MYAPP.dom.Text('Kliknij mnie!');


el1.appendChild(el2);
document.body.appendChild(el1);

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');

odpowiada nieco bardziej żmudnemu fragmentowi:


MYAPP.dom = {};
MYAPP.dom.style = {};

Metodę namespace() możesz napisać samodzielnie. Zacznij od rozdzielenia łańcucha przeka-


zanego jako parametr w miejscach wystąpienia kropek i umieść wynik tej operacji w tablicy.
Następnie dla każdego elementu tablicy dodaj pole do obiektu globalnego pod warunkiem, że
takie pole jeszcze nie istnieje.
var MYAPP = {};
MYAPP.namespace = function(nazwa){
var kawałki = nazwa.split('.');
var bieżący = MYAPP;
for (var i in kawałki) {
if (!bieżący[kawałki[i]]) {
bieżący[kawałki[i]] = {};
}
bieżący = bieżący[kawałki[i]];
}
}

Przetestuj metodę namespace() w następujący sposób:


MYAPP.namespace('event');
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

Rozgałęzianie kodu w czasie inicjalizacji


W poprzednim rozdziale zwróciłem Twoją uwagę na to, że często różne przeglądarki w od-
mienny sposób implementują te same lub podobne funkcjonalności. W takim wypadku niezbędne
jest rozgałęzienie kodu w zależności od tego, jakie możliwości oferuje przeglądarka aktualnie
wykonująca Twój skrypt. Jeśli takie rozgałęzianie ma miejsce zbyt często, skrypt będzie działał
wolniej, niż powinien.

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ć.

Zacznijmy od zdefiniowania przestrzeni nazw oraz przygotowania miejsca na metody reali-


zujące funkcjonalności obiektu użytkowego event.
var MYAPP = {};
MYAPP.event = {
addListener: null,
removeListener: null
};

Metody dodające (addListener()) i usuwające (removeListener()) obserwatorów nie zostały


jeszcze zaimplementowane. W oparciu o wykryte funkcjonalności przeglądarki można zdefi-
niować je w odmienny sposób.
if (typeof window.addEventListener === 'function') {
MYAPP.event.addListener = function(el, type, fn) {
el.addEventListener(type, fn, false);
};
MYAPP.event.removeListener = function(el, type, fn) {
el.removeEventListener(type, fn, false);
};
} else if (typeof document.attachEvent === 'function'){ // IE
MYAPP.event.addListener = function(el, type, fn) {
el.attachEvent('on' + type, fn);
};
MYAPP.event.removeListener = function(el, type, fn) {
el.detachEvent('on' + type, fn);
};
} else { // starsze przeglądarki
MYAPP.event.addListener = function(el, type, fn) {
el['on' + type] = fn;
};
MYAPP.event.removeListener = function(el, type, fn) {
el['on' + type] = null;
};
};

267
JavaScript. Programowanie obiektowe

Po wykonaniu tego skryptu metody addListener() i removeListener() będą zaimplementowa-


ne w sposób zgodny z przeglądarką, z której korzysta użytkownik. Kiedy któraś z nich zosta-
nie wywołana, nie będzie ona musiała sprawdzać cech przeglądarki, dzięki czemu będzie
działała szybciej.

Podczas wykrywania funkcjonalności przeglądarki należy zachować pewną ostrożność. Nie


jest dobrym pomysłem wyciąganie wniosków na podstawie zbadania obecności tylko jednej
funkcji. W powyższym przykładzie ta zasada została złamana, jako że kod sprawdza tylko ob-
sługę add* i w oparciu o wynik tego testu definiuje zarówno metodę add*, jak i remove*. Być
może założenie, że jeśli w nowej wersji przeglądarki IE pojawi się metoda addEventListener(), to
będzie jej towarzyszyła removeEventListener(), jest słuszne. Łatwiej już wyobrazić sobie, że
w IE pojawi się stopPropagation() bez preventDefault() — jeśli Twój kod sprawdza tylko jedną
z nich, to masz poważny problem. W powyższym kodzie zakładamy, że jeśli nie jest zdefinio-
wana funkcja addEventListener(), to przeglądarką jest IE, więc w takim wypadku dalsza
część kodu jest pisana pod konkretne potrzeby tej przeglądarki. Musisz mieć świadomość, że
sposób działania przeglądarki może ulec zmianie. Z tego powodu należy unikać generalizo-
wania i sprawdzać obecność poszczególnych metod przed ich użyciem.

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.

Przeanalizujmy to na przykładzie definicji funkcji addListener(). Początkowo funkcja posiada


ogólne ciało. Podczas pierwszego wywołania kod funkcji sprawdza oferowane przez przeglą-
darkę funkcjonalności, po czym zmienia definicję funkcji na najodpowiedniejszą w danej sy-
tuacji. Pod koniec pierwszego wywołania funkcja wywołuje samą siebie, by dołączyć odpo-
wiednie zdarzenie. Przy kolejnym wywołaniu funkcji jej implementacja będzie już ustalona
i nie będzie konieczne żadne dodatkowe rozgałęzianie.
var MYAPP = {};
MYAPP.myevent = {
addListener: function(el, type, fn){
if (typeof el.addEventListener === 'function') {
MYAPP.myevent.addListener = function(el, type, fn) {
el.addEventListener(type, fn, false);
};
} else if (typeof el.attachEvent === 'function'){
MYAPP.myevent.addListener = function(el, type, fn) {
el.attachEvent('on' + type, fn);

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');

O wiele lepszym rozwiązaniem jest wykorzystanie jednego parametru config, będącego


obiektem. Definicja funkcji może wyglądać tak:
MYAPP.dom.Button = function(text, conf) {
var type = conf.type || 'submit';
var font = conf.font || 'Verdana';
// ...
}

Przykład użycia konstruktora:


var config = {
font: 'Arial, Verdana, sans-serif',
color: 'white'
};
new MYAPP.dom.Button('Naciśnij!', config);

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.

Prywatne pola i metody


JavaScript nie umożliwia definiowania praw dostępu do pól i metod. Języki obiektowe korzy-
stające z klas z reguły posiadają następujące modyfikatory dostępu:

270
Rozdział 8. • Wzorce kodowania i wzorce projektowe

Q Publiczne: wszyscy użytkownicy obiektu mają dostęp do danego pola (metody).


Q Prywatne: tylko sam obiekt ma dostęp do danego pola (metody).
Q Chronione: tylko dany obiekt lub obiekt dziedziczący z niego ma dostęp do danego
pola (metody).

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ą.

Funkcje prywatne w roli metod publicznych


Wyobraź sobie, że istnieje funkcja, która absolutnie nie może zostać zmieniona, dlatego im-
plementujesz ją w sposób prywatny. Chcesz jednak, by oferowana przez nią funkcjonalność
była dostępna w różnych miejscach Twojego kodu. W takim wypadku można przypisać funkcję
prywatną polu dostępnemu w sposób publiczny.

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
};
})()

Jeśli teraz wywołasz MYAPP.dom.setStyle(), uruchomiona zostanie prywatna funkcja _setStyle().


Możesz z zewnątrz nadpisać setStyle():
MYAPP.dom.setStyle = function(){alert('b')};

Efekt będzie następujący:


Q MYAPP.dom.setStyle wskazuje nową funkcję.
Q MYAPP.dom.inna nadal wskazuje _setStyle().
Q _setStyle() zawsze będzie dostępne dla wewnętrznego kodu polegającego na tej
funkcji, niezależnie od wszelkich zewnętrznych zmian.

272
Rozdział 8. • Wzorce kodowania i wzorce projektowe

Funkcje samowywołujące się


Kolejny przydatny wzorzec pozwalający uniknąć zaśmiecania globalnej przestrzeni nazw po-
lega na opakowaniu kodu w funkcję anonimową oraz na natychmiastowym wywołaniu tej
funkcji. Dzięki temu wszystkie zmienne wewnątrz funkcji są lokalne (o ile użyta została de-
klaracja var) i są niszczone po zakończeniu wykonania funkcji, o ile nie są częścią żadnego
domknięcia. Ten wzorzec został dokładniej opisany w rozdziale 3.
function(){
// tu należy umieścić kod
})()

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

var obj = new MYAPP.dom.Element('span');


obj.setText('hello');
obj.setStyle('color', 'red');
obj.setStyle('font', 'Verdana');
document.body.appendChild(obj);

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.

JSON to popularny, lekki format wymiany danych. Podczas korzystania z XMLHttpRequest()


często okazuje się wygodniejszy niż XML. JSON to skrót od JavaScript Object Notation (no-
tacja obiektów języka JavaScript). Format ten ma tylko jedną charakterystyczną cechę — jest
wręcz niesamowicie wygodny. Opisuje on dane za pomocą literałów obiektowych i tablicowych.
Poniżej zamieszczam przykładowy łańcuch znaków, jaki mógłby zostać zwrócony przez serwer
w odpowiedzi na żądanie XHR.
{
'imię': 'Stoyan',
'nazwisko': 'Stefanov',
'publikacje': ['phpBB2', 'phpBB UG', 'PEAR']
}

274
Rozdział 8. • Wzorce kodowania i wzorce projektowe

Odpowiada temu mniej więcej taki dokument XML:


<?xml version="1.1" encoding="iso-8859-1"?>
<response>
<imię>Stoyan</imię>
<nazwisko>Stefanov</nazwisko>
<publikacje>
<publikacja>phpBB2</publikacja>
<publikacja>phpBB UG</publikacja >
<publikacja>PEAR</publikacja>
</publikacje >
</response>

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 + ')' );

Dostęp do poszczególnych fragmentów danych uzyskasz, traktując je jako pola obiektu:


alert(obj.imię); // Stoyan
alert(obj.publikacje[2]); // PEAR

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

Q wzorce konstrukcyjne, związane z tworzeniem obiektów;


Q wzorce strukturalne, opisujące zalecany sposób budowy obiektów dostarczających
nowych funkcjonalności;
Q wzorce czynnościowe, związane z komunikacją pomiędzy obiektami.

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.

Omówię cztery następujące wzorce:


Q singleton,
Q fabryka,
Q dekorator,
Q obserwator.

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ą.

Najprościej zaimplementować singleton za pomocą literału:


var single = {};

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

var other_log = new Logger();


other_log.log('jakieś inne zdarzenie');
alert(other_log === my_log); // true

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;
}

Działanie konstruktora jest zgodne z oczekiwaniami:


var a = new Logger();
var b = new Logger();
alert(a === b); // true

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.

Załóżmy, że możesz skorzystać z trzech różnych konstruktorów implementujących zbliżony


zakres funkcjonalności. Każdy z nich pobiera adres URL, ale przetwarza go na swój własny
sposób. Pierwszy tworzy węzeł tekstowy DOM, drugi link, a trzeci obrazek.
var MYAPP = {};
MYAPP.dom = {};
MYAPP.dom.Text = function() {
this.insert = function(where) {
var txt = document.createTextNode(this.url);
where.appendChild(txt);
};
};
MYAPP.dom.Link = function() {
this.insert = function(where) {
var link = document.createElement('a');
link.href = this.url;
link.appendChild(document.createTextNode(this.url));
where.appendChild(link);
};
};
MYAPP.dom.Image = function() {
this.insert = function(where) {
var im = document.createElement('img');
im.src = this.url;
where.appendChild(im);
};
};

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

var o = new MYAPP.dom.Image();


o.url = 'http://helion.pl/img/logo162_35.gif';
o.insert(document.body);
var o = new MYAPP.dom.Text();
o.url = 'http://helion.pl/img/logo162_35.gif';
o.insert(document.body);
var o = new MYAPP.dom.Link();
o.url = 'http://helion.pl/img/logo162_35.gif';
o.insert(document.body);

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.

Dodajmy metodę factory do obiektu użytkowego MYAPP.dom:


MYAPP.dom.factory = function(type) {
return new MYAPP.dom[type];
}

Zamiast sekwencji instrukcji if wystarczy napisać:


var o = MYAPP.dom.factory(type);
o.url = 'http://...'
o.insert()

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];
};

Pora na utworzenie pierwszego dekoratora, o nazwie CzerwoneBombki(), który będzie polem


obiektu drzewko (ponieważ należy unikać zaśmiecania globalnej przestrzeni nazw). Obiekt typu
CzerwoneBombki również posiada metodę decorate(), ale zawsze najpierw wywołuje metodę
decorate() rodzica.
drzewko.CzerwoneBombki = function() {
this.decorate = function() {
this.CzerwoneBombki.prototype.decorate();
alert('Powieś kilka czerwonych bombek');
}
};

W podobny sposób dodawane są dekoratory NiebieskieBombki() i Gwiazda():


drzewko.NiebieskieBombki = function() {
this.decorate = function() {
this.NiebieskieBombki.prototype.decorate();
alert('Dodaj niebieskie bombki');
}
};
drzewko.Gwiazda = function() {
this.decorate = function() {
this.Gwiazda.prototype.decorate();
alert('Gwiazda na czubku');
}
};

Pora dodać dekoratory do obiektu bazowego:


drzewko = drzewko.getDecorator('NiebieskieBombki');
drzewko = drzewko.getDecorator('Gwiazda');
drzewko = drzewko.getDecorator('CzerwoneBombki');

Wywołanie metody decorate():


drzewko.decorate();

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.

Pewnie widzisz tu istotne podobieństwo do obsługi zdarzeń przeglądarki — zdarzenia prze-


glądarki są jednym z przykładów zastosowania tego wzorca. Przeglądarka jest obiektem ob-
serwowanym: publikuje ona informacje o wystąpieniu zdarzeń (takich jak kliknięcie). Funkcje
obsługi zdarzeń podłączone do danego typu zdarzenia zostaną poinformowane o jego wystąpieniu.
Przeglądarka wyśle im obiekt reprezentujący dane zdarzenie. Twoja prywatna implementacja te-
go wzorca nie musi korzystać z obiektów zdarzeń. Możesz przesyłać dane dowolnego typu.

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.

Spójrzmy na przykładową implementację modelu push. Umieśćmy kod związany z implementa-


cją wzorca w osobnym obiekcie observer i wykorzystajmy go jako miksin, którego funkcjonal-
ności zostaną dodane do każdego obiektu obserwowanego. Dzięki temu każdy obiekt będzie
mógł być obserwowany, a każda funkcja będzie mogła być obserwatorem. Obiekt observer ma
następujące pola i metody:
Q Tablicę subscribers funkcji-obserwatorów, które mogą być wywoływane zwrotnie.
Q Metody addSubscriber() (dodaj obserwatora) i removeSubscriber() (usuń
obserwatora), które pozwalają dodawać lub usuwać obiekty z tablicy subscribers.
Q Metodę publish(), która pobiera dane i wywołuje wszystkie funkcje-obserwatorów,
przekazując im te dane.
Q Metodę make(), która pobiera dowolny obiekt i zamienia go w obiekt obserwowany,
dodając do niego wszystkie opisane powyżej metody.

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 = [];
}
}
};

Stwórzmy teraz kilka obiektów publikujących (obserwowanych). Mogą to być dowolne


obiekty — muszą tylko pamiętać o wywoływaniu metody publish() za każdym razem, gdy
zdarzy się coś ważnego. Przykładowo następujący obiekt blogger wywołuje metodę publish()
za każdym razem, gdy gotowy jest nowy post na blogu.
var blogger = {
napiszPost: function() {
var content = 'Dzisiaj jest ' + new Date();
this.publish(content);
}
};

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);
}
};

Dodanie do tych obiektów funkcjonalności obiektów obserwowanych jest bardzo proste:


observer.make(blogger);
observer.make(la_times);

283
JavaScript. Programowanie obiektowe

Weźmy teraz dwa proste obiekty jaś i adaś:


var jaś = {
czytaj: function(treść) {
console.log('Przeczytałem właśnie, że ' + treść)
}
};
var adaś = {
plotkuj: function(treść) {
console.log('Nie mów nikomu, że ci powiedziałem, ale ' + treść)
}
};

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();

"Przeczytałem właśnie, że Dzisiaj jest Sun Apr 06 2008 00:43:54 GMT+0100"


" Nie mów nikomu, że ci powiedziałem, ale Dzisiaj jest Sun Apr 06 2008 00:43:54
GMT+0100"

W dowolnym momencie adaś może przerwać nasłuchiwanie. W takim wypadku o nowych


postach informowany będzie już tylko jaś:
>>> blogger.removeSubscriber(adaś.plotkuj);
>>> blogger.writeBlogPost();

"Przeczytałem właśnie, że Dzisiaj jest Sun Apr 06 2008 00:44:37 GMT+0100"

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();

"Nie mów nikomu, że ci powiedziałem, ale Marsjanie wylądowali na Ziemi!"

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 nie mogą być używane jako zmienne.


var break = 1; // błąd składni

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

Lista słów zarezerwowanych mających


specjalne znaczenie w języku JavaScript
Q break
Q case
Q catch
Q continue
Q default
Q delete
Q do
JavaScript. Programowanie obiektowe

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

Lista słów zarezerwowanych


na użytek przyszłych implementacji
Q abstract
Q boolean
Q byte
Q char
Q class
Q const
Q debugger
Q double
Q enum
Q export
Q extends
Q final
Q float
Q goto

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

W tym dodatku zebrane zostały informacje na temat konstruktorów wbudowanych wymie-


nionych w standardzie ECMAScript oraz na temat pól i metod obiektów tworzonych przez te
konstruktory.

Object
Object() to konstruktor tworzący obiekty. Na przykład:
>>> var o = new Object();

Ten sam efekt da użycie literału obiektowego:


>>> var o = {}; // zalecane

Konstruktorowi Object() można przekazać dowolny argument — spróbuje on wtedy odgad-


nąć jego znaczenie i wywołać bardziej szczegółowy konstruktor. Przykładowo jeśli argumen-
tem będzie łańcuch znaków, wynik wywołania tego konstruktora będzie taki sam jak wynik
wywołania konstruktora String() na tym samym argumencie. Korzystanie z tej funkcjonalno-
ści nie jest zalecane.
>>> var o = new Object('coś');
>>> o.constructor

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.

Składowe konstruktora Object


Pole lub metoda Opis
Object.prototype Prototyp wszystkich obiektów (także samego Object). Wszystko,
co dodasz do tego prototypu, zostanie odziedziczone przez każdy
z istniejących i tworzonych obiektów.
>>> var s = new String('kluski');
>>> Object.prototype.custom = 1;
1
>>> s.custom
1

Składowe obiektów tworzonych przez konstruktor Object


Pole lub metoda Opis
constructor Zawiera wskaźnik na Object.
>>> Object.prototype.
constructor === Object
true
>>> var o = new Object();
>>> o.constructor === Object
true
toString(podstawa) Zwraca łańcuch znaków reprezentujący obiekt. Jeśli obiekt jest typu Number,
parametr podstawa zostanie uznany za podstawę przekształcenia. Domyślną
wartością jest 10.
>>> var o = {prop: 1};
>>> o.toString()
"[object Object]"
>>> var n = new Number(255);
>>> n.toString()
"255"
>>> n.toString(16)
"ff"

296
Dodatek C • Obiekty wbudowane

Pole lub metoda Opis


toLocaleString() Działa podobnie do toString(), ale stara się dostosować do ustawień
lokalnych. Ta wersja metody powinna być implementowana przez obiekty
takie jak Date(), które w zależności od ustawień na komputerze użytkownika
powinny w różny sposób formatować wyświetlane informacje.
valueOf() Zwraca wartość obiektu. W przypadku Object wartością jest this, jednak
inne typy obiektów mogą posiadać własną implementację. Przykładowo
obiekty Number zwracają wartość prostą, a obiekty Date znacznik czasu.
>>> var o = {};
>>> typeof o.valueOf()
"object"
>>> var n = new Number(101);
>>> typeof n.valueOf()
"number"
>>> var d = new Date();
>>> typeof d.valueOf()
"number"
>>> d.valueOf()
1208158875493
hasOwnProperty(pole) Zwraca true, jeśli dane pole jest polem własnym obiektu, a false, jeśli pole
zostało odziedziczone za pośrednictwem łańcucha prototypów. Zwraca false
również w przypadku, gdy takie pole nie istnieje.
>>> var o = {prop: 1};
>>> o.hasOwnProperty('prop')
true
>>> o.hasOwnProperty('toString')
false
isPrototypeOf(obj) Zwraca true, jeśli obiekt obj został użyty jako prototyp (niekoniecznie
bezpośredni) obiektu, na rzecz którego wywoływana jest metoda.
>>> var s = new String('');
>>> Object.prototype.isPrototypeOf(s)
true
>>> String.prototype.isPrototypeOf(s)
true
>>> Array.prototype.isPrototypeOf(s)
false
propertyIsEnumerable(pole) Zwraca true, jeśli dane pole jest widoczne w pętli for…in.
>>> var a = [1,2,3];
>>> a.propertyIsEnumerable('length')
false
>>> a. propertyIsEnumerable(0)
true

297
JavaScript. Programowanie obiektowe

Array
Konstruktor Array() tworzy tablicę.
>>> var a = new Array(1,2,3);

Ten sam efekt da użycie literału tablicowego:


>>> var a = [1,2,3]; //zalecane

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

[undefined, undefined, undefined]

Może to doprowadzić do nieoczekiwanych zachowań. Na przykład można użyć literału tabli-


cowego w następujący sposób:
>>> var a = [3.14]
>>> a

[3.14]

Jednak przekazanie pojedynczej liczby zmiennoprzecinkowej konstruktorowi Array zostanie


uznane za błąd:
>>> var a = new Array(3.14)

invalid array length

Składowe obiektów Array


Pole lub metoda Opis
length Liczba elementów tablicy.
>>> [1,2,3,4].length
4
concat(i1, i2, i3...) Scala kilka tablic w jedną.
>>> [1,2].concat([10,20], [300,400])
[1, 2, 10, 20, 300, 400]

298
Dodatek C • Obiekty wbudowane

Pole lub metoda Opis


join(separator) Zamienia tablice w łańcuchy znaków. separator ma postać łańcucha znaków,
którym domyślnie jest przecinek.
>>> [1,2,3].join()
"1,2,3"
>>> [1,2,3].join('|')
"1|2|3"
>>> [1,2,3].join(' to mniej niż ')
"1 to mniej niż 2 to mniej niż 3"
pop() Usuwa i zwraca ostatni element tablicy.
>>> var a = ['une', 'deux', 'trois'];
>>> a.pop()
"trois"
>>> a
["une", "deux"]
push(i1, i2, i3...) Dodaje elementy na końcu tablicy i zwraca długość tablicy po dodaniu.
>>> var a = [];
>>> a.push('zig', 'zag', 'zebra','zoo');
4
reverse() Odwraca kolejność elementów tablicy i zwraca tablicę w zmienionej postaci.
>>> var a = [1,2,3];
>>> a.reverse()
[3, 2, 1]
>>> a
[3, 2, 1]
shift() Działa jak pop(), ale usuwa pierwszy, a nie ostatni element tablicy.
>>> var a = [1,2,3];
>>> a.shift();
1
>>> a
[2, 3]
slice(indeks początkowy, Zwraca żądany fragment tablicy bez wprowadzania zmian w oryginale.
´indeks_końcowy)
>>> var a = ['jabłko', 'banan', 'js', 'css', 'pomarańcza'];
>>> a.slice(2,4)
["js", "css"]
>>> a
["jabłko", "banan", "js", "css", "pomarańcza"]

299
JavaScript. Programowanie obiektowe

Pole lub metoda Opis


sort(callback) Sortuje zawartość tablicy. Opcjonalnie jako parametr pobiera funkcję, która
zostanie wykorzystana do sortowania (jeśli podczas sortowania mają zostać
zastosowane niestandardowe zasady). Funkcja przekazywana jako callback
musi być w stanie przyjąć dwa elementy tablicy i zwrócić 0, jeśli są równe; 1,
jeśli pierwszy element jest większy; lub -1, jeśli większy jest drugi element.
Przykład niestandardowej funkcji sortującej, która porównuje liczby, a nie
znaki, jak ma to miejsce w przypadku domyślnym:
function customSort(a, b){
if (a > b) return 1;
if (a < b) return -1;
return 0;
}
Przykłady wywołań sort():
>>> var a = [101, 99, 1, 5];
>>> a.sort();
[1, 101, 5, 99]
>>> a.sort(customSort);
[1, 5, 99, 101]
>>> [7,6,5,9].sort(customSort);
[5, 6, 7, 9]
splice(start, ile, i1, Prawdopodobnie najpotężniejsza z funkcji tablicowych. Potrafi zarówno dodawać,
´i2, i3...) jak i usuwać elementy, na dodatek może wykonywać te czynności jednocześnie.
Pierwszy parametr określa miejsce, od którego ma się rozpocząć usuwanie,
drugi mówi, ile elementów ma zostać usuniętych, a reszta parametrów to nowe
elementy, które zostaną wstawione w miejsce usuniętych.
>>> var a = ['jabłko', 'banan', 'js', 'css', 'pomarańcza'];
>>> a.splice(2, 2, 'gruszka', 'ananas');
["js", "css"]
>>> a
var a = ['jabłko', 'banan', 'gruszka', 'ananas', 'pomarańcza'];
unshift(i1, i2, i3...) Podobna do push(), ale dodaje elementy na początku tablicy, a nie na końcu.
Podobna do shift(), ale dodaje elementy do tablicy, zamiast je usuwać.
Zwraca długość tablicy po wprowadzeniu zmian.
>>> var a = [1,2,3];
>>> a.unshift('raz', 'dwa');
5
>>> a
["raz", "dwa", 1, 2, 3]

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;');

Ten sam efekt da użycie literału funkcyjnego:


>>> var sum = function(a, b){return a + b;};

Najpopularniejsza jest następująca postać literału:


>>> function sum(a, b){return a + b;}

Zaleca się stosowanie literałów funkcyjnych, a nie konstruktora Function.

Składowe obiektów Function


Pole lub metoda Opis
apply(obiekt_this, Pozwala wywołać inną funkcję, nadpisując jej wartość this. Pierwszy parametr
´parametry) to obiekt, który wewnątrz funkcji ma być widoczny jako this, drugi to tablica
parametrów, które zostaną przekazane podczas wywołania funkcji.
function coToJest(){
return this.toString();
}
>>> var myObj = {};
>>> coToJest.apply(myObj);
"[object Object]"
>>> coToJest.apply(window);
"[object Window]"
call(obiekt_this, p1, Działa jak apply(), ale parametry przekazuje się osobno, a nie wewnątrz tablicy.
´p2, p3...)
length Liczba parametrów, których oczekuje funkcja.
>>> alert.length
1
>>> parseInt.length
2

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"

Obiekty Boolean nie posiadają żadnych pól poza odziedziczonymi z 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"

Składowe konstruktora Number


Pole lub metoda Opis
Number.MAX_VALUE Pole o wartości stałej (tylko do odczytu, czyli read-only). Przechowuje
najwyższą dozwoloną liczbę.
>>> Number.MAX_VALUE
1.7976931348623157e+308
>>> Number.MAX_VALUE = 101;
Number.MAX_VALUE is read-only
Number.MIN_VALUE Najmniejsza liczba dozwolona w języku JavaScript.
>>> Number.MIN_VALUE
5e-324
Number.NaN Przechowuje specjalną liczbę NaN („nieliczbę”).
>>> Number.NaN
NaN
Wartość NaN nie jest równa niczemu, nawet sobie samej.
>>> Number.NaN === NaN
false
Stosowanie Number.NaN jest bezpieczniejsze niż stosowanie samego NaN,
ponieważ NaN może zostać omyłkowo nadpisane.
>>> NaN = 1; // nigdy tego nie rób!
1
>>> NaN
1
>>> Number.NaN
NaN
Number.POSITIVE_INFINITY Przechowuje specjalną liczbę Infinity (nieskończoność). Pole to jest bardziej
godne zaufania niż globalna wartość Infinity, ponieważ jest polem tylko
do odczytu.
Number.NEGATIVE_INFINITY Wartość –Infinity, szczegóły jak powyżej.

303
JavaScript. Programowanie obiektowe

Składowe obiektów Number


Pole lub metoda Opis
toFixed(liczba) Zwraca łańcuch znaków reprezentujący daną liczbę w postaci stałoprzecinkowej.
Zwracana wartość jest zaokrąglana.
>>> var n = new Number(Math.PI);
>>> n.valueOf();
3.141592653589793
>>> n.toFixed(3)
"3.142"
toExponential(liczba) Zwraca łańcuch znaków reprezentujący daną liczbę w postaci wykładniczej.
Zwracana wartość jest zaokrąglana.
>>> var n = new Number(56789);
>>> n.toExponential(2)
"5.68e+4"
toPrecision(precyzja) Reprezentacja liczby w postaci łańcucha znaków. W zależności od liczby
zostanie użyta notacja stałoprzecinkowa lub wykładnicza.
>>> var n = new Number(56789);
>>> n.toPrecision(2)
"5.7e+4"
>>> n.toPrecision(5)
"56789"
>>> n.toPrecision(4)
"5.679e+4"
>>> var n = new Number(Math.PI);
>>> n.toPrecision(4)
"3.142"

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.

Utworzenie obiektu typu String oraz zmiennej typu prostego:


>>> var s_obj = new String('coś');
>>> var s_proste = 'coś';
>>> typeof s_obj

"object"

304
Dodatek C • Obiekty wbudowane

>>> typeof s_proste

"string"

Wynikiem porównania s_obj i s_proste za pomocą === będzie false:


>>> s_obj === s_prim

false
>>> s_obj == s_prim

true

Obiekty typu String posiadają pole length:


>>> s_obj.length

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

Składowe konstruktora String


Pole lub metoda Opis
String.fromCharCode Zwraca łańcuch znaków utworzony w oparciu o podane kody znaków:
´(kod1, kod2, kod3...)
>>> String.fromCharCode(115, 99, 114, 105, 112, 116);
"script"

Składowe obiektów String


Pole lub metoda Opis
length Liczba znaków w łańcuchu.
>>> new String('zero').length
4
charAt(pos) Zwraca znak znajdujący się na określonej pozycji, licząc od 0.
>>> "script".charAt(0);
"s"

305
JavaScript. Programowanie obiektowe

Pole lub metoda Opis


charCodeAt(pos) Zwraca kod znaku na określonej pozycji.
>>> "script".charCodeAt(0);
115
concat(str1, str2...) Zwraca nowy łańcuch znaków, będący wynikiem sklejenia argumentów.
>>> "".concat('R2', '-', 'D2');
"R2-D2"
indexOf(co, start) Jeśli parametr co pasuje do fragmentu łańcucha, zwracana jest pozycja
dopasowania. Opcjonalny drugi argument pozwala określić pozycję, od której
ma się rozpocząć wyszukiwanie. Jeśli dopasowanie nie zostanie odnalezione,
funkcja zwraca -1.
>>> "javascript".indexOf('scr')
4
>>> "javascript".indexOf('scr', 5)
-1
lastIndexOf(co, start) Jak indexOf(), ale wyszukiwanie rozpoczyna się od końca łańcucha. Ostatnie
wystąpienie "a":
>>> "javascript".lastIndexOf('a')
3
localeCompare(co) Porównuje dwa łańcuchy znaków w oparciu o bieżące ustawienia locale.
Zwraca 0, jeśli łańcuchy są równe; 1, jeśli co podczas sortowania poprzedza
obiekt; lub -1 w przeciwnym przypadku.
>>> "script".localeCompare('crypt')
1
>>> "script".localeCompare('sscript')
-1
>>> "script".localeCompare('script')
0
match(regexp) Pobiera wyrażenie regularne i zwraca tablicę dopasowań.
>>> "R2-D2 i C-3PO".match(/[0-9]/g)
["2", "2", "3"]
replace(co, na_co) Pozwala podmienić fragmenty tekstu pasujące do wyrażenia regularnego.
na_co może być łańcuchem lub funkcją działającą na dopasowaniach,
dostępnych jako $1, $2…$9.
>>> "R2-D2".replace(/2/g, '-dwa')
"R-dwa-D-dwa"
>>> "R2-D2".replace(/(2)/g, '$1$1')
"R22-D22"

306
Dodatek C • Obiekty wbudowane

Pole lub metoda Opis


search(regexp) Zwraca pozycje pierwszego dopasowania dla wyrażenia regularnego.
>>> "C-3PO".search(/[0-9]/)
2
slice(początek, koniec) Zwraca fragment łańcucha znaków wyznaczony przez pozycje początek
i koniec. Jeśli początek ma wartość ujemną, za pozycję początkową
przyjmowana jest wartość length + początek; podobnie w przypadku
ujemnej wartości koniec, za pozycję końcową zostanie przyjęta wartość
length + koniec.
>>> "R2-D2 i C-3PO".slice(4,11)
"2 i C-3"
>>> "R2-D2 i C-3PO".slice(4,-1)
"2 i C-3P"
split(separator, limit) Przekształca łańcuch w tablicę. Drugi parametr, limit, jest opcjonalny.
separator może być łańcuchem znaków lub wyrażeniem regularnym.
>>> "1,2,3,4".split(',')
["1", "2", "3", "4"]
>>> "1,2,3,4".split(',', 2)
["1", "2"]
substring(początek, Metoda podobna do slice(). Jeśli początek lub koniec są ujemne, traktowane są,
koniec) jakby miały wartość 0. Jeśli ich wartość jest większa niż długość łańcucha (length),
traktowane są, jakby miały wartość length. Jeśli koniec > początek, kolejność
argumentów jest zamieniana.
>>> "R2-D2 i C-3PO".substring(4, 11)
"2 i C-3"
>>> "R2-D2 i C-3PO".substring(11, 4)
"2 i C-3"
toLowerCase() Zamieniają wszystkie litery na małe.
toLocaleLowerCase() >>> "JAVA".toLowerCase()
"java"
toUpperCase() Zamieniają wszystkie litery na wielkie.
toLocaleUpperCase() >>> "script".toUpperCase()
"SCRIPT"

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

Składowe konstruktora Date


Pole lub metoda Opis
Date.parse(string) Metoda działa podobnie jak konstruktor new Date(), któremu
został przekazany łańcuch znaków. W przypadku powodzenia
zwraca znacznik czasu, w przeciwnym razie wartość NaN.
>>> Date.parse('May 4, 2008')
1209884400000
>>> Date.parse('4th')
NaN
Date.UTC(rok, miesiąc, dzień, Zwraca znacznik czasu uniwersalnego (UTC), a nie lokalnego.
godzina, minuta, sekunda, milisekunda)
>>> Date.UTC(2011, 0, 1, 13, 30, 35, 500)
1293888635500

308
Dodatek C • Obiekty wbudowane

Składowe obiektów Date


Pole lub metoda Opis
toUTCString() Działa jak toString(), ale zwraca czas uniwersalny. Różnica
pomiędzy czasem polskim a UTC:
>>> var d = new Date(2010, 0, 1);
>>> d.toString()
"Fri Jan 01 2010 00:00:00 GMT+0100"
>>> d.toUTCString()
"Thu, 31 Dec 2009 23:00:00 GMT"
toDateString() Zwraca tylko fragment toString() związany z datą (a nie czasem):
>>> new Date(2010, 0,1).toDateString();
"Fri Jan 01 2010"
toTimeString() Zwraca tylko fragment toString() związany z czasem:
new Date(2010, 0, 1).toTimeString();
"00:00:00 GMT+0100"
toLocaleString() Funkcje są odpowiednikami kolejno toString(), toDateString()
toLocaleDateString() i toTimeString(), ale zwracają informację w bardziej przyjaznym
toLocaleTimeString() formacie, zależnym od locale użytkownika:
>>> new Date(2010, 0, 1).toString();
"Fri Jan 01 2010 00:00:00 GMT+0100"
>>> new Date(2010, 0, 1).toLocaleString();
"1 styczeń 2010 00:00:00"
getTime() Metody służące do pobierania (get) lub ustawiania (set) czasu
setTime(time) przy użyciu znacznika czasu. W poniższym przykładzie data
jest przesuwana o jeden dzień do przodu:
>>> var d = new Date(2010, 0, 1);
>>> d.getTime();
1262300400000
>>> d.setTime(d.getTime() + 1000 * 60 * 60 * 24);
1262386800000
>>> d.toLocaleString()
"2 styczeń 2010 00:00:00"
getFullYear() Pobranie lub ustawienie roku przy użyciu czasu lokalnego
getUTCFullYear() lub uniwersalnego. Istnieje jeszcze metoda getYear(), ale
setFullYear(rok, miesiąc, dzień) nie rozwiązano w niej problemu roku 2000, dlatego należy
korzystać z getFullYear().
setUTCFullYear(rok, miesiąc, dzień)
>>> var d = new Date(2010, 0, 1);
>>> d.getYear()
110

309
JavaScript. Programowanie obiektowe

Pole lub metoda Opis


>>> d.getFullYear()
2010
>>> d.setFullYear(2011)
1293868800000
>>> d
Sat Jan 01 2011 00:00:00 GMT+0100
getMonth() Pobranie lub ustawienie miesiąca, licząc od 0 (styczeń):
getUTCMonth() >>> var d = new Date(2010, 0, 1);
setMonth(miesiąc, dzień) >>> d.getMonth()
setUTCMonth(miesiąc, dzień) 0
>>> d.setMonth(11)
1291190400000
>>> d.toLocaleDateString()
"1 grudzień 2010"
getDate() Pobranie lub ustawienie dnia miesiąca.
getUTCDate() >>> var d = new Date(2010, 0, 1);
setDate(dzień) >>> d.toLocaleDateString()
setUTCDate(dzień) "1 styczeń 2010"
>>> d.getDate();
1
>>> d.setDate(31);
1264924800000
>>> d.toLocaleDateString()
"31 styczeń 2010"
getHours() Pobranie lub ustawienie godziny, minuty, sekundy i milisekundy,
getUTCHours() wszystkie liczone od 0.
setHours(godz, min, sek, ms) >>> var d = new Date(2010, 0, 1);
setUTCHours(godz, min, sek, ms) >>> d.getHours() + ':' + d.getMinutes()
getMinutes() "0:0"
getUTCMinutes() >>> d.setMinutes(59)
setMinutes(min, sek, ms) 1262336399000
setUTCMinutes(min, sek, ms) >>> d.getHours() + ':' + d.getMinutes()
getSeconds() "0:59"
getUTCSeconds()
setSeconds(sek, ms)
setUTCSeconds(sek, ms)
getMilliseconds()
getUTCMilliseconds()
setMilliseconds(ms)
setUTCMilliseconds(ms)

310
Dodatek C • Obiekty wbudowane

Pole lub metoda Opis


getTimezoneOffset() Zwraca różnicę pomiędzy czasem lokalnym a uniwersalnym
wyrażoną w minutach. Różnica pomiędzy czasem w Polsce a UTC:
>>> new Date().getTimezoneOffset()
-60
>>> -60/60
1
getDay() Zwraca dzień tygodnia, licząc od 0 (niedziela):
getUTCDay() >>> var d = new Date(2010, 0, 1);
>>> d.toLocaleDateString()
"Friday, January 01, 2010"
>>> d.getDay()
5
>>> var d = new Date(2010, 0, 3);
>>> d.toLocaleDateString()
"3 styczeń 2010"
>>> d.getDay()
0

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

Składowe obiektu Math


Pole lub metoda Opis
Stałe matematyczne, wszystkie tylko do odczytu. Oto ich wartości:
Math.E >>> Math.E
2.718281828459045
Math.LN10 >>> Math.LN10
2.302585092994046
Math.LN2 >>> Math.LN2
0.6931471805599453
Math.LOG2E >>> Math.LOG2E
1.4426950408889634
Math.LOG10E >>> Math.LOG10E
0.4342944819032518
Math.PI >>> Math.PI
3.141592653589793
Math.SQRT1_2 >>> Math.SQRT1_2
0.7071067811865476
Math.SQRT2 >>> Math.SQRT2
1.4142135623730951
Math.acos(x) Funkcje trygonometryczne.
Math.asin(x)
Math.atan(x)
Math.atan2(y, x)
Math.cos(x)
Math.sin(x)
Math.tan(x)
round() zwraca najbliższą liczbę całkowitą, ceil() zaokrągla w górę,
zaś floor() w dół.
Math.round(x) >>> Math.round(5.5)
6
Math.floor(x) >>> Math.floor(5.5)
5
Math.ceil(x) >>> Math.ceil(5.1)
6
max() zwraca największą, a min() najmniejszą spośród liczb przekazanych
im w postaci argumentów. Jeśli choć jeden z parametrów wejściowych
ma wartość NaN, wynikiem również będzie NaN.

312
Dodatek C • Obiekty wbudowane

Pole lub metoda Opis


Math.max(num1, num2, >>> Math.max(2, 101, 4.5)
num3...)
101
Math.min(num1, num2, >>> Math.min(2, 101, 4.5)
num3...)
2
Math.abs(x) Wartość absolutna.
>>> Math.abs(-101)
101
>>> Math.abs(101)
101
Math.exp(x) Funkcja wykładnicza: Math.E do potęgi x.
Math.log(x) Logarytm naturalny z x.
Math.sqrt(x) Pierwiastek kwadratowy z x.
>>> Math.sqrt(9)
3
>>> Math.sqrt(2) === Math.SQRT2
true
Math.pow(x, y) x do potęgi y.
>>> Math.pow(3, 2)
9
Math.random() Losuje liczbę pomiędzy 0 (włącznie) a 1.
>>> Math.random()
0.8279076443185321

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

Więcej informacji na temat wyrażeń regularnych znajdziesz w rozdziale 4. oraz w dodatku D.

313
JavaScript. Programowanie obiektowe

Składowe obiektów RegExp


Pole lub metoda Opis
global Tylko do odczytu. Ma wartość true, jeśli podczas tworzenia obiektu został ustawiony
modyfikator g.
ignoreCase Tylko do odczytu. Ma wartość true, jeśli podczas tworzenia obiektu został ustawiony
modyfikator i.
multiline Tylko do odczytu. Ma wartość true, jeśli podczas tworzenia obiektu został ustawiony
modyfikator m.
lastIndex Przechowuje pozycję łańcucha znaków, od której będzie wyszukiwane kolejne
dopasowanie. test() i exec() ustawiają tę wartość po udanym dopasowaniu.
To pole ma znaczenie tylko, jeśli został użyty modyfikator g.
>>> var re = /[dn]o+dle/g;
>>> re.lastIndex
0
>>> re.exec("noodle doodle");
["noodle"]
>>> re.lastIndex
6
>>> re.exec("noodle doodle");
["doodle"]
>>> re.lastIndex
13
>>> re.exec("noodle doodle");
null
>>> re.lastIndex
0
source Tylko do odczytu. Zwraca wzorzec wyrażenia (bez modyfikatorów).
>>> var re = /[nd]o+dle/gmi;
>>> re.source
"[nd]o+dle"
exec(string) Próbuje dopasować wejściowy łańcuch do wyrażenia regularnego. W przypadku
powodzenia zwraca tablicę zawierającą dopasowanie oraz ewentualne podgrupy.
Jeśli został użyty modyfikator g, metoda zwróci pierwsze dopasowanie i zmieni
wartość pola lastIndex. Jeśli dopasowanie nie jest możliwe, metoda zwraca null.
>>> var re = /([dn])(o+)dle/g;
>>> re.exec("noodle doodle");
["noodle", "n", "oo"]
>>> re.exec("noodle doodle");
["doodle", "d", "oo"]

314
Dodatek C • Obiekty wbudowane

Pole lub metoda Opis


test(string) Działa podobnie jak exec(), ale zwraca tylko true lub false.
>>> /noo/.test('Noodle')
false
>>> /noo/i.test('Noodle')
true

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).

Składowe obiektów Error


Pole lub metoda Opis
name Nazwa konstruktora, który został użyty podczas tworzenia obiektu błędu:
>>> var e = new EvalError('Ojejku');
>>> e.name
"EvalError"
message Dodatkowa informacja o błędzie:
>>> var e = new Error('Ojejku... znowu');
>>> e.message
"Ojejku... znowu"

315
JavaScript. Programowanie obiektowe

316
D

Wyrażenia regularne

Za pomocą wyrażeń regularnych (omówionych w rozdziale 4.) można wyszukiwać w łańcu-


chach znaków określone fragmenty, na przykład:
>>> "fragment tekstu".match(/me/)

["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

documentElement.nodeType, 224 domena załadowanej strony, 241


documentElement.tagName, 224 domknięcie, 90, 94
dodawanie elementów tablicy, 57 iterator, 99
dodawanie kodu HTML do strony, 240 pętle, 96
dodawanie składowych, 156 przerwanie łańcucha, 93
dokumenty dostęp do dokumentu, 239
HTML, 24 dostęp do obiektu-rodzica, 180
XML, 221 dostęp do węzłów DOM, 222
DOM, 208, 219, 257 dostęp do własności obiektu, 106
appendChild(), 233, 236 dostęp do zawartości znacznika, 226
atrybuty, 225 drzewo DOM, 220
body, 228 dziedziczenie, 29, 31, 171
cloneNode(), 235 dostęp do obiektu-rodzica, 180
createElement(), 233 extend(), 181
createTextNode(), 233 głębokie kopiowanie, 187, 198
document, 223 implementacja, 172
documentElement, 224 konstruktor tymczasowy, 178
dostęp do węzłów, 222 kopiowanie pól, 182, 190
dostęp do zawartości znacznika, 226 kopiowanie pól prototypu, 198
drzewo, 220 kopiowanie przez referencję, 184
formularze, 232 kopiowanie wszystkich pól, 198
getElementById(), 227 kształty, 200
getElementsByName(), 227 łańcuch prototypów, 172
getElementsByTagName(), 227 miksiny, 193
insertBefore(), 236 obiekty, 186
klonowanie węzłów, 235 object(), 189
kopiowanie węzłów, 235 pasożytnicze dziedziczenie, 193, 199
modyfikacja stylu, 231 prototypy, 177, 190, 197, 198
modyfikacja węzłów, 230 przenoszenie wspólnych pól do prototypu, 175
obiekty istniejące tylko w HTML, 238 uber, 180
obserwator zdarzenia, 243 wielokrotne dziedziczenie, 191, 199
ostatnie dziecko, 228 wypożyczanie konstruktora, 194, 199
pierwsze dziecko, 228 dziel i zwyciężaj, 29
propagacja zdarzeń, 245
przechodzenie przez węzły, 230
removeChild(), 236
E
replaceChild(), 237 ECMA, 24
rówieśnicy, 228 ECMA-262, 24
tworzenie węzłów, 233 ECMAScript, 24, 26
uproszczone metody dostępowe, 227 ekran, 209
usuwanie węzłów, 236 Element, 221
węzeł document, 223 elementy, 105
węzły, 222 else, 61
węzły-dzieci, 224 encodeURI(), 76, 80, 292
wstawianie węzłów, 236 encodeURIComponent(), 76, 80, 292
wyszukiwanie w strukturze drzewa XML, 224 error, 250
DOM Inspector, 220 Error, 117, 146, 315
domain, 240 składowe, 315
domain relaxation, 241 escape(), 80

325
Skorowidz

eval(), 76, 80, 275, 293 isFinite(), 79, 292


EvalError, 315 isNaN(), 79, 292
event listeners, 243 nazwy, 74
eventPhase, 245 parametry, 74
exec(), 314 parseFloat(), 78, 291
extend(), 181 parseInt(), 76, 291
predefiniowane, 76
przekazywanie obiektów, 114
F return, 74
fabryka, 278 samowywołujące się, 87, 273
false, 49, 128 unescape(), 80
figury, 200, 202 wewnętrzne, 87
finally, 148 wywołanie, 74
Firebug, 31, 32 wywołanie zwrotne, 84
obiekty, 115 zapis literałowy, 83
poziom ostrzeżeń, 33 zasada czarnej skrzynki, 76
sprawdzanie zawartości obiektu, 210 zasięg leksykalny, 91
wpisanie kodu, 32 zasięg zmiennych, 81
Firefox, 31 zastępowanie oryginalnej funkcji, 89
firstChild, 229 zwracanie funkcji, 88
Flash, 26 zwracanie obiektów, 113
Flex, 26 zwracanie wartości, 74
focus, 250 funkcje dostępowe, 98
for, 66 get, 98
for…in, 69, 160 set, 98
format JSON, 274 funkcje prywatne, 87
formularze, 232 metody publiczne, 272
zdarzenia, 250
function, 74 G
Function, 117, 122, 301
apply(), 125 get, 98
call(), 125 GET, 251
caller, 123 getAttribute(), 226
length, 123 getDate(), 310
prototype, 124, 155 getElementById(), 227, 228, 254
składowe, 301 getElementsByName(), 227
toString(), 125 getElementsByTagName(), 227, 228, 254, 256
funkcje, 73, 122, 291, 301 getFullYear(), 309
alert(), 81 getHours(), 310
anonimowe, 84 getMilliseconds(), 310
argumenty, 74 getMinutes(), 310
ciało, 74 getMonth(), 310
decodeURI(), 80, 293 getSeconds(), 310
decodeURIComponent(), 80, 292 getTime(), 309
deklaracja, 74 getTimezoneOffset(), 311
encodeURI(), 80, 292 getUTCDate(), 310
encodeURIComponent(), 80, 292 getUTCDay(), 311
escape(), 80 getUTCFullYear(), 309
eval(), 80, 293 getUTCHours(), 310

326
Skorowidz

getUTCMilliseconds(), 310 Internet Explorer, 24


getUTCMinutes(), 310 isFinite(), 76, 79, 292
getUTCMonth(), 310 isNaN(), 76, 79, 292
getUTCSeconds(), 310 isPrototypeOf(), 162, 297
global, 314 iteracja po elementach tablicy, 69
głębia koloru, 214 iterator, 99
głęboka kopia, 187 izolowanie zachowania, 262
głębokie kopiowanie, 187
Gmail, 23, 25
Google Gears, 26
J
Google Maps, 23, 25, 34 JavaScript, 19, 23, 24, 251
JavaScript Object Notation, 274
H język
JavaScript, 23
hasAttributes(), 225 LiveScript, 24
hasChildNodes(), 224 join(), 120, 134, 299
hasOwnProperty(), 161, 297 JSON, 274
height, 215 JSON.parse(), 275
hermetyzacja, 28 json_decode(), 275
hiperłącza, 24 json_encode(), 275
historia odwiedzonych stron, 212
history.back(), 212
history.forward(), 212
K
history.go(), 213 kapsułkowanie, 28
HTML, 23, 207 keydown, 250
HTML DOM, 221 keypress, 250
HTMLCollection, 222 keyup, 250
HTMLDocument, 221 klasy, 28
HTMLElement, 221 bazowe, 180
HTMLHeadElement(), 221 klawiatura, 250
HTMLLinkElement, 221 kliknięcie, 242
klonowanie węzłów, 235
I kod HTML, 207
kolory, 42
IE, 24 komentarze, 70
XMLHttpRequest, 253 kompozycja, 29
if, 61 konfiguracja środowiska rozwijania aplikacji, 31
alternatywna składnia, 63 konkatenacja łańcuchów, 46
else, 61 konstruktory, 109
ignoreCase, 314 przestrzenie nazw, 265
in_array(), 165 tymczasowy konstruktor, 178, 198
indexOf(), 132, 306 konwersja łańcuchów, 46
Infinity, 43 kopiowanie pól, 182, 190
informacje o przeglądarce, 210 kopiowanie pól prototypu, 198
inicjalizacja zmiennej, 36 kopiowanie przez referencję, 184
inkrementacja, 39 kształty, 200
innerHTML, 226, 231, 234
insertBefore(), 236
instanceof, 112

327
Skorowidz

L Math.E, 135, 312


Math.exp(), 313
lastChild, 229 Math.floor(), 136, 312
lastIndex, 314 Math.LN10, 135, 312
lastIndexOf(), 133, 306 Math.LN2, 135, 312
length, 298, 301, 305 Math.log(), 313
leniwe definicje, 268 Math.LOG10E, 312
leniwe wartościowanie, 52 Math.LOG2E, 312
liczby, 40, 41, 302 Math.max(), 136, 313
całkowite, 41 Math.min(), 136, 313
heksadecymalne, 42 Math.PI, 135, 312
nieskończoność, 43 Math.pow(), 136, 313
ósemkowe, 41 Math.random(), 135, 313
szesnastkowe, 41 Math.round(), 136, 312
zmiennoprzecinkowe, 41 Math.sin(), 136, 312
Line, 200, 201 Math.sqrt(), 136, 313
linki, 24 Math.SQRT1_2, 312
listy, 245 Math.SQRT2, 135, 312
literały obiektowe, 105 Math.tan(), 312
literały tablicowe, 105 message, 315
LiveScript, 24 metody, 27, 105
load, 250 wywołanie, 107
localeCompare(), 306 metody uprzywilejowane, 271
location.hostname, 211 Microsoft, 24, 25
location.href, 211 miksiny, 193
modulo, 38
modyfikacja
Ł style, 231
węzły DOM, 230
łańcuch prototypów, 172, 197 modyfikatory dostępu, 270
tworzenie, 172 mousedown, 250
łańcuch zakresów, 91 mousemove, 250
łańcuchowanie, 273 mouseover, 250
łańcuchy znaków, 40, 45, 130, 304 mouseup, 250
konkatenacja, 46 MSXML2.XMLHTTP.3.0, 253
konwersja, 46 multiline, 314
znaki specjalne, 47 My Yahoo!, 23
łączenie JavaScriptu z kodem HTML, 207 myszka, 250

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

navigator.userAgent, 210 opakowujące, 117


nawiasy klamrowe, 61 pola, 27, 105
nazwy, 264 porównywanie, 114
funkcje, 74 przekazywanie do funkcji, 114
zmienne, 36 przestrzenie nazw, 264
Netscape, 24 RegExp, 313
new, 109, 150 reprezentacja tekstowa, 118
next(), 99 String, 130, 304
nextSibling, 228 this, 109
nieskończoność, 43 tworzenie, 109
Node, 221 użytkowe, 117
NodeList, 222 window, 209
nodeName, 226 wywołanie metod, 107
NOT, 49 obiekty ActiveX, 253
null, 40, 54 obiekty obserwowane, 282
Number, 117, 128, 302 obiekty obserwujące, 282
składowe, 303 object, 57
Number.MAX_VALUE, 129, 303 Object, 117, 295
Number.MIN_VALUE, 129, 303 object(), 189
Number.NaN, 129, 303 Object.prototype, 296
Number.NEGATIVE_INFINITY, 129, 303 obserwator, 282
Number.POSITIVE_INFINITY, 129, 303 obserwator zdarzenia, 242, 243
obsługa błędów, 146
O obsługa zdarzeń, 242
przeglądarki, 248
obiekt globalny, 110 odseparowanie warstwy zachowania
obiekt konfiguracyjny, 269 od treści strony, 263
obiekt XMLHttpRequest, 25 okienka pop-up, 215
obiektowość, 23 okno, 209
obiektowy model dokumentu, 208, 219 okno dialogowe, 81
obiektowy model przeglądarki, 208, 209 onclick, 242, 263
obiekty, 27, 103, 295 onmouseover, 263
Array, 118, 120, 298
open(), 251
błędy, 117
operacje, 37
Boolean, 127, 302
operatory, 37
console, 116
arytmetyczne, 38
Date, 136, 308
instanceof, 112
document, 223
leniwe wartościowanie, 52
dostęp do własności, 106
logiczne, 49
dziedziczenie, 186
Error, 146, 315 porównywanie, 53
Firebug, 115 priorytety, 51
Function, 122, 301 proste przypisanie, 39
konstruktory, 109 trójkowy, 63
Math, 135, 311 typeof, 41
metody, 27, 105 złożone, 39
modyfikacja składowych, 108 OR, 49, 50
modyfikatory dostępu, 270 osadzanie kodu JavaScript na stronie HTML, 207
Number, 128, 302 ostatnie dziecko, 228
Object, 117, 295 otwieranie okna przeglądarki, 215

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

resize, 250 równość, 53


responseText, 252, 254 zawartość obiektu, 210
responseXML, 254, 256 Square, 204
return, 74 standardy sieciowe, 25
reverse(), 299 starsze sposoby dostępu do dokumentu, 239
rich media, 26 status, 252, 254
rozgałęzianie kodu w czasie inicjalizacji, 267 statyczne dokumenty HTML, 24
rozszerzanie obiektów wbudowanych, 165, 166 stopPropagation(), 246
rówieśnicy, 228 string, 40, 45
String, 117, 130, 304
charAt(), 132
S fromCharCode(), 305
screen.colorDepth, 214 indexOf(), 132
scroll, 250 join(), 134
search(), 134, 143, 307 lastIndexOf(), 133
select, 250 match(), 134
send(), 251 replace(), 134
set, 98 search(), 134
setCapture(), 246 składowe, 305
Set-Cookie, 240 slice(), 133
setDate(), 310 split(), 134
setFullYear(), 309 substring(), 133
setHours(), 310 toLowerCase(), 132
setMilliseconds(), 310 toUpperCase(), 132
setMinutes(), 310 struktury danych, 56
setMonth(), 310 style, 231, 263
setSeconds(), 310 submit, 250, 264
setTime(), 309 subscriber objects, 282
setUTCDate(), 310 substring(), 133, 307
setUTCFullYear(), 309 switch, 63, 64
setUTCHours(), 310 break, 64
setUTCMilliseconds(), 310 case, 64
setUTCMinutes(), 310 default, 64
setUTCMonth(), 310 SyntaxError, 315
setUTCSeconds(), 310
Shape, 200, 202 Ś
shift(), 299
singleton, 276 średniki, 60
singleton 2, 276 środowisko, 26
skrypty, 26 środowisko działania aplikacji, 23
slice(), 120, 133, 299, 307 środowisko przeglądarki, 207
słowa zarezerwowane, 287 środowisko rozwijania aplikacji, 31
sort(), 120, 300
source, 314
specyfikacja DOM, 221
T
splice(), 300 tablice, 56, 298
split(), 134, 143, 146, 307 aktualizacja elementów, 57
sprawdzanie dodawanie elementów, 57
istnienie zmiennej, 62 elementy, 57

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

węzły-dzieci, 224 obserwator, 282


while, 66 singleton, 276
widżety Yahoo!, 26 singleton 2, 276
wielkość liter, 36 wzorce strukturalne, 276, 280
wierzchołki, 220 wzorzec leniwego definiowania funkcji, 268
win.close(), 215
window, 110, 209
window.alert(), 216
X
window.close(), 215 XHTML 1.0, 208
window.confirm(), 216 XML, 251, 254
window.document, 219 XMLHttpRequest, 25, 250, 258
window.frames, 213 asynchroniczność, 254
window.history, 212 getElementsByTagName(), 256
window.location, 211, 242 IE, 253
window.moveBy(), 216 JSON, 254
window.moveTo(), 216 open(), 251
window.navigator, 210 przetwarzanie odpowiedzi, 251, 252
window.open(), 215 readyState, 252, 254
window.prompt(), 216 readystatechange, 251, 252
window.resizeTo(), 216 responseText, 252, 254
window.screen, 209, 214 responseXML, 254, 256
window.setInterval(), 217 send(), 251
window.setTimeout(), 217 status, 252, 254
własności CSS, 231 tworzenie obiektów w IE
wprowadzanie tekstu, 217 w wersjach starszych niż 7, 253
wstawianie węzłów, 236 wysyłanie żądania, 251
wydzielanie warstwy zachowania, 263 XML, 254
wykładniki potęg, 42
wykrywanie
funkcjonalność przeglądarki, 268 Y
typ przeglądarki, 210 Yahoo!, 34
wyliczalne pola, 160 Yahoo! Mail, 23
wypożyczanie konstruktora, 194, 199 Yahoo! Maps, 23
wyrażenia regularne, 140, 313, 317 Yahoo! UI library, 263
klasy znaków, 317 Yahoo! User Interface, 166
modyfikatory, 141, 142 YouTube, 23
wzorzec, 141, 318 YUI, 166
wyskakujące okienka, 215 YUI Theater, 34
wysyłanie żądań HTTP, 250, 251
wywołanie
funkcje, 74 Z
metody, 107
zachowania domyślne, 248
wywołanie zwrotne, 84, 85
zachowanie, 263
wzorce czynnościowe, 276, 282
zamykanie okna, 215
wzorce kodowania, 261, 262
zapis literałowy funkcji, 83
wzorce konstrukcyjne, 276
zasada czarnej skrzynki, 76
wzorce projektowe, 261, 275
zasięg leksykalny, 91
dekorator, 280
zasięg zmiennych, 81
fabryka, 278
zatrzymanie propagacji zdarzeń, 246

333
Skorowidz

zdarzenia, 242, 257 zmiana wielkości liter, 132


addEventListener(), 243 zmienne, 35
anulowanie zachowania domyślnego, 248 deklaracja, 36
attachEvent(), 249 globalne, 81, 82, 277
bąbelkowanie, 244 inicjalizacja, 36
cancelBubble, 249 lokalne, 81
delegowanie, 246 nazwy, 36
detachEvent(), 249 sprawdzanie istnienia, 62
faza, 245 zasięg, 81
formularze, 250 znacznik czasu, 139
IE, 249 znaczniki prezentacyjne, 263
klawiatura, 250 znaki specjalne, 47
kliknięcie, 242
ładowanie, 250
myszka, 250 Ż
obserwator, 242, 243
obsługa, 242 żądania HTTP, 25, 250, 251
obsługa w różnych przeglądarkach, 248
okna, 250
preventDefault(), 248, 249
propagacja, 245
przechwytywanie, 244
removeEventListener(), 247
uchwyt, 242
zatrzymanie propagacji, 246

334
Notatki

You might also like