JavaScript. Zasady programowania obiektowego 978-83-246-9595-9 [PDF]

Programiści pracujący na co dzień z użyciem języków takich, jak Java, C# czy C++, z pewnym pobłażaniem patrzą na JavaScr

144 21 2MB

Polish Pages 129 Year 2014

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
Spis treści......Page 3
O redaktorze merytorycznym......Page 7
Podziękowania......Page 8
Wstęp......Page 9
Wprowadzenie......Page 11
Dla kogo jest ta książka?......Page 13
Pomoc i wsparcie......Page 14
1. Typy proste i referencje......Page 15
Czym są typy?......Page 16
Typy proste......Page 17
Identyfikowanie typów prostych......Page 19
Metody typów prostych......Page 20
Tworzenie obiektów......Page 21
Dereferencja obiektów......Page 22
Dodawanie i usuwanie właściwości......Page 23
Literały......Page 24
Literały obiektów i tablic......Page 25
Literały wyrażeń regularnych......Page 26
Dostęp do właściwości......Page 27
Identyfikowanie typów referencyjnych......Page 28
Typy opakowujące......Page 30
Podsumowanie......Page 33
2. Funkcje......Page 35
Deklaracje kontra wyrażenia......Page 36
Funkcje jako wartości......Page 37
Parametry......Page 39
Przeciążanie......Page 41
Metody obiektów......Page 43
Obiekt this......Page 44
Metoda call()......Page 45
Metoda apply()......Page 46
Metoda bind()......Page 47
Podsumowanie......Page 48
Definiowanie właściwości......Page 51
Wykrywanie właściwości......Page 53
Usuwanie właściwości......Page 55
Wyliczenia......Page 56
Rodzaje właściwości......Page 58
Wspólne atrybuty......Page 60
Atrybuty właściwości danych......Page 62
Atrybuty właściwości funkcji dostępowych......Page 64
Definiowanie wielu właściwości......Page 66
Pobieranie atrybutów właściwości......Page 67
Zapobieganie rozszerzaniu......Page 68
Pieczętowanie obiektów......Page 69
Zamrażanie obiektów......Page 70
Podsumowanie......Page 71
Konstruktory......Page 73
Prototypy......Page 78
Właściwość [[Prototype]]......Page 79
Używanie prototypów z konstruktorami......Page 82
Modyfikowanie prototypów......Page 86
Prototypy wbudowanych obiektów......Page 88
Podsumowanie......Page 89
Łańcuchy prototypów i Object.prototype......Page 91
Metody dziedziczone po Object.prototype......Page 92
Metoda toString()......Page 93
Modyfikowanie prototypu Object.prototype......Page 94
Dziedziczenie obiektów......Page 96
Dziedziczenie konstruktorów......Page 99
Zawłaszczanie konstruktora......Page 103
Uzyskiwanie dostępu do metod supertypu......Page 104
Podsumowanie......Page 106
6. Wzorce tworzenia obiektów......Page 107
Wzorzec modułu......Page 108
Prywatne składniki w konstruktorach......Page 110
Domieszki......Page 113
Zabezpieczenie zasięgu w konstruktorach......Page 120
Podsumowanie......Page 122
Skorowidz......Page 123
Papiere empfehlen

JavaScript. Zasady programowania obiektowego
 978-83-246-9595-9 [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Tytuł oryginału: The Principles of Object-Oriented JavaScript Tłumaczenie: Aleksander Lamża ISBN: 978-83-246-9595-9 Original edition Copyright © 2014 by Nicholas C. Zakas. ISBN 978-1-59327-540-2, published by No Starch Press. All rights reserved. Published by arrangement with No Starch Press, Inc. Polish language edition copyright © 2014 by Helion S.A. All rights reserved. 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) Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/jascpo_ebook Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.

 Poleć książkę na Facebook.com

 Księgarnia internetowa

 Kup w wersji papierowej

 Lubię to! » Nasza społeczność

 Oceń książkę

Spis treści

O AUTORZE ..................................................................... 7 WSTĘP . ............................................................................. 9 WPROWADZENIE ........................................................... 11 1 TYPY PROSTE I REFERENCJE ......................................... 15 Czym są typy? .......................................................................................16 Typy proste ..........................................................................................17 Identyfikowanie typów prostych . .........................................................19 Metody typów prostych .......................................................................20 Typy referencyjne ................................................................................21 Tworzenie obiektów ............................................................................21 Dereferencja obiektów ........................................................................22 Dodawanie i usuwanie właściwości .....................................................23 Tworzenie instancji wbudowanych typów . ..........................................24 Literały .................................................................................................24 Literały obiektów i tablic ......................................................................25 Literały funkcji ......................................................................................26 Literały wyrażeń regularnych ...............................................................26 Dostęp do właściwości ........................................................................27 Identyfikowanie typów referencyjnych . ..................................................28

Identyfikowanie tablic . .........................................................................30 Typy opakowujące . ..............................................................................30 Podsumowanie . ...................................................................................33 2 FUNKCJE ........................................................................ 35 Deklaracje kontra wyrażenia . ..............................................................36 Funkcje jako wartości . .........................................................................37 Parametry . ...........................................................................................39 Przeciążanie . ........................................................................................41 Metody obiektów . ...............................................................................43 Obiekt this . ..........................................................................................44 Modyfikowanie this . .............................................................................45 Podsumowanie . ...................................................................................48 3 OBIEKTY ........................................................................ 51 Definiowanie właściwości . ...................................................................51 Wykrywanie właściwości . ....................................................................53 Usuwanie właściwości . ........................................................................55 Wyliczenia . ...........................................................................................56 Rodzaje właściwości . ...........................................................................58 Atrybuty właściwości . ..........................................................................60 Wspólne atrybuty . ...............................................................................60 Atrybuty właściwości danych . .............................................................62 Atrybuty właściwości funkcji dostępowych ..........................................64 Definiowanie wielu właściwości ...........................................................66 Pobieranie atrybutów właściwości .......................................................67 Zapobieganie modyfikowaniu obiektu ....................................................68 Zapobieganie rozszerzaniu . .................................................................68 Pieczętowanie obiektów ......................................................................69 Zamrażanie obiektów . .........................................................................70 Podsumowanie . ...................................................................................71 4 KONSTRUKTORY I PROTOTYPY ................................... 73 Konstruktory . ......................................................................................73 Prototypy . ............................................................................................78 Właściwość [[Prototype]] . ...............................................................79 Używanie prototypów z konstruktorami .........................................82 4

Spis treści

Modyfikowanie prototypów .................................................................86 Prototypy wbudowanych obiektów . ....................................................88 Podsumowanie .....................................................................................89 5 DZIEDZICZENIE ............................................................. 91 Łańcuchy prototypów i Object.prototype . ..........................................91 Metody dziedziczone po Object.prototype . ........................................92 Modyfikowanie prototypu Object.prototype . ......................................94 Dziedziczenie obiektów .......................................................................96 Dziedziczenie konstruktorów . .............................................................99 Zawłaszczanie konstruktora . .............................................................103 Uzyskiwanie dostępu do metod supertypu . .......................................104 Podsumowanie ...................................................................................106 6 WZORCE TWORZENIA OBIEKTÓW ............................... 107 Prywatne i uprzywilejowane składniki obiektów . ..............................108 Wzorzec modułu ................................................................................108 Prywatne składniki w konstruktorach . ...............................................110 Domieszki ..........................................................................................113 Zabezpieczenie zasięgu w konstruktorach ........................................120 Podsumowanie ...................................................................................122 SKOROWIDZ ................................................................ 123

Spis treści

5

6

Spis treści

O autorze Nicholas C. Zakas jest inżynierem oprogramowania w serwisie Box.com. Jest znany z licznych publikacji i wystąpień dotyczących najnowszych i najlepszych praktyk programowania w JavaScripcie. Przez pięć lat współpracy z Yahoo! na stanowisku głównego dewelopera frontendu zdobywał doświadczenie i szlifował swoje umiejętności. Jest autorem kilku książek, m.in. Maintainable JavaScript (O’Reilly Media, 2012) i Professional JavaScript for Web Developers (Wrox, 2012).

O redaktorze merytorycznym Angus Croll, z urodzenia Anglik, należy do zespołu pracującego w Twitterze w San Francisco — jest współautorem oraz głównym programistą frameworku Flight. Ma dwie obsesje — język JavaScript i literaturę. Jest gorącym zwolennikiem zaangażowania artystów i ludzi o twórczym podejściu w proces tworzenia oprogramowania. Angus często występuje na konferencjach organizowanych na całym świecie, a obecnie pracuje nad dwiema książkami dla wydawnictwa No Starch Press. Można go złapać na Twitterze (@angustweets).

Podziękowania Chciałbym podziękować Kate Matsudairze za przekonanie mnie, że wydanie e-booka jest najlepszym sposobem na podzielenie się swoją wiedzą. Gdyby nie ona, nadal zastanawiałbym się nad tym, co powinienem zrobić z tym wszystkim, co udało mi się zawrzeć w tej książce. Dziękuję też Robowi Frieselowi za żywy odzew już od początkowych etapów pracy nad książką, a także Cody’emu Lindleyowi za cenne sugestie. Podziękowania trafiają również do Angusa Crolla, redaktora merytorycznego ostatecznej wersji książki posiadającego niezwykłą zdolność szukania dziury w całym — bez niego ta książka wiele by straciła. Nie mógłbym też zapomnieć o Billu Pollocku, którego poznałem na jednej z konferencji. To on wprawił w ruch całą tę maszynerię, dzięki której powstała ta książka.

8

Podziękowania

Wstęp

NA

NICHOLASA ZAKASA OD RAZU MYŚLI SIĘ O PROJAVASCRIPCIE. CO PRAWDA PEANAMI NA JEGO CZEŚĆ MÓGŁBYM ZAPISAĆ WIELE STRON, ALE TEGO NIE ZROBIĘ. NICHOLASA NIE trzeba przedstawiać — jest powszechnie znany jako wybitny programista JavaScriptu oraz autor publikacji i książek. Chciałbym jednak, zanim przejdę do wychwalania książki, podzielić się kilkoma osobistymi przemyśleniami. Znam Nicholasa już od dawna z jego książek, wpisów na blogu, wystąpień na seminariach i tweetów na temat JavaScriptu. Pierwszy raz spotkaliśmy się kilka lat temu, kiedy zaprosiłem go na konferencję społeczności związanej z jQuery. Od tego czasu nie tylko wielokrotnie braliśmy wspólnie udział w publicznych dyskusjach, ale też kontaktowaliśmy się prywatnie przez internet. Od samego początku uznawałem Nicholasa za kogoś więcej niż tylko programistę i lidera społeczności związanej z JavaScriptem. Jego słowa są zawsze przemyślane i precyzyjne, a wszystkich rozmówców i słuchaczy traktuje z należnym szacunkiem. Jako programista, mówca i autor dąży przede wszystkim do tego, aby pomagać, edukować i ulepszać. Kiedy mówi, trzeba go słuchać, ale nie tylko dlatego, że jest ekspertem w sprawach JavaScriptu. Również dlatego, że jego osobowość wznosi się ponad status profesjonalisty. DŹWIĘK NAZWISKA

GRAMOWANIU W

Już sam tytuł książki i wprowadzenie zdradzają intencje Nicholasa — napisał ją po to, by pomóc programistom pracującym w językach bazujących na klasach (np. C++ czy Java) przejść na język pozbawiony klas. Wyjaśnia, jak w JavaScripcie wygląda kapsułkowanie, agregacja, dziedziczenie i polimorfizm. Jest to idealna pozycja dla programistów chcących w pełni i świadomie wykorzystać obiektowość JavaScriptu. Jeżeli jesteś jedną z takich osób, z pewnością docenisz zwięzłość i fachowość objaśnień Nicholasa. Na tym nie kończy się jednak grono odbiorców tej książki. Jest to lektura obowiązkowa dla wszystkich programistów JavaScriptu, którzy znają ten język z czasów specyfikacji ECMAScript 3 (ES3). W nowej specyfikacji — ECMAScript 5 (ES5) wiele się zmieniło, zwłaszcza w sprawie obiektowości. Książka doskonale wypełnia tę lukę w wiedzy na temat obiektów w ES3 i ES5. Możesz pomyśleć: „Co w tym wyjątkowego? W wielu książkach mogę znaleźć informacje o nowościach wprowadzonych w ECMAScript 5”. Oczywiście masz rację. Uwierz jednak, że jest to jedyna z do tej pory napisanych książek, w której obiekty grają pierwszoplanową rolę. Znajdziesz w niej nie tylko spójne wprowadzenie w świat obiektów ES5, ale poznasz też te mechanizmy ES3, które musisz zgłębić, by w pełni zrozumieć nowe podejście zaproponowane w ES5. Mam szczerą nadzieję, że po ukazaniu się specyfikacji ES6 Nicholas opracuje kolejne wydanie i uwzględni w nim wszystkie nowo wprowadzone zmiany, tak jak to zrobił w tej książce z obiektami i mechanizmami języka w wersji ES5. Cody Lindley (www.codylindley.com) Autor książek JavaScript Enlightenment, DOM Enlightenment i jQuery Enlightenment Boise, Idaho 16 grudnia 2013 r.

10

Wstęp

Wprowadzenie

WIĘKSZOŚĆ

PROGRAMISTÓW KOJARZY PROGRAMOWANIE

OBIEKTOWE Z JĘZYKAMI, KTÓRYCH UCZY SIĘ PRZEWAŻNIE W SZKOLE, TAKIMI JAK C++ CZY JAVA, w których obiektowość jest realizowana w oparciu o klasy. Pierwsze, co trzeba zrobić, aby stworzyć jakiś program, to zdefiniować klasę bądź klasy, nawet jeśli jest to najprostsza aplikacja konsolowa. Typowe wzorce projektowe stosowane w branży programistycznej są również ściśle powiązane z klasami. JavaScript jest innym językiem — nie posiada klas — i jest to jeden z największych problemów, z jakim stykają się programiści chcący poznać ten język, a którzy mają już pewne doświadczenie w klasycznych językach obiektowych.

Charakterystycznymi cechami języków obiektowych są: 

Kapsułkowanie1 (ang. encapsulation) — dane mogą zostać zgrupowane z operującymi na nich funkcjami. W gruncie rzeczy to właśnie jest definicją obiektu.



Agregacja (ang. aggregation) — obiekt może zawierać odwołanie (referencję) do innego obiektu.



Dziedziczenie (ang. inheritance) — nowo tworzony obiekt może mieć te same właściwości co inny, bez konieczności ich duplikowania.



Polimorfizm (ang. polymorphism) — jeden interfejs może być implementowany przez wiele obiektów.

JavaScript posiada wszystkie te cechy, ale ponieważ nie istnieje w nim pojęcie klasy, niektóre z nich są realizowane inaczej, niż można by przypuszczać. Na pierwszy rzut oka kod napisany w JavaScripcie może wyglądać na proceduralny, a nie obiektowy. Wystarczy napisać funkcję, przekazać jej jakieś argumenty i już mamy działający program, ale obiektów nie dostrzeżemy. Jeżeli jednak przyjrzymy się bliżej językowi, odkryjemy istnienie obiektów, które objawia się choćby notacją z kropką. W wielu językach obiektowych dostęp do pól danych i metod obiektów jest realizowany za pomocą notacji z kropką. W JavaScripcie stosuje się tę samą składnię. Nie pisze się jednak definicji klas, nie importuje pakietów i nie dołącza plików nagłówkowych. Po prostu rozpoczyna się kodowanie, tworzy własne typy danych i grupuje je na wiele różnych sposobów. W JavaScripcie można pisać w stylu proceduralnym (podobnie jak w wielu klasycznych językach obiektowych), ale prawdziwa potęga języka ujawnia się wtedy, gdy zaczniemy korzystać z jego obiektowej natury. O tym właśnie jest ta książka. Żeby wszystko było jasne — wiele rozwiązań, które stosuje się w klasycznych językach obiektowych, nie ma zastosowania w JavaScripcie. Często jest to przyczyną zdezorientowania wśród początkujących programistów, ale bardzo szybko można docenić naturę JavaScriptu ze słabym 1

Angielski termin encapsulation ma w kontekście programowania dwa znaczenia. Pierwsze, częściej spotykane, wiąże się z ukrywaniem danych. Wtedy słowo „hermetyzacja” wydaje się najtrafniejszym polskim odpowiednikiem (choć często jest używany termin „enkapsulacja”, będący kalką językową). W drugim znaczeniu — zastosowanym w tym miejscu — chodzi o konstrukcję pozwalającą połączyć dane z operującymi na nich metodami (funkcjami). W tym przypadku właściwe wydaje się tłumaczenie terminu jako „kapsułkowanie” — przyp. tłum.

12

Wprowadzenie

typowaniem (ang. weak typing) na czele. Okaże się wtedy, że realizacja niektórych zadań wymaga mniej kodu niż w tradycyjnych językach. Najistotniejszą różnicą jest brak klas. Potrzebujesz obiektu z jakimiś polami? Nie ma sprawy — możesz go utworzyć w dowolnym momencie. Obiekt nie ma metody, która w danej chwili jest potrzebna? Też żaden problem — można ją w każdej chwili dodać. Dzięki tej książce dowiesz się, jak wygląda i jak jest realizowana obiektowość w JavaScripcie. Zapomnij o klasach i dziedziczeniu opartym na klasach. Przestaw się na dziedziczenie, które bazuje na prototypach, oraz funkcje konstruktorów. Dowiesz się, jak tworzyć obiekty, definiować własne typy, stosować dziedziczenie oraz poznasz techniki, dzięki którym wyciśniesz z JavaScriptu to, co najlepsze. Jednym słowem, nauczysz się wszystkiego, co jest niezbędne, by pisać w JavaScripcie jak profesjonalista. Miłej zabawy!

Dla kogo jest ta książka? Książka jest z założenia przewodnikiem dla tych wszystkich, którzy rozumieją programowanie obiektowe, ale chcą szczegółowo poznać mechanizmy obiektowe zastosowane w JavaScripcie. W związku z tym niezbędna jest znajomość dowolnego języka programowania obiektowego, jak choćby Java czy C#. Książka jest kierowana do trzech grup czytelników:  programistów zaznajomionych z zasadami programowania obiektowego, którzy chcą je stosować w JavaScripcie,  programistów aplikacji webowych i aplikacji serwerowych działających w środowisku Node.js, którzy chcą tworzyć lepszy i wydajniejszy kod,  początkujących programistów JavaScriptu, którzy chcą lepiej poznać język. Książka nie jest przeznaczona dla początkujących programistów, którzy nie zetknęli się jeszcze z JavaScriptem. W takim przypadku trzeba najpierw poznać podstawy tworzenia i wykonywania kodu.

Wprowadzenie

13

Przegląd zagadnień Rozdział 1. „Typy proste i referencje” — zawiera omówienie dwóch różnych rodzajów wartości: typów prostych i referencji. Dowiesz się, co je od siebie odróżnia, i przekonasz się, że poznanie tego zagadnienia jest kluczowe dla zrozumienia języka JavaScript. Rozdział 2. „Funkcje” — wyjaśnia szczegóły dotyczące funkcji. Jedną z przyczyn, dla których JavaScript jest tak ciekawym językiem, jest to, że funkcje są w nim typem pierwszoklasowym. Rozdział 3. „Obiekty” — zawiera dogłębne omówienie obiektów. W JavaScripcie obiekty zachowują się inaczej niż ich odpowiedniki w innych językach, więc niezwykle ważne jest dokładne zrozumienie ich działania. Rozdział 4. „Konstruktory i prototypy” — rozszerza wiadomości zawarte w rozdziale dotyczącym funkcji o informacje na temat konstruktorów. Konstruktory są funkcjami, ale używa się ich trochę inaczej. W rozdziale znajdziesz wyjaśnienie tych różnic oraz dowiesz się więcej na temat tworzenia własnych typów. Rozdział 5. „Dziedziczenie” — wyjaśnia mechanizm dziedziczenia zastosowany w JavaScripcie. To, że nie mamy do dyspozycji klas, wcale nie oznacza, że nie może dojść do dziedziczenia. Poznasz mechanizm dziedziczenia prototypowego i dowiesz się, czym się różni od klasycznego, opierającego się na klasach. Rozdział 6. „Wzorce tworzenia obiektów” — to przegląd często stosowanych wzorców tworzenia obiektów. W JavaScripcie istnieje wiele różnych sposobów tworzenia i zestawiania obiektów. Warto poznać najlepsze z nich, by tworzyć elastyczny i wydajny kod.

Pomoc i wsparcie Jeśli masz pytania lub komentarze albo chcesz podyskutować o książce, zajrzyj na listę mailingową http://groups.google.com/group/zakasbooks.

14

Wprowadzenie

1 Typy proste i referencje

WIĘKSZOŚĆ

PROGRAMISTÓW UCZY SIĘ PROGRAMOWANIA

OBIEKTOWEGO NA PRZYKŁADZIE JĘZYKÓW BAZUJĄCYCH NA KLASACH, TAKICH JAK JAVA LUB C#. PRZY PIERWSZYM zetknięciu z JavaScriptem można się poczuć zdezorientowanym, ponieważ w tym języku pojęcie klas formalnie nie istnieje. Pracy nad kodem nie rozpoczyna się więc od ich zdefiniowania, tylko od razu tworzy się potrzebne struktury danych. Brak klas pociąga za sobą pewne konsekwencje — skoro nie ma klas, nie ma też pakietów. W językach takich jak Java nazwy pakietów i klas definiują zarówno typy używanych w kodzie obiektów, jak i rozmieszczenie plików oraz folderów w projekcie, natomiast w JavaScripcie nic nie jest narzucone, więc o strukturę plików musimy zadbać sami. Niektórzy programiści starają się stosować rozwiązania z innych języków, natomiast inni korzystają z elastyczności JavaScriptu i podchodzą do problemu w całkiem nowy sposób. Co prawda dla stawiających pierwsze kroki programistów ta swoboda może stać się przyczyną nieporozumień

i problemów, ale kiedy się przywyknie do specyfiki JavaScriptu, okaże się, że to niezwykle efektywny i wygodny język programowania. Przejście z klasycznych języków obiektowych na JavaScript może ułatwiać to, że on również bazuje na obiektach — przeważnie zapisuje się w nich dane i umieszcza funkcje. Tak naprawdę w JavaScripcie nawet funkcje są obiektami, co czyni je najistotniejszymi elementami języka. Zrozumienie obiektowości w JavaScripcie i nauczenie się korzystania z obiektów są kluczowe dla zrozumienia JavaScriptu jako języka. Obiekty można tworzyć w dowolnym momencie, niejako z marszu. Dotyczy to również właściwości obiektów — można je dodawać i usuwać, kiedy tylko okaże się to konieczne. Poza tym elastyczność obiektów w JavaScripcie pozwala na stosowanie ciekawych i przydatnych wzorców, których nie da się zrealizować w wielu innych językach. Ten rozdział jest poświęcony dwóm podstawowym typom danych w JavaScripcie: typom prostym oraz referencjom. Mimo że dostęp do zmiennych obu typów jest realizowany przez obiekty, zachowują się one inaczej, więc konieczne jest zrozumienie dzielących je różnic.

Czym są typy? Mimo że w JavaScripcie nie stosuje się pojęcia klas, funkcjonują dwa rodzaje typów danych: typy proste i referencje. Typy proste (ang. primitives) służą do zapisywania danych w prostej postaci, natomiast w przypadku typów referencyjnych (ang. references) dane są zapisywane w obiektach, do których (a właściwie ich lokalizacji w pamięci) prowadzi referencja. Co ciekawe, JavaScript pozwala traktować typy proste jak referencje, dzięki czemu z punktu widzenia programisty język jest bardziej spójny. W wielu innych językach istnieje ścisłe rozróżnienie między tymi dwoma typami danych. Zmienna typu prostego jest przechowywana na stosie, a referencja na stercie. W JavaScripcie jest to rozwiązane zupełnie inaczej — zmienna jest zapisywana w obiekcie zmiennych. Proste wartości są przechowywane bezpośrednio w tym obiekcie, a w przypadku referencji w obiekcie zmiennej jest zapisany wskaźnik do lokalizacji obiektu w pamięci. Z dalszej lektury tego rozdziału dowiesz się, że mimo że pozornie typy te są do siebie podobne, w wielu sytuacjach zachowują się zupełnie inaczej.

16

Rozdział 1

Typy proste Typy proste reprezentują dane zapisane bezpośrednio w takiej formie, w jakiej występują, jak choćby wartości true czy 25. W JavaScripcie istnieje pięć typów prostych: boolean

wartość logiczna true albo false

number

liczbowa wartość całkowita lub zmiennoprzecinkowa

string

znak lub łańcuch znaków ujęty w cudzysłowy lub apostrofy (w JavaScripcie nie ma odrębnego typu dla pojedynczego znaku)

null

typ prosty posiadający tylko jedną wartość: null

undefined typ prosty posiadający tylko jedną wartość: undefined

(jest to wartość przypisywana do zmiennej, która nie została jeszcze zainicjalizowana) Pierwsze trzy typy (boolean, number i string) zachowują się podobnie, natomiast dwa ostatnie (null i undefined) są traktowane inaczej, co zostanie szczegółowo opisane w dalszej części rozdziału. Wartości wszystkich typów prostych mają reprezentację w postaci literałów. Literały to te wartości, które nie znajdują się w zmiennych, czyli na przykład zapisane w kodzie imię lub cena. Poniżej kilka przykładów użycia literałów omawianych typów prostych: // string var name = "Nicholas"; var selection = "a"; // number var count = 25; var cost = 1.51; // boolean var found = true; // null var object = null; // undefined var flag = undefined; var ref; // wartość undefined zostanie przypisana automatycznie

W JavaScripcie, podobnie jak w wielu innych językach, zmienna typu prostego przechowuje wartość (a nie wskaźnik do obiektu). Przypisanie

Typy proste i referencje

17

wartości do takiej zmiennej polega więc na utworzeniu kopii wartości i zapisaniu jej w zmiennej. Stąd wniosek, że jeśli do zmiennej przypisze się inną zmienną, każda z nich będzie przechowywała własną kopię tej samej wartości. Ilustruje to poniższy przykład: var color1 = "czerwony"; var color2 = color1;

Najpierw zmiennej color1 jest przypisywana wartość "czerwony". Następnie zmiennej color2 jest przypisywana wartość zmiennej color1, czyli "czerwony". Mimo że obie zmienne mają tę samą wartość, nie są w żaden sposób ze sobą powiązane, więc jeśli zmieni się wartość pierwszej zmiennej, nie wpłynie to na drugą (i odwrotnie). Wynika to z tego, że wartości tych zmiennych są przechowywane w innych miejscach, co ilustruje rysunek 1.1.

Rysunek 1.1. Obiekt zmiennej Podsumowując, zmienne typów prostych zajmują odrębne miejsca w pamięci, więc zmiany wartości jednej zmiennej nie wpływają na inne zmienne. Oto jeszcze jeden przykład: var color1 = "czerwony"; var color2 = color1; console.log(color1); // "czerwony" console.log(color2); // "czerwony" color1 = "niebieski"; console.log(color1); // "niebieski" console.log(color2); // "czerwony"

18

Rozdział 1

W powyższym kodzie zmieniamy wartość zmiennej color1 na "niebieski", co nie wpływa na oryginalną wartość ("czerwony") zmiennej color2.

Identyfikowanie typów prostych Najlepszym sposobem określenia typu jest użycie operatora typeof. Działa on ze wszystkimi zmiennymi i zwraca tekstową reprezentację typu danych. W przypadku łańcuchów tekstowych, liczb, wartości logicznych i typu undefined operator działa w przewidywalny sposób, co widać w poniższym przykładzie: console.log(typeof console.log(typeof console.log(typeof console.log(typeof console.log(typeof

"Nicholas"); 10); 5.1); true); undefined);

// "string" // "number" // "number" // "boolean" // "undefined"

Jak można się spodziewać, operator typeof zwraca "string", jeśli wartość jest łańcuchem tekstowym, "number" w przypadku liczb (bez względu na to, czy to liczba całkowita, czy zmiennoprzecinkowa), "boolean" dla wartości logicznych oraz "undefined" — w sytuacji gdy wartość nie jest zdefiniowana. Sprawy się komplikują w przypadku typu null. Niejednego programistę zdziwi wynik wykonania poniższego kodu: console.log(typeof null);

// "object"

Operator typeof dla wartości null zwraca "object". Dlaczego obiekt, skoro to typ null? Sprawa jest dyskusyjna (TC39, czyli komitet odpowiedzialny za zaprojektowanie i utrzymywanie JavaScriptu, przyznał, że to błąd), ale jeśli przyjmiemy, że null jest pustym wskaźnikiem do obiektu, rezultat "object" można uznać za uzasadniony. Najlepszym sposobem na sprawdzenie, czy wartość to null, jest bezpośrednie porównanie: console.log(value === null);

// true albo false

Typy proste i referencje

19

PORÓWNYWANIE BEZ ZMIANY TYPU Zwróć uwagę, że w powyższym przykładzie został zastosowany operator potrójnego znaku równości (===), a nie standardowy (==). Wynika to z tego, że potrójny operator nie wymusza zmiany typu porównywanych zmiennych. Poniższy przykład pomoże zrozumieć, dlaczego to takie ważne: console.log("5" == 5); console.log("5" === 5); console.log(undefined == null); console.log(undefined === null);

// true // false // true // false

W przypadku zastosowania podwójnego znaku równości łańcuch "5" i liczba 5 są równe, ponieważ przed wykonaniem porównania łańcuch zostaje przekonwertowany na liczbę. Operator potrójnego znaku równości nie uzna tych dwóch wartości za równe, ponieważ są innego typu. Podobnie sprawa wygląda w przypadku porównywania undefined i null. Wniosek stąd prosty — jeśli chcemy sprawdzić, czy jakaś wartość jest null, musimy zastosować potrójny znak równości, tak by był brany pod uwagę również typ.

Metody typów prostych Mimo że łańcuchy, liczby i wartości logiczne są typami prostymi, mają metody (wyjątkiem są jednak typy null i undefined). Pod względem liczby przydatnych metod wyróżnia się typ string. Kilka z nich zostało przedstawionych na poniższym listingu: var var var var

name = "Nicholas"; lowercaseName = name.toLowerCase(); // zamienia na małe znaki firstLetter = name.charAt(0); // zwraca pierwszy znak middleOfName = name.substring(2, 5); // zwraca sekwencję znaków 2 - 4

var count = 10; var fixedCount = count.toFixed(2); var hexCount = count.toString(16);

// konwertuje na łańcuch "10.00" // konwertuje na łańcuch "a"

var flag = true; var stringFlag = flag.toString();

// konwertuje na łańcuch "true"

20

Rozdział 1

UWAGA Na wartościach typów prostych — mimo że nie są obiektami —

można wywoływać metody. Odpowiadają za to mechanizmy JavaScriptu, dzięki którym wartości te wydają się obiektami, a to podnosi spójność języka.

Typy referencyjne Typy referencyjne reprezentują w JavaScripcie obiekty. Referencje to instancje typów referencyjnych, więc można je uznać za synonim obiektów (w dalszej części rozdziału terminy te będą stosowane zamiennie). Obiekt jest nieuporządkowaną listą właściwości złożonych z nazwy (będącej zawsze łańcuchami) i wartości. W przypadku gdy wartością właściwości jest funkcja, nazywa się ją metodą. Funkcje są tak naprawdę referencjami, więc istnieje drobna różnica między właściwością przechowującą — powiedzmy — tablicę a taką, która zawiera funkcję (oczywiście poza tym, że funkcję można wywołać, a tablicę nie). Zanim zacznie się korzystać z obiektu, trzeba go utworzyć.

Tworzenie obiektów Obiekty w JavaScripcie dobrze jest sobie wyobrazić jako tablice asocjacyjne (patrz rysunek 1.2).

Rysunek 1.2. Struktura obiektu Istnieje kilka sposobów tworzenia obiektów (instancji). Pierwszym z nich jest użycie operatora new w połączeniu z konstruktorem obiektu. Konstruktor to funkcja, która jest używana przez operator new (może to być dowolna funkcja). W JavaScripcie przyjęto konwencję rozpoczynania nazw konstruktorów wielką literą, by odróżnić je od zwykłych funkcji. Poniższy kod tworzy instancję ogólnego obiektu i zapisuje referencję do niego w zmiennej object:

Typy proste i referencje

21

var object = new Object();

W przypadku typów referencyjnych obiekty nie są zapisywane bezpośrednio w zmiennej, do której są przypisywane, więc zmienna object z powyższego przykładu nie zawiera utworzonego obiektu. Przechowywany jest za to wskaźnik (czyli referencja) do lokalizacji w pamięci, gdzie znajduje się obiekt. Jest to podstawowa różnica między obiektami i wartościami typów prostych, które są przechowywane bezpośrednio w zmiennej. Po przypisaniu obiektu do zmiennej zostaje w niej tak naprawdę zapisany wskaźnik. Gdybyśmy tę zmienną przypisali innej zmiennej, obie przechowywałyby kopię wskaźnika, który prowadzi do tego samego obiektu: var object1 = new Object(); var object2 = object1;

W tym kodzie najpierw jest tworzony obiekt (za pomocą operatora new), a referencja do niego jest zapisywana w zmiennej object1. Następnie zmienna ta jest przypisywana zmiennej object2. W tej chwili istnieje

nadal tylko jedna instancja (utworzona w pierwszym wierszu kodu), ale referencja do niej jest przechowywana w dwóch zmiennych, co widać na rysunku 1.3.

Rysunek 1.3. Dwie zmienne wskazujące ten sam obiekt

Dereferencja obiektów W języku JavaScript jest stosowany mechanizm odśmiecania pamięci (ang. garbage collection), więc podczas stosowania referencji nie ma potrzeby zbytniego przejmowania się zwalnianiem alokowanej pamięci.

22

Rozdział 1

Dobrym zwyczajem jest jednak dereferencja obiektów, które nie będą już używane, tak aby odśmiecacz mógł zwolnić zajmowaną przez nie pamięć. Aby to zrobić, wystarczy zmiennej referencyjnej przypisać null: var object1 = new Object(); // jakieś operacje na obiekcie object1 = null; // dereferencja

Obiekt object1 jest tworzony, następnie używany, a na końcu zmiennej jest przypisywany null. Jeśli nie istnieją żadne inne referencje do tego obiektu, odśmiecacz może zwolnić zajmowaną przez niego pamięć. Świadome wykorzystanie tego mechanizmu jest szczególnie istotne w dużych aplikacjach, w których jest tworzonych bardzo dużo obiektów.

Dodawanie i usuwanie właściwości Bardzo ciekawą cechą obiektów w JavaScripcie jest możliwość dodawania i usuwania właściwości w dowolnym momencie. Oto przykład: var object1 = new Object(); var object2 = object1; object1.myCustomProperty = "Super!"; console.log(object2.myCustomProperty); // "Super!"

Do obiektu object1 zostaje dodana właściwość myCustomProperty o wartości "Super!". Do właściwości tej można się odwołać również przez obiekt object2, ponieważ obie zmienne (object1 i object2) wskazują tę samą instancję. UWAGA Ten przykład ilustruje jedną z ciekawszych cech języka Java-

Script — możliwość modyfikowania obiektów w dowolnym momencie, nawet jeśli wcześniej nie zostały w ogóle zdefiniowane. W sytuacjach gdy taka możliwość jest niepożądana, można temu zapobiegać na kilka sposobów (poznasz je w dalszej części książki). Poza podstawowym, ogólnym typem referencyjnym JavaScript oferuje kilka innych, bardzo przydatnych, wbudowanych typów.

Typy proste i referencje

23

Tworzenie instancji wbudowanych typów Wcześniej omówiliśmy sposób tworzenia ogólnych obiektów za pomocą operatora new i konstruktora: new Object(). Typ Object jest tylko jednym z kilku przydatnych wbudowanych typów referencyjnych dostępnych w języku JavaScript. Pozostałe są bardziej wyspecjalizowane. Poniżej znajduje się krótki opis wbudowanych typów: Array

tablica indeksowana numerycznie

Date

data i czas

Error

błąd w czasie wykonania (istnieją również bardziej konkretne podtypy błędów)

Function funkcja Object

ogólny obiekt

RegExp

wyrażenie regularne

Instancje wbudowanych typów można tworzyć za pomocą operatora new, co widać w poniższym przykładzie: var var var var var var

items = new Array(); now = new Date(); error = new Error("Stało się coś złego."); func = new Function("console.log('Cześć');"); object = new Object(); re = new RegExp("\\d+");

Literały Niektóre wbudowane typy są dostępne w formie literałów. Literał to składnia umożliwiająca zdefiniowanie typu referencyjnego bez potrzeby jawnego tworzenia obiektu za pomocą operatora new i konstruktora. We wcześniejszych przykładach z tego rozdziału były stosowane literały wartości prostych, czyli literały łańcuchów, liczb, wartości logicznych, a także literały null i undefined.

24

Rozdział 1

Literały obiektów i tablic Aby utworzyć obiekt za pomocą literału obiektu, wystarczy zdefiniować właściwości wewnątrz nawiasów klamrowych. Każda właściwość jest zbudowana z nazwy (łańcucha), dwukropka i wartości. Poszczególne właściwości rozdziela się przecinkami: var book = { name: "JavaScript. Zasady programowania obiektowego", year: 2014 };

Nazwa właściwości może być również literałem łańcucha, co bywa przydatne w przypadku nazw zawierających spacje lub inne znaki specjalne: var book = { "name": "JavaScript. Zasady programowania obiektowego", "year": 2014 };

Pomijając różnicę w składni, ten przykład jest równoważny poprzedniemu. Ten sam efekt można uzyskać w jeszcze inny sposób: var book = new Object(); book.name = "JavaScript. Zasady programowania obiektowego"; book.year = 2014;

Wynik działania wszystkich trzech przedstawionych fragmentów kodu jest taki sam — powstaje obiekt z dwiema właściwościami. Można więc korzystać z dowolnego zapisu, ponieważ funkcjonalnie są one dokładnie takie same. UWAGA Użycie literału obiektu nie wiąże się tak naprawdę z wywołaniem konstruktora new Object(). Silnik JavaScriptu wykonuje jednak

te same operacje co w przypadku tego wywołania. Odnosi się to do wszystkich literałów typów referencyjnych. W podobny sposób stosuje się literał tablicy: dowolną liczbę wartości rozdzielonych przecinkami umieszcza się w nawiasach kwadratowych:

Typy proste i referencje

25

var colors = [ "czerwony", "niebieski", "zielony" ]; console.log(colors[0]); // "czerwony"

Taki zapis daje ten sam efekt co poniższy kod: var colors = new Array("czerwony", "niebieski", "zielony"); console.log(colors[0]); // "czerwony"

Literały funkcji W większości przypadków funkcje definiuje się za pomocą literałów. Tak naprawdę konstruktor Function jest stosowany w specyficznych sytuacjach — gdy ciało funkcji jest zapisane jako łańcuch. Taki kod jest trudny w utrzymaniu, analizowaniu i debugowaniu, więc raczej nie spotkasz się z nim w praktyce. Tworzenie funkcji za pomocą literału jest o wiele prostsze i niesie za sobą mniejsze ryzyko popełnienia błędu. Oto przykład: function reflect(value) { return value; } // jest równoważne temu: var reflect = new Function("value", "return value;");

W powyższym kodzie jest definiowana funkcja reflect(), która zwraca przekazaną do niej wartość. Nawet w przypadku tak prostej funkcji można zauważyć, że postać literału jest o wiele prostsza w czytaniu i zrozumieniu niż wersja z konstruktorem. Trzeba też pamiętać, że nie ma dobrego sposobu na debugowanie funkcji utworzonych za pomocą konstruktora, ponieważ takie funkcje nie są rozpoznawane przez debugery JavaScriptu. W aplikacji są więc czarnymi skrzynkami — wiemy tylko, co do nich jest przekazywane i co zwracają.

Literały wyrażeń regularnych JavaScript oferuje również literały wyrażeń regularnych, dzięki którym można definiować wyrażenia regularne bez konieczności użycia konstruktora RegExp. Są one zapisywane podobnie jak w języku Perl: wzorzec 26

Rozdział 1

jest zapisywany między prawymi ukośnikami, a ewentualne opcje umieszcza się za ukośnikiem zamykającym: var numbers = /\d+/g; // jest równoważne temu: var numbers = new RegExp("\\d+", "g");

Z literałów wyrażeń regularnych korzysta się trochę prościej niż z konstruktora, ponieważ nie trzeba stosować znaków ucieczki (tzw. escaping). Kiedy używa się konstruktora, wyrażenie regularne przekazuje się jako łańcuch, więc wszystkie znaki lewych ukośników trzeba poprzedzić dodatkowym lewym ukośnikiem (właśnie dlatego w powyższym przykładzie w wersji z literałem jest \d, a w wersji z konstruktorem — \\d). Z tego względu lepiej stosować literały, z wyjątkiem sytuacji gdy wyrażenie regularne jest konstruowane dynamicznie. Podsumowując, poza przypadkiem konstruktora Function tak naprawdę nie ma większego znaczenia sposób tworzenia instancji wbudowanych typów. Niektórzy programiści preferują literały, a inni konstruktory. Stosuj więc takie rozwiązanie, które w danej sytuacji wydaje się wygodniejsze i lepsze.

Dostęp do właściwości Jak już zostało wspomniane, właściwości to pary nazwa-wartość zapisane w obiekcie. W JavaScripcie, podobnie jak w wielu innych językach, w celu uzyskania dostępu do właściwości najczęściej stosuje się notację z kropką. Można jednak zastosować zapis z nawiasami klamrowymi, między którymi umieszcza się nazwę właściwości. Jako przykład posłuży prosty kod, w którym został zastosowany zapis z kropką: var array = []; array.push(12345);

Można to zapisać inaczej, umieszczając nazwę metody (jako łańcuch) w nawiasach kwadratowych:

Typy proste i referencje

27

var array = []; array["push"](12345);

Taki zapis przydaje się w sytuacjach, gdy musimy dynamicznie decydować, do której właściwości chcemy uzyskać dostęp. Zamiast korzystać z literału łańcucha, nazwę pożądanej właściwości można zapisać w zmiennej i przekazać ją w nawiasach klamrowych: var array = []; var method = "push"; array[method](12345);

Zmienna method ma wartość "push", więc na tablicy (zmiennej array) jest wywoływana metoda push(). Ta przydatna możliwość zostanie wykorzystana jeszcze kilkukrotnie w dalszej części książki. Jeśli pominiemy składnię czy wydajność, zauważymy, że jedyna różnica między zapisem z kropką i nawiasami polega na tym, że drugi sposób daje możliwość zastosowania w nazwach właściwości znaków specjalnych, co czasem może się przydać. Programiści preferują zapis z kropką, ponieważ taki kod łatwiej się czyta i analizuje.

Identyfikowanie typów referencyjnych Najprostszym do zidentyfikowania typem referencyjnym jest funkcja, ponieważ operator typeof zwraca łańcuch "function": function reflect(value) { return value; } console.log(typeof reflect);

// "function"

Pozostałe typy referencyjne trudniej określić, ponieważ operator typeof zwraca dla nich wszystkich łańcuch "object". Może się to okazać problemem, jeśli korzysta się z wielu różnych typów. W takich sytuacjach z pomocą przychodzi inny operator języka JavaScript — instanceof.

28

Rozdział 1

Operator ten ma dwa parametry: obiekt i konstruktor. Jeśli obiekt jest instancją typu określonego przez konstruktor, operator zwraca true, a w przeciwnych przypadku — false. Oto przykład: var items = []; var object = {}; function reflect(value) { return value; } console.log(items instanceof Array); console.log(object instanceof Object); console.log(reflect instanceof Function);

// true // true // true

W powyższym kodzie za pomocą operatora instanceof jest testowanych kilka wartości. Wszystkie zostały prawidłowo zidentyfikowane jako instancje typów określonych przez podane konstruktory (mimo że obiekty były tworzone za pomocą literałów). Operator instanceof poprawnie identyfikuje również typy dziedziczone po innych typach. Ze względu na to, że wszystkie typy wbudowane dziedziczą po Object, każdy obiekt będzie rozpoznany jako instancja typu Object. Dobrze to ilustruje poniższy przykład, w którym pod tym kątem są sprawdzane wcześniej utworzone obiekty: var items = []; var object = {}; function reflect(value) { return value; } console.log(items instanceof Array); console.log(items instanceof Object); console.log(object instanceof Object); console.log(object instanceof Array); console.log(reflect instanceof Function); console.log(reflect instanceof Object);

// true // true // true // false // true // true

Typy proste i referencje

29

Wszystkie obiekty typów wbudowanych zostały rozpoznane jako instancje typu Object, ponieważ po nim dziedziczą.

Identyfikowanie tablic Mimo że operator instanceof prawidłowo rozpoznaje tablice, jest jeden wyjątek, gdy sobie nie radzi. W aplikacjach webowych może dojść do sytuacji, gdy wartości są wymieniane między ramkami w obrębie tej samej strony internetowej. Problem polega na tym, że każda ramka ma swój własny globalny kontekst, a więc własne wersje Object, Array i pozostałych typów wbudowanych. Jeśli przekaże się tablicę z jednej ramki do drugiej, operator instanceof nie zadziała prawidłowo, ponieważ ta tablica jest instancją typu Array z innej ramki. Problem ten rozwiązuje wprowadzona w ECMAScript 5 metoda Array.isArray(), która jednoznacznie określa, czy przekazana jej wartość jest tablicą, czy nie. Metoda ta zwraca true, jeśli przekaże się jej natywną tablicę, niezależnie od kontekstu, z którego pochodzi. Jest to najlepszy sposób na identyfikowanie tablic: var items = []; console.log(Array.isArray(items));

// true

Metodę Array.isArray() można stosować w środowiskach zgodnych z ECMAScript 5, a więc w zdecydowanej większości przeglądarek (Internet Explorer obsługuje ją od wersji 9.) i w środowisku Node.js.

Typy opakowujące W języku JavaScript spore zamieszanie może wprowadzać koncepcja typów opakowujących typy proste (ang. primitive wrapper types). Istnieją trzy takie typy: String, Number i Boolean. Są to typy referencyjne, dzięki którym wartościami typów prostych można się posługiwać jak obiektami, co okazuje się bardzo przydatne. Wystarczy sobie wyobrazić bałagan w kodzie spowodowany koniecznością zastosowania innej składni lub przejścia na styl programowania proceduralnego z powodu, na przykład, potrzeby wyznaczenia podłańcucha z jakiejś zmiennej tekstowej.

30

Rozdział 1

Typy opakowujące są automatycznie tworzone, w chwili gdy odczytywana jest wartość typu prostego string, number lub boolean. Przeanalizujmy prosty przykład. W pierwszym wierszu poniższego fragmentu kodu zmiennej name jest przypisywany łańcuch typu prostego (string). W drugim wierszu traktujemy tę zmienną jak obiekt — wywołujemy na niej metodę charAt(0) z zastosowaniem notacji z kropką. var name = "Nicholas"; var firstChar = name.charAt(0); console.log(firstChar);

// "N"

Aby zobrazować, co się dzieje w tym kodzie poza naszą wiedzą, trzeba go uzupełnić o kilka dodatkowych operacji: // silnik JavaScriptu wykonuje takie operacje var name = "Nicholas"; var temp = new String(name); var firstChar = temp.charAt(0); temp = null; console.log(firstChar); // "N"

Ponieważ potraktowaliśmy zmienną typu prostego jak obiekt, silnik JavaScriptu utworzył instancję typu String, tak by dało się wywołać metodę charAt(0). Obiekt typu String istnieje tylko na potrzeby tej jednej operacji (proces ten określa się terminem autoboxing). Łatwo to sprawdzić — wystarczy dodać do zmiennej name jakąś właściwość (traktując tę zmienną jak zwykły obiekt): var name = "Nicholas"; name.last = "Zakas"; console.log(name.last);

// undefined

W tym kodzie do łańcucha name staramy się dodać właściwość last. Kod wydaje się prawidłowy, ale wynik jego działania już nie. Co się dzieje? Gdy pracujemy z obiektami, możemy dodawać do nich nowe właściwości, kiedy tylko chcemy, a co ważne, pozostają one w obiekcie aż do chwili ich usunięcia. W przypadku typów opakowujących jest inaczej, ponieważ automatycznie tworzone obiekty są praktycznie od razu niszczone.

Typy proste i referencje

31

Powyższy fragment kodu zostanie uzupełniony przez silnik JavaScriptu w następujący sposób: // silnik JavaScriptu wykonuje takie operacje var name = "Nicholas"; var temp = new String(name); temp.last = "Zakas"; temp = null; // tymczasowy obiekt zostaje zniszczony var temp = new String(name); console.log(temp.last); // undefined temp = null;

Nowa właściwość nie jest przypisywana do łańcucha, ale do tymczasowego obiektu, który jest od razu niszczony. Kiedy w dalszej części kodu chcemy odczytać tę właściwość, znów jest tworzony tymczasowy obiekt, który oczywiście jej nie posiada. Jeżeli na zmiennej typu prostego zastosujemy operator instanceof, uzyskamy wynik false, mimo że — jak widzieliśmy — referencje są tworzone automatycznie: var name = "Nicholas"; var count = 10; var found = false; console.log(name instanceof String); console.log(count instanceof Number); console.log(found instanceof Boolean);

// false // false // false

Dzieje się tak dlatego, że tymczasowe obiekty są tworzone tylko w sytuacji, gdy odczytywana jest wartość zmiennej. Operator instanceof niczego nie odczytuje, więc nie jest tworzony tymczasowy obiekt, w związku z czym zmienna nie jest instancją typu opakowującego. Obiekt opakowujący można utworzyć jawnie, ale ma to pewne efekty uboczne: var name = new String("Nicholas"); var count = new Number(10); var found = new Boolean(false);

32

Rozdział 1

console.log(typeof name); console.log(typeof count); console.log(typeof found);

// "object" // "object" // "object"

Jak widać, tworzymy w ten sposób ogólne obiekty, więc operator typeof nie może zidentyfikować faktycznego typu przechowywanych w nich danych. Trzeba też wiedzieć, że w wielu sytuacjach instancje typów String, Number i Boolean zachowują się inaczej niż ich proste odpowiedniki. W poniższym fragmencie kodu korzystamy z obiektu Boolean o wartości false. Po uruchomieniu kodu w konsoli zostanie jednak wyświetlony komunikat "Znaleziony", ponieważ w instrukcji warunkowej obiekt jest zawsze traktowany jak wartość true. Nie jest więc istotne, że ten obiekt opakowuje wartość false — jest obiektem, więc w tej sytuacji jest traktowany jako true. var found = new Boolean(false); if (found) { console.log("Znaleziony"); }

// to zostanie wykonane

Ręczne tworzenie instancji typów opakowujących może wprowadzać zamieszanie, więc lepiej tego unikać, chyba że ma się wyraźny powód. Korzystanie z obiektów opakowujących zamiast prostych wartości prowadzi najczęściej do trudnych do wykrycia błędów.

Podsumowanie Mimo że w języku JavaScript nie ma klas, są typy. Wszystkie zmienne i fragmenty danych są powiązane z określonymi wartościami typu prostego lub referencjami. Pięć typów prostych (łańcuchy, liczby, wartości logiczne, null i undefined) reprezentuje wartości zapisywane bezpośrednio w obiekcie zmiennych dla danego kontekstu. Do identyfikowania tych typów można użyć operatora typeof. Wyjątkiem jest null, który można wykryć tylko przez bezpośrednie porównanie z wartością null. Typy referencyjne w JavaScripcie najbliżej odpowiadają klasom znanym z innych języków. Obiekty są instancjami klas referencyjnych. Nowe obiekty można tworzyć za pomocą operatora new lub literału referencji.

Typy proste i referencje

33

Dostęp do właściwości i metod jest realizowany przede wszystkim za pomocą notacji z kropką, ale można też korzystać z notacji z nawiasami kwadratowymi. Funkcje są w JavaScripcie obiektami i można je identyfikować za pomocą operatora typeof. Aby sprawdzić, czy obiekt jest instancją określonego typu referencyjnego, należy użyć operatora instanceof. Dzięki trzem typom opakowującym — String, Number i Boolean — zmienne typów prostych można traktować jak referencje. Silnik JavaScriptu automatycznie tworzy obiekty opakowujące, więc z poziomu kodu prostymi zmiennymi można się posługiwać jak obiektami, ale trzeba pamiętać, że obiekty te są tymczasowe, więc są niszczone zaraz po ich użyciu. Mimo że instancje typów opakowujących można tworzyć samodzielnie, nie jest to zalecane ze względu na wprowadzanie zamieszania i większe prawdopodobieństwo popełnienia pomyłki.

34

Rozdział 1

2 Funkcje

JAK

JUŻ WIESZ Z LEKTURY PIERWSZEGO ROZDZIAŁU,

W JAVASCRIPCIE FUNKCJE SĄ OBIEKTAMI.

CECHĄ

OD-

RÓŻNIAJĄCĄ JE OD INNYCH OBIEKTÓW JEST WEW-

nętrzna właściwość [[Call]]. Właściwości tego typu nie są dostępne z poziomu kodu, służą raczej do określenia zachowania obiektu podczas wykonywania kodu. W standardzie ECMAScript jest zdefiniowanych wiele wewnętrznych właściwości dla różnych obiektów, a wszystkie łączy jedno — nazwa umieszczona w podwójnych nawiasach kwadratowych. Właściwość [[Call]], która wskazuje, że obiekt może zostać wykonany, jest obecna tylko w funkcjach. Korzysta z tego operator typeof, który zwraca wynik "function", jeśli w obiekcie znajduje się właściwość [[Call]]. W przeszłości niektóre przeglądarki dołączały tę właściwość do wyrażeń regularnych, przez co były one nieprawidłowo rozpoznawane jako funkcje. Obecnie ten problem już nie występuje. W tym rozdziale znajdziesz omówienie sposobów definiowania i wywoływania funkcji w JavaScripcie. Ponieważ funkcje są obiektami, zachowują się trochę inaczej niż w innych językach. Dogłębne poznanie tych mechanizmów jest kluczowe dla zrozumienia JavaScriptu.

Deklaracje kontra wyrażenia Dla funkcji istnieją dwie postacie literałów. Pierwsza to deklaracja funkcji, która rozpoczyna się od słowa kluczowego function i następującej po nim nazwy funkcji. Zawartość funkcji jest umieszczana w nawiasach klamrowych, co widać w poniższym przykładzie: function add(num1, num2) { return num1 + num2; }

Drugi z literałów to wyrażenie funkcyjne, które nie wymaga podawania nazwy funkcji po słowie kluczowym function. Tego typu funkcje określa się jako anonimowe, ponieważ obiekt funkcji nie ma nazwy. Do wyrażeń funkcyjnych tworzy się zazwyczaj referencje i zapisuje je w zmiennych lub właściwościach, jak w poniższym przykładzie: var add = function(num1, num2) { return num1 + num2; };

W kodzie funkcja jest przypisywana do zmiennej add. Wyrażenie funkcyjne jest zbudowane prawie tak samo jak deklaracja funkcji. Jedyną różnicą jest brak nazwy oraz średnik na końcu. Obecność średnika wynika z tego, że jest to zwykła instrukcja przypisania, po której zwyczajowo umieszcza się średnik. Mimo że te dwie definicje wyglądają bardzo podobnie, różnią się w dość znaczący sposób. Przed uruchomieniem kodu deklaracja funkcji jest niejawnie przenoszona na początek kontekstu (czyli zawierającej ją funkcji lub zasięgu globalnego). Mechanizm ten jest określany terminem hoisting. Wynika stąd jeden istotny wniosek — funkcję można zdefiniować w dowolnym miejscu kodu (nawet po jej wywołaniu), a i tak będzie widoczna wszędzie. Oto przykład: var result = add(5, 5); function add(num1, num2) { return num1 + num2; }

36

Rozdział 2

Może się wydawać, że po uruchomieniu tego kodu zostanie zgłoszony błąd, ale tak nie jest, dzięki zastosowaniu mechanizmu przenoszenia deklaracji na początek. Silnik JavaScriptu wykonuje więc kod wyglądający mniej więcej tak: // silnik JavaScriptu interpretuje kod w taki sposób function add(num1, num2) { return num1 + num2; } var result = add(5, 5);

Mechanizm przenoszenia działa tylko w przypadku deklaracji funkcji, ponieważ ich nazwy są znane z wyprzedzeniem. Wyrażenia funkcyjne nie mogą zostać przeniesione, ponieważ referencje do nich są przechowywane w zmiennych. W związku z tym poniższy kod spowoduje zgłoszenie błędu: // błąd! var result = add(5, 5); var add = function(num1, num2) { return num1 + num2; };

Deklaracje funkcji i wyrażenia funkcyjne można stosować wymiennie, pod warunkiem że definicje znajdą się przed wywołaniem funkcji.

Funkcje jako wartości Ponieważ w JavaScripcie funkcje są obiektami, można je traktować jak każde inne obiekty. Można je przypisywać do zmiennych, umieszczać w obiektach, przekazywać do innych funkcji jako argumenty, a także zwracać z funkcji. Po prostu funkcje można zastosować wszędzie tam, gdzie można użyć referencji. Te cechy sprawiają, że funkcje mają w JavaScripcie ogromne możliwości. Przeanalizujmy poniższy przykład:

Funkcje

37

 function sayHi() { console.log("Cześć!"); } sayHi();

// wyświetla "Cześć!"

 var sayHi2 = sayHi; sayHi2();

// wyświetla "Cześć!"

W kodzie znajduje się deklaracja funkcji sayHi . Następnie jest tworzona zmienna sayHi2 i przypisywana jest do niej wartość sayHi . Obie zmienne wskazują tę samą funkcję, a to oznacza, że można ją wywołać za pośrednictwem dowolnej z nich, uzyskując ten sam efekt. W zrozumieniu tego zachowania powinien pomóc poniższy kod, odpowiadający poprzedniemu, ale w którym został zastosowany konstruktor Function: var sayHi = new Function("console.log(\"Cześć!\");"); sayHi();

// wyświetla "Cześć!"

var sayHi2 = sayHi; sayHi2();

// wyświetla "Cześć!"

Zastosowanie konstruktora pokazuje wyraźniej niż w poprzednim przykładzie, że sayHi może być traktowane jak każdy inny obiekt. Wystarczy zapamiętać, że funkcje są obiektami, by wiele konstrukcji i zachowań w języku JavaScript nabrało sensu. Jak wspomnieliśmy, funkcję można przekazać do innej funkcji jako argument. Na przykład metoda sort() typu tablicowego ma opcjonalny parametr, w którym można przekazać funkcję porównującą. Funkcja ta jest wywoływana za każdym razem, gdy muszą zostać porównane dwie wartości z tablicy. Jeśli pierwsza wartość jest mniejsza niż druga, funkcja porównująca musi zwrócić liczbę ujemną. Jeśli pierwsza jest większa, funkcja zwraca wartość dodatnią, a jeśli są sobie równe — wartość 0. Domyślnie metoda sort() konwertuje elementy tablicy na łańcuchy i wtedy dokonuje porównania. To oznacza, że gdybyśmy chcieli poprawnie posortować tablicę liczb, musielibyśmy napisać własną funkcję porównującą. Przykład takiej funkcji znajduje się w poniższym kodzie:

38

Rozdział 2

var numbers = [ 1, 5, 8, 4, 7, 10, 2, 6 ];  numbers.sort(function(first, second) { return first - second; }); console.log(numbers);

// "[1, 2, 4, 5, 6, 7, 8, 10]"

 numbers.sort(); console.log(numbers);

// "[1, 10, 2, 4, 5, 6, 7, 8]"

W tym przykładzie funkcja porównująca  przekazywana do metody sort() jest wyrażeniem funkcyjnym. Zwróć uwagę, że nie ma nazwy, a istnieje tylko referencja do niej, przekazywana do metody sort() (jest więc

funkcją anonimową). W ciele funkcji wystarczy zwrócić różnicę dwóch wartości. Porównaj otrzymany wynik z drugim wywołaniem funkcji sort() , w którym nie stosujemy funkcji porównującej. Kolejność elementów tablicy jest inna niż oczekiwana, ponieważ po 1 następuje 10. Wynika to z tego, że — jak wspomnieliśmy — domyślnie wszystkie wartości są zamieniane na łańcuchy i dopiero wtedy porównywane.

Parametry Kolejną ciekawą cechą języka JavaScript jest możliwość przekazania do każdej funkcji dowolnej liczby parametrów. Jest to możliwe dzięki temu, że parametry funkcji są przechowywane w strukturze arguments przypominającej tablicę. Tak jak w przypadku zwykłej tablicy, ta również dynamicznie się powiększa w miarę dodawania nowych elementów. Dostęp do wartości jest realizowany za pomocą numerycznego indeksu, a właściwość length zawiera liczbę elementów tablicy. Obiekt arguments jest dostępny w każdej funkcji. Z tego wniosek, że nazwane parametry istnieją tylko dla wygody programisty (i w celu zachowania zgodności z innymi językami), jednak zdefiniowanie w funkcji listy parametrów nie ogranicza faktycznej liczby wartości, które można przekazać. UWAGA Obiekt arguments nie jest instancją typu Array, więc nie posiada tych samych metod co tablice. Wywołanie Array.isArray(arguments) zwraca false.

Funkcje

39

Z drugiej strony silnik JavaScriptu nie ignoruje nazwanych parametrów. Liczba oczekiwanych argumentów jest zapisana we właściwości length funkcji (pamiętaj, że funkcje są obiektami, więc mają właściwości). Właściwość length określa tzw. arność czy inaczej argumentowość (ang. arity) funkcji, czyli liczbę jej parametrów. Wiedza o arności funkcji jest istotna, ponieważ w przypadku przekazania zbyt dużej lub zbyt małej liczby argumentów, nie zostanie zgłoszony żaden błąd. Poniżej znajduje się prosty przykład ilustrujący wykorzystanie obiektu arguments i arności funkcji. Co ważne, liczba argumentów przekazanych w wywołaniu funkcji nie zmienia jej arności. function reflect(value) { return value; } console.log(reflect("Cześć!")); // "Cześć!" console.log(reflect("Cześć!", 25)); // "Cześć!" console.log(reflect.length); // 1 reflect = function() { return arguments[0]; }; console.log(reflect("Cześć!")); // "Cześć!" console.log(reflect("Cześć!", 25)); // "Cześć!" console.log(reflect.length); // 0

Najpierw definiujemy funkcję reflect() przyjmującą jeden nazwany parametr. W przypadku gdy wywołujemy ją z dwoma parametrami, nie jest zgłaszany żaden błąd. Właściwość length ma wartość 1, ponieważ w definicji funkcji znajduje się jeden nazwany parametr. Następnie przedefiniowaliśmy funkcję reflect(), tak by nie posiadała żadnego nazwanego parametru. Zwracamy z niej wartość arguments[0], czyli pierwszy przekazany argument. Nowa wersja funkcji działa dokładnie tak samo jak poprzednia, ale właściwość length jest równa 0. Pierwszą implementację funkcji znacznie łatwiej zrozumieć, ponieważ używamy w niej nazwanego parametru (podobnie jak się to robi w innych językach). Wersja wykorzystująca obiekt arguments może być myląca — do pełnego zrozumienia wymagane jest przeanalizowanie całego kodu funkcji. Dlatego właśnie większość programistów stara się unikać korzystania z obiektu arguments. 40

Rozdział 2

Czasami jednak użycie tego obiektu zamiast nazwanych parametrów jest bardziej efektywne. Jeśli na przykład chcielibyśmy zdefiniować funkcję przyjmującą dowolną liczbę parametrów i zwracającą ich sumę, nie moglibyśmy posłużyć się nazwanymi parametrami, ponieważ w chwili definiowania nie wiemy, ile ich będzie potrzebnych. W związku z tym w takim przypadku użycie obiektu arguments jest jak najbardziej uzasadnione. function sum() { var result = 0, i = 0, len = arguments.length; while (i < len) { result += arguments[i]; i++; } return result; } console.log(sum(1, 2)); console.log(sum(3, 4, 5, 6)); console.log(sum(50)); console.log(sum());

// 3 // 18 // 50 // 0

Funkcja sum() przyjmuje dowolną liczbę parametrów (w obiekcie arguments) i sumuje je za pomocą pętli while. To samo podejście zastosowalibyśmy, gdyby trzeba było zsumować elementy tablicy. Funkcja działa nawet wtedy, gdy nie przekażemy żadnego parametru, ponieważ lokalna zmienna result jest inicjowana wartością 0.

Przeciążanie Większość języków obiektowych daje możliwość przeciążania funkcji, które polega na zdefiniowaniu wielu sygnatur pojedynczej funkcji. Sygnatura składa się z nazwy funkcji oraz listy przyjmowanych przez nią parametrów wraz z ich typami. Tak więc można zdefiniować funkcję, której pierwsza sygnatura zawiera jeden parametr typu łańcuchowego, a druga sygnatura — dwa parametry numeryczne. Silnik języka wybiera odpowiednią wersję funkcji w zależności od przekazanych argumentów.

Funkcje

41

Jak wspomnieliśmy wcześniej, w języku JavaScript funkcje mogą przyjmować dowolną liczbę parametrów, a ich typy nie są w ogóle określane. Z tego prosty wniosek — w JavaScripcie funkcje nie mają sygnatur. To z kolei pociąga za sobą brak mechanizmu przeciążania. Zobaczmy, co się stanie, gdy spróbujemy zadeklarować dwie funkcje o tej samej nazwie: function sayMessage(message) { console.log(message); } function sayMessage() { console.log("Domyślny komunikat"); } sayMessage("Cześć!");

// wyświetla "Domyślny komunikat"

Gdybyśmy to napisali w innym języku, wywołanie sayMessage("Cześć!") spowodowałoby wyświetlenie komunikatu "Cześć!". W JavaScripcie przy próbie zdefiniowania wielu funkcji o tej samej nazwie ostatnia definicja „wygrywa”, czyli zastępuje poprzednie. W zrozumieniu przyczyn takiego zachowania pomaga znów spojrzenie na funkcje jak na obiekty: var sayMessage = new Function("message", "console.log(message);"); sayMessage = new Function("console.log(\"Domyślny komunikat\");"); sayMessage("Cześć!"); // wyświetla "Domyślny komunikat"

Dzięki przedstawieniu kodu w takiej postaci stało się jasne, dlaczego nie działa on tak, jak byśmy tego oczekiwali. Do zmiennej sayMessage zostaje dwukrotnie przypisany obiekt funkcji, więc jest oczywiste, że ten, który został przypisany jako pierwszy, znika w momencie drugiego przypisania. To, że w JavaScripcie nie istnieją sygnatury funkcji, nie oznacza, że nie można zastosować rozwiązania przypominającego przeciążanie. Można przecież odczytać liczbę parametrów przekazanych za pośrednictwem obiektu arguments i na tej podstawie zdecydować, którą wersję kodu wykonać. Na przykład: function sayMessage(message) { if (arguments.length === 0) { message = "Domyślny komunikat"; }

42

Rozdział 2

console.log(message); } sayMessage("Cześć!");

// wyświetla "Cześć!"

W tym przykładzie funkcja sayMessage() zachowuje się w różny sposób w zależności od liczby przekazanych argumentów. Jeśli żaden nie został przekazany (arguments.length === 0), zostaje ustawiony domyślny komunikat. W przeciwnym przypadku używany jest pierwszy parametr. Konstrukcja ta jest trochę bardziej zawiła niż w przypadku innych języków, ale osiąga się ten sam efekt. Jeśli istotne jest rozróżnienie typów przekazanych wartości, można skorzystać z operatorów typeof i instanceof. UWAGA Zwykle zamiast sprawdzania liczby przekazanych argumentów za pomocą właściwości arguments.length porównuje się nazwany parametr z undefined.

Metody obiektów Jak wspomnieliśmy w pierwszym rozdziale, do obiektów w dowolnej chwili można dodawać nowe właściwości oraz usuwać istniejące. Jeśli wartość właściwości jest funkcją, właściwość tę nazywa się metodą. Metody można dodawać do obiektów dokładnie na tej samej zasadzie co zwykłe właściwości. W poniższym przykładzie do zmiennej person zostaje przypisany literał obiektowy zawierający właściwość name oraz metodę sayName. var person = { name: "Nicholas", sayName: function() { console.log(person.name); } }; person.sayName();

// wyświetla "Nicholas"

Składnia dla właściwości przechowujących dane i metody jest dokładnie taka sama — identyfikator, po nim dwukropek i wartość. Wartością właściwości sayName jest funkcja, więc można ją wywołać na tym obiekcie: person.sayName("Nicholas").

Funkcje

43

Obiekt this W poprzednim przykładzie znalazła się dosyć dziwna konstrukcja. W metodzie sayName() odwołujemy się bezpośrednio do person.name, co tworzy silne powiązanie między metodą a obiektem. Jest to niewskazane z wielu powodów. Przede wszystkim w przypadku zmiany nazwy zmiennej trzeba będzie pamiętać o zmodyfikowaniu odwołania do niej w metodzie. Dodatkowo taka konstrukcja ogranicza możliwość zastosowania raz zdefiniowanej funkcji w innych obiektach. Na szczęście JavaScript pozwala na obejście tych problemów. Każdy zasięg w JavaScripcie posiada obiekt this reprezentujący obiekt, na którym została wywołana funkcja. W globalnym zasięgu this reprezentuje globalny obiekt (jeśli kod jest wykonywany w przeglądarce, jest to obiekt window). Jeśli funkcja jest wywoływana na obiekcie, wartość this jest domyślnie ustawiana na ten obiekt. W związku z tym zamiast bezpośrednio odwoływać się do obiektu wewnątrz metody, można to zrobić za pośrednictwem this. Kod z poprzedniego przykładu po wprowadzeniu this wygląda następująco: var person = { name: "Nicholas", sayName: function() { console.log(this.name); } }; person.sayName();

// wyświetla "Nicholas"

Kod ten działa dokładnie tak samo jak poprzednia jego wersja, ale tym razem w metodzie sayName() odwołujemy się do pola name poprzez this, a nie person. Dzięki temu można bez problemu zmienić nazwę zmiennej przechowującej obiekt, a nawet użyć tej funkcji w innych obiektach. function sayNameForAll() { console.log(this.name); } var person1 = { name: "Nicholas", sayName: sayNameForAll };

44

Rozdział 2

var person2 = { name: "Greg", sayName: sayNameForAll }; var name = "Michael"; person1.sayName(); person2.sayName();

// wyświetla "Nicholas" // wyświetla "Greg"

sayNameForAll();

// wyświetla "Michael"

W tym przykładzie rozpoczynamy od zdefiniowania funkcji sayName ForAll(). Następnie są tworzone dwa literały obiektowe, w których do właściwości sayName jest przypisywana funkcja sayNameForAll. Funkcje są typami referencyjnymi, więc można je przypisać na tej samej zasadzie jak zwykłe obiekty. Kiedy na obiekcie person1 wywołamy metodę sayName(), zostanie wyświetlone imię "Nicholas", natomiast jeżeli zrobimy to na obiekcie person2, pojawi się imię "Greg". Dzieje się tak dlatego, że obiekt this jest ustawiany w chwili wywołania, więc this.name zawsze wskazuje odpowiednie imię. W kodzie jest również definiowana globalna zmienna name. Kiedy funkcję sayNameForAll() wywołamy bezpośrednio, jest wyświetlane imię "Michael", ponieważ globalna zmienna jest traktowana jak właściwość globalnego obiektu.

Modyfikowanie this Możliwości używania wartości this i manipulowania nią są kluczowe dla wydajnego programowania obiektowego w JavaScripcie. Funkcje mogą być używane w wielu różnych kontekstach, więc muszą być przygotowane do pracy w każdej sytuacji. Mimo że wartość this jest ustalana automatycznie, można ją zmienić, by osiągnąć zamierzony cel. Istnieją trzy metody obiektu funkcji, które pozwalają na taką zmianę (pamiętaj, że funkcje są obiektami, więc mają również metody).

Metoda call() Pierwszą z metod umożliwiających zmianę this jest call(), która uruchamia funkcję z określoną wartością this. Wartość tę przekazujemy przez pierwszy parametr metody. Kolejne parametry zawierają wartości,

Funkcje

45

które mają zostać przekazane do wywoływanej funkcji jako jej parametry. Załóżmy, że zmodyfikowaliśmy funkcję sayNameForAll() znaną z poprzedniego przykładu, tak by przyjmowała jeden parametr: function sayNameForAll(label) { console.log(label + ":" + this.name); } var person1 = { name: "Nicholas" }; var person2 = { name: "Greg" }; var name = "Michael"; sayNameForAll.call(this, "globalny"); sayNameForAll.call(person1, "person1"); sayNameForAll.call(person2, "person2");

// wyświetla "globalny:Michael" // wyświetla "person1:Nicholas" // wyświetla "person2:Greg"

Funkcja sayNameForAll() przyjmuje jeden parametr, którym jest etykieta dołączana do wyświetlanego imienia. W dalszej części kodu funkcja ta jest wywoływana trzy razy. Zwróć uwagę, że po nazwie funkcji nie ma nawiasów — to dlatego, że w tym przypadku jest ona traktowana jako obiekt. W pierwszym wywołaniu został przekazany globalny obiekt this oraz łańcuch "globalny", co spowodowało wyświetlenie tekstu "globalny: Michael". Ta sama funkcja jest wywoływana jeszcze dwa razy — dla obiektów person1 i person2. Ponieważ jest stosowana metoda call(), w obiektach nie trzeba umieszczać metody sayName (jak to robiliśmy wcześniej). Wartość this określamy sami, więc silnik JavaScriptu nie musi już tego robić.

Metoda apply() Drugą metodą obiektu funkcji, dzięki której można modyfikować this, jest apply(). Działa ona bardzo podobnie jak call(), ale ma tylko dwa parametry: wartość dla this oraz tablicę lub obiekt pełniący rolę tablicy z wartościami parametrów, które mają zostać przekazane do wywoływanej funkcji (oznacza to, że drugim parametrem może być obiekt arguments).

46

Rozdział 2

W niektórych sytuacjach jest to wygodne, ponieważ zamiast przekazywać każdy parametr osobno (jak w przypadku metody call()), możemy przekazać je wszystkie za jednym razem (jako drugi parametr wywołania apply()). Poza tą różnicą działanie obu metod jest identyczne. Sposób wykorzystania metody apply() został przedstawiony w poniższym przykładzie: function sayNameForAll(label) { console.log(label + ":" + this.name); } var person1 = { name: "Nicholas" }; var person2 = { name: "Greg" }; var name = "Michael"; sayNameForAll.apply(this, ["globalny"]); // wyświetla "globalny:Michael" sayNameForAll.apply(person1, ["person1"]); // wyświetla "person1:Nicholas" sayNameForAll.apply(person2, ["person2"]); // wyświetla "person2:Greg"

Ten kod różni się od poprzedniego przykładu tylko jednym — w miejscu wywołań call() pojawiła się metoda apply(), co nie zmieniło w żaden sposób wyników działania. Wybór metody, którą w danej sytuacji powinno się zastosować, powinien być podyktowany formą danych. Jeśli mamy już tablicę wartości, lepiej użyć apply(), natomiast w przypadku pojedynczych zmiennych — call().

Metoda bind() Trzecia metoda umożliwiająca zmianę this to bind(). Została wprowadzona w ECMAScript 5 i jej działanie różni się od dwóch pozostałych. Pierwszy argument to wartość this dla nowej funkcji. Wszystkie pozostałe argumenty to nazwane parametry, które powinny być na stałe ustawione w nowej funkcji. Poniższy kod ilustruje dwa przykłady użycia metody bind(). Funkcję sayNameForPerson1() tworzymy poprzez powiązanie wartości this z obiektem person1. Z kolei funkcja sayNameForPerson2() wiąże this z obiektem person2, a pierwszy parametr wywoływanej funkcji ustawia na "person2".

Funkcje

47

function sayNameForAll(label) { console.log(label + ":" + this.name); } var person1 = { name: "Nicholas" }; var person2 = { name: "Greg" }; // tworzy funkcję powiązaną z obiektem person1  var sayNameForPerson1 = sayNameForAll.bind(person1); sayNameForPerson1("person1"); // wyświetla "person1:Nicholas" // tworzy funkcję powiązaną z obiektem person2  var sayNameForPerson2 = sayNameForAll.bind(person2, "person2"); sayNameForPerson2(); // wyświetla "person2:Greg" // dołączanie metody do obiektu nie zmienia wartości this  person2.sayName = sayNameForPerson1; person2.sayName("person2"); // wyświetla "person2:Nicholas"

Z funkcją sayNameForPerson1()  nie został powiązany żaden parametr, więc trzeba go przekazać w wywołaniu. W przypadku funkcji sayNameForPerson2() nie tylko powiązaliśmy this z obiektem person2, ale ustawiliśmy pierwszy parametr na "person2" . Dzięki temu można wywołać tę funkcję bez dodatkowych argumentów. W ostatniej części przykładu do obiektu person2 dodajemy właściwość sayName i przypisujemy do niej funkcję sayNameForPerson1 . W funkcji tej this powiązaliśmy z person1, czego nie zmieni nawet to, że sayNameForPerson1 jest teraz metodą obiektu person2 i nadal wyświetla wartość person1.name.

Podsumowanie Funkcje w języku JavaScript są wyjątkowe, ponieważ są obiektami, a więc można je kopiować, nadpisywać i traktować jak wszystkie inne „normalne” obiekty. Funkcje odróżnia jednak od innych obiektów specjalna wewnętrzna właściwość [[Call]], która zawiera instrukcje wykonywania

48

Rozdział 2

funkcji. Operator typeof szuka tej właściwości i jeśli ją znajdzie, zwraca wartość "function". Istnieją dwie formy literału funkcji: deklaracje i wyrażenia. Deklaracje funkcji składają się z nazwy funkcji poprzedzonej słowem kluczowym function. Co ważne, deklaracje są przenoszone na początek kontekstu (ang. hoisting), w którym zostały umieszczone. Wyrażenia funkcyjne są stosowane tam, gdzie można użyć również innych wartości, czyli w przypisaniach, parametrach funkcji, a także jako wartości zwracane przez inne funkcje. Ponieważ funkcje są obiektami, posiadają konstruktor Function. Można go oczywiście użyć do tworzenia nowych funkcji, ale nie jest to polecane, ponieważ tak skonstruowany kod trudniej się analizuje i debuguje. Taka możliwość przydaje się jednak od czasu do czasu, gdy ciało funkcji nie jest znane przed uruchomieniem kodu. Funkcje trzeba dobrze zrozumieć, ponieważ to na nich opiera się obiektowość w JavaScripcie. Ze względu na brak klas funkcje i inne obiekty są wszystkim, czym dysponujemy, by tworzyć obiektowy kod wykorzystujący mechanizmy kompozycji i dziedziczenia.

Funkcje

49

50

Rozdział 2

3 Obiekty

MIMO ŻE W JAVASCRIPCIE ISTNIEJE WIELE WBUDOWANYCH TYPÓW REFERENCYJNYCH, BARDZO CZĘSTO TWORZY SIĘ WŁASNE OBIEKTY. TRZEBA PAMIĘTAĆ, ŻE OBIEKTY W JavaScripcie są dynamiczne, czyli można je modyfikować w czasie wykonywania kodu, co nie jest możliwe w wielu innych językach obiektowych, w których definicja klasy blokuje strukturę obiektu. Programowanie w JavaScripcie polega przede wszystkim na pracy z obiektami, więc zrozumienie wszystkich mechanizmów obiektowości jest kluczowe dla zrozumienia JavaScriptu jako języka. Zagadnienia te zostaną omówione w dalszej części rozdziału.

Definiowanie właściwości W pierwszym rozdziale zostały przedstawione dwa podstawowe sposoby tworzenia obiektów: za pomocą konstruktora Object i literału obiektu. Oto przykład:

var person1 = { name: "Nicholas" }; var person2 = new Object(); person2.name = "Nicholas";  person1.age = "ukryty"; person2.age = "ukryty";  person1.name = "Greg"; person2.name = "Michael";

Oba obiekty — person1 i person2 — mają właściwość name. W dalszej części kodu w obu obiektach jest ustawiana właściwość age . Można to zrobić bezpośrednio po definicji obiektu lub w dowolnym innym momencie. Tworzone obiekty są gotowe na modyfikacje, chyba że zastosuje się rozwiązanie blokujące taką możliwość (więcej szczegółów na ten temat znajdziesz w podrozdziale „Zapobieganie modyfikowaniu obiektów” na stronie 45). Na końcu przykładu zmieniamy wartość właściwości name obu obiektów  — również tę operację można oczywiście wykonać w dowolnym momencie. W chwili dodawania właściwości do obiektu silnik JavaScriptu wywołuje wewnętrzną metodę [[Put]]. Jej zadaniem jest przygotowanie w obiekcie miejsca, w którym zostanie zapisana właściwość. Jest to operacja analogiczna do dodawania nowego klucza do tablicy asocjacyjnej. Poza ustawieniem początkowej wartości są jeszcze ustalane atrybuty właściwości. W omawianym przykładzie metoda [[Put]] jest wywoływana czterokrotnie — przy dodawaniu właściwości age i name do każdego z dwóch obiektów. Wynikiem działania metody [[Put]] jest utworzenie właściwości przynależącej do konkretnego obiektu. Jest ona zapisywana w danej instancji i wszystkie operacje na tej właściwości muszą być realizowane poprzez ten obiekt. UWAGA Właściwości należące do obiektu są czymś innym niż właściwości

prototypu, które są opisane w rozdziale 4. Przypisanie wartości do istniejącej właściwości wiąże się z wywołaniem wewnętrznej metody [[Set]]. Zmienia ona bieżącą wartość właściwości na nową. W powyższym przykładzie taka sytuacja ma miejsce

52

Rozdział 3

przy drugim przypisaniu do pola name. Na rysunku 3.1 zostały przedstawione operacje wykonywane przez silnik JavaScriptu na obiekcie person1, gdy dodajemy i modyfikujemy jego właściwości age i name.

Rysunek 3.1. Dodawanie i modyfikowanie właściwości obiektu Zaczynamy od utworzenia obiektu person1 za pomocą literału. Wykonywana jest wtedy operacja [[Put]] dodająca właściwość name. Przypisanie wartości do person1.age powoduje wywołanie metody [[Put]] dla właściwości age. Z kolei przypisanie nowej wartości ("Greg") do person1.name powoduje wykonanie operacji [[Set]] na właściwości name, która nadpisuje dotychczasową wartość.

Wykrywanie właściwości Ponieważ właściwości mogą być dodawane w dowolnym momencie, czasem konieczne jest sprawdzenie, czy dana właściwość znajduje się już w obiekcie. Początkujący programiści JavaScriptu często stosują w tej sytuacji nieprawidłową konstrukcję: // antywzorzec! if (person1.age) { // operacja na właściwości age }

Cały problem z tą konstrukcją wynika z wewnętrznego mechanizmu rzutowania zastosowanego w JavaScripcie. Warunek podany w instrukcji if zostanie uznany za true, jeśli jego wartość jest „prawdziwa” (czyli jest obiektem, niepustym łańcuchem znaków, niezerową wartością numeryczną lub wartością true), a za false, jeżeli jest „fałszywa” (null, undefined, 0, false, NaN lub pusty łańcuch znaków). Łatwo więc zauważyć, że jeśli

Obiekty

53

wykrywana właściwość ma jedną z „fałszywych” wartości, zostanie uznana za nieistniejącą. Na przykład jeżeli właściwość person1.age miałaby wartość 0, warunek instrukcji if nie zostałby spełniony, mimo że właściwość istnieje. Lepszym sposobem sprawdzania istnienia właściwości jest użycie operatora in. Operator in szuka w określonym obiekcie właściwości o podanej nazwie i jeśli ją znajdzie, zwraca true. Operacja ta sprowadza się do wyszukania w tablicy asocjacyjnej określonego klucza. Sposób użycia operatora został przedstawiony w poniższym przykładzie: console.log("name" in person1); console.log("age" in person1); console.log("title" in person1);

// true // true // false

Pamiętaj, że metody również są właściwościami (przechowującymi referencje do funkcji), więc w ten sam sposób można sprawdzać istnienie metod. W poniższym przykładzie do obiektu person1 dodajemy funkcję sayName(), a następnie za pomocą operatora in sprawdzamy, czy została dodana: var person1 = { name: "Nicholas", sayName: function() { console.log(this.name); } }; console.log("sayName" in person1);

// true

W większości przypadków wykrywanie istnienia właściwości w obiektach za pomocą operatora in jest najlepszym rozwiązaniem. Za dodatkową zaletę należy uznać to, że nie dochodzi do wyznaczenia wartości sprawdzanej właściwości, co może się okazać szczególnie istotne, w sytuacji gdy dana operacja spowoduje błąd lub wstrzymanie działania programu. Istnieje jednak jeden szczególny przypadek — gdy chcemy sprawdzić obecność właściwości instancji (ang. own property — własna właściwość). Operator in wykrywa zarówno właściwości dodane, jak i te, które istnieją w prototypie, w związku z czym musimy zastosować inne podejście. Z pomocą przychodzi metoda hasOwnProperty() obecna we wszystkich obiektach, która zwraca true, jeśli dana właściwość istnieje 54

Rozdział 3

i jest to właściwość instancji. W poniższym przykładzie znajduje się porównanie wyników operatora in i metody hasOwnProperty() dla różnych właściwości obiektu person1: var person1 = { name: "Nicholas", sayName: function() { console.log(this.name); } }; console.log("name" in person1); console.log(person1.hasOwnProperty("name"));

// true // true

console.log("toString" in person1); // true  console.log(person1.hasOwnProperty("toString")); // false

W powyższym przykładzie name to właściwość instancji, więc zarówno operator in, jak i metoda hasOwnProperty() zwracają true. Z kolei metoda toString() to właściwość prototypu, która jest obecna we wszystkich obiektach. Operator in zwraca dla niej wartość true, natomiast metoda hasOwnProperty() — false . Jest to dosyć istotne rozróżnienie, które jest dokładniej opisane w rozdziale 4.

Usuwanie właściwości Właściwości mogą być w dowolnym momencie nie tylko dodawane do obiektu, ale również z niego usuwane. Przypisanie null do właściwości nie powoduje jej usunięcia, ponieważ tak naprawdę zostaje w takiej sytuacji wywołana wewnętrzna metoda [[Set]], która jedynie modyfikuje jej wartość. Aby usunąć właściwość z obiektu, trzeba użyć operatora delete. Operator delete działa na pojedynczej właściwości obiektu i wywołuje wewnętrzną metodę [[Delete]]. Operację tę można potraktować jak usunięcie pary klucz-wartość z tablicy asocjacyjnej. Jeśli operacja zakończy się sukcesem, operator delete zwraca true (niektóre właściwości nie mogą zostać usunięte; zostanie to opisane w dalszej części rozdziału). W poniższym przykładzie zostało przedstawione działanie operatora delete:

Obiekty

55

var person1 = { name: "Nicholas" }; console.log("name" in person1);

// true

delete person1.name; // true - not output console.log("name" in person1); // false  console.log(person1.name); // undefined

W powyższym kodzie z obiektu person1 usuwana jest właściwość name. Po tej operacji operator in zwraca false. Odczyt właściwości, która nie istnieje, skutkuje wynikiem undefined . Na rysunku 3.2 zostało zilustrowane działanie operatora delete.

Rysunek 3.2. Po usunięciu właściwości name za pomocą operatora delete, znika ona z obiektu person1

Wyliczenia Wszystkie właściwości dodawane do obiektu są domyślnie wyliczalne (ang. enumerable), czyli można po nich iterować za pomocą pętli for-in. Wyliczalne właściwości mają wewnętrzny atrybut [[Enumerable]] ustawiony na true. Pętla for-in przechodzi przez wszystkie właściwości wyliczalne w obiekcie i w każdej iteracji przypisuje nazwę właściwości do zmiennej. Kod z poniższego przykładu wyświetla nazwy wszystkich właściwości obiektu wraz z ich wartościami: var property; for (property in object) {

56

Rozdział 3

console.log("Nazwa: " + property); console.log("Wartość: " + object[property]); }

W każdym obiegu pętli for-in zmienna property jest wypełniana kolejną wyliczalną właściwością obiektu. Proces ten jest przeprowadzany dla wszystkich właściwości, po czym pętla kończy działanie. W powyższym przykładzie wartość właściwości jest odczytywana za pomocą notacji z nawiasami kwadratowymi. Jest to typowa sytuacja, w której stosuje się tę notację w JavaScripcie. Jeśli w programie jest potrzebna lista wszystkich właściwości jakiegoś obiektu, można skorzystać z metody Object.keys(), która zwraca tablicę nazw wyliczalnych właściwości:  var properties = Object.keys(object); // rozwiązanie alternatywne dla pętli for-in var i, len; for (i=0, len=properties.length; i < len; i++){ console.log("Nazwa: " + properties[i]); console.log("Wartość: " + object[properties[i]]); }

W tym kodzie za pomocą metody Object.keys() pobieramy nazwy właściwości obiektu object . Następnie za pomocą pętli for iterujemy po tych właściwościach i wyświetlamy ich nazwy oraz wartości. Metody Object.keys() używa się głównie w sytuacjach, gdy chcemy uzyskać tablicę nazw właściwości, a instrukcji for-in, gdy ta tablica nie jest potrzebna. UWAGA Trzeba wiedzieć o różnicy między właściwościami zwracanymi przez metodę Object.keys() a tymi, które uzyskujemy za pomocą pętli for-in. Metoda zwraca listę właściwości instancji, natomiast pętla iteruje

również po właściwościach prototypu. Różnice między właściwościami instancji i prototypu są omówione szerzej w rozdziale 4. Musisz pamiętać, że nie wszystkie właściwości są wyliczalne. Tak naprawdę większość natywnych metod obiektów ma atrybut [[Enumerable]] ustawiony na false. Do sprawdzania, czy dana właściwość jest wyliczalna, służy metoda propertyIsEnumerable(), którą można wywołać na każdym obiekcie:

Obiekty

57

var person1 = { name: "Nicholas" }; console.log("name" in person1);  console.log(person1.propertyIsEnumerable("name"));

// true // true

var properties = Object.keys(person1); console.log("length" in properties); // true  console.log(properties.propertyIsEnumerable("length")); // false

Jak widać, właściwość name jest wyliczalna, ponieważ jest właściwością instancji zdefiniowaną w obiekcie person1 . Z kolei właściwość length tablicy properties nie jest wyliczalna , ponieważ jest wbudowaną właściwością prototypu typu Array (Array.prototype). Większość natywnych właściwości domyślnie nie jest wyliczalna.

Rodzaje właściwości Istnieją dwa odmienne rodzaje właściwości: danych i funkcji dostępowych. Właściwości danych zawierają wartości, tak jak w przypadku właściwości name z poprzednich przykładów z tego rozdziału. Domyślną operacją wykonywaną przez metodę [[Put]] jest utworzenie tego typu właściwości. We wszystkich dotychczasowych przykładach mieliśmy do czynienia tylko z właściwościami danych. Z kolei właściwości funkcji dostępowych nie zawierają wartości, lecz definicję funkcji wywoływanej w celu odczytania (tzw. getter) lub zapisania (tzw. setter) wartości właściwości. Właściwości tego typu wymagają co najmniej jednej z tych funkcji, ale można oczywiście zdefiniować obie. Aby zdefiniować właściwość funkcji dostępowych, w literale obiektu należy użyć specjalnej składni: var person1 = {  _name: "Nicholas", 

get name() { console.log("Odczytuję właściwość name"); return this._name;

58

Rozdział 3

}, 

set name(value) { console.log("Właściwość name ustawiam na %s", value); this._name = value; }

}; console.log(person1.name); person1.name = "Greg"; console.log(person1.name);

// "Odczytuję właściwość name", a następnie // "Nicholas" // "Właściwość name ustawiam na", // a następnie "Greg"

W kodzie definiujemy właściwość funkcji dostępowych o nazwie name. Wartość powiązaną z tą właściwością przechowujemy we właściwości danych nazwanej _name . (Została tu zastosowana typowa konwencja, w której nazwy właściwości z założenia prywatnych poprzedza się znakiem podkreślenia. Trzeba jednak pamiętać, że tak naprawdę właściwości te pozostają publiczne). Składnia zastosowana do zdefiniowania gettera  i settera  przypomina składnię funkcji, ale bez słowa function. Zamiast niego — czyli przed nazwą właściwości, po której następują nawiasy okrągłe i ciało funkcji — należy zastosować jedno z dwóch słów kluczowych: get lub set. Getter musi zwracać wartość, natomiast setter otrzymuje wartość powiązanej właściwości jako argument. Chociaż w tym przykładzie wartość właściwości jest zapisywana w polu _name, może być ona równie dobrze zapisana w dowolnej zmiennej, a nawet w innym obiekcie. Zdefiniowane w przykładzie funkcje dostępowe wyświetlają w konsoli komunikat o wykonanej operacji. Zwykle nie ma potrzeby używania funkcji dostępowych, jeśli służą tylko do ustawiania i odczytywania wartości innej właściwości. W takich przypadkach lepiej pozostać przy jednej właściwości przechowującej dane. Właściwości funkcji dostępowych są za to przydatne w sytuacjach, gdy w chwili przypisywania wartości chcemy wyzwolić jakąś inną akcję albo — podczas odczytywania — wyliczyć zwracaną wartość. UWAGA Nie trzeba za każdym razem definiować obu funkcji dostępo-

wych. Jeśli zdefiniujemy tylko funkcję gettera, utworzymy właściwość tylko do odczytu. Przy próbie przypisania wartości takiej właściwości

Obiekty

59

zostanie zgłoszony błąd (w trybie ścisłym1) lub nic się nie stanie (w trybie zwykłym). Jeśli zdefiniujemy tylko setter, właściwość stanie się tylko do zapisu, więc próba odczytu nie powiedzie się (bez zgłaszania błędu w obu trybach).

Atrybuty właściwości We wcześniejszych wersjach języka JavaScript (do ECMAScript 5) nie istniała możliwość określenia, czy właściwość ma być wyliczalna. Tak naprawdę nie można było się dostać do żadnych wewnętrznych atrybutów właściwości. W ECMAScript 5 zostało wprowadzonych kilka sposobów na bezpośredni dostęp do tych atrybutów, a także pojawiły się nowe atrybuty obsługujące dodatkowe możliwości. Dzięki temu można na przykład tworzyć właściwości instancji, które zachowują się dokładnie tak samo jak właściwości wbudowane w język. W tym podrozdziale zostały szczegółowo opisane atrybuty właściwości danych i funkcji dostępowych. Jako pierwsze zostały przedstawione te, które występują w obu rodzajach właściwości.

Wspólne atrybuty Istnieją dwa atrybuty występujące w obu rodzajach właściwości. Pierwszy z nich to [[Enumerable]], który określa, czy właściwość zostanie uwzględniona podczas iterowania po zawierającym ją obiekcie. Drugi atrybut — [[Configurable]] — określa, czy właściwość jest konfigurowalna, czyli czy jest możliwa jej zmiana. Konfigurowalne właściwości można usuwać za pomocą operatora delete, a także zmieniać ich atrybuty (co oznacza również, że można zmieniać ich rodzaj — z właściwości danych na właściwość funkcji dostępowych, i odwrotnie). Wszystkie dodawane do obiektu właściwości są domyślnie wyliczalne i konfigurowalne. Aby zmienić atrybut właściwości, należy użyć metody Object.define Property(), która przyjmuje trzy argumenty: obiekt zawierający wła1

Tryb ścisły (ang. strict mode) został wprowadzony w ECMAScript 5. Pozwala na korzystanie z nowszej, ulepszonej wersji języka. Można go włączyć poprzez umieszczenie na samym początku skryptu lub funkcji łańcucha "use script". Dobre opracowanie (w j. angielskim) na temat trybu ścisłego można znaleźć np. na stronie https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ Functions_and_function_scope/Strict_mode — przyp. tłum.

60

Rozdział 3

ściwość, nazwę właściwości i deskryptor właściwości definiujący stan atrybutów. W deskryptorze umieszcza się właściwości o tych samych nazwach co wewnętrzne atrybuty, ale bez nawiasów kwadratowych. W związku z tym, aby ustawić właściwość jako wyliczalną ([[Enumerable]]), w deskryptorze należy użyć właściwości enumerable, a w przypadku atrybutu konfigurowalności ([[Configurable]]) — configurable. W poniższym przykładzie ustawiamy właściwość obiektu jako niewyliczalną i niekonfigurowalną: var person1 = {  name: "Nicholas" }; Object.defineProperty(person1, "name", {  enumerable: false }); console.log("name" in person1); // true  console.log(person1.propertyIsEnumerable("name")); // false var properties = Object.keys(person1); console.log(properties.length);

// 0

Object.defineProperty(person1, "name", {  configurable: false }); // próbujemy usunąć właściwość delete person1.name;  console.log("name" in person1); console.log(person1.name);  Object.defineProperty(person1, "name", { configurable: true });

// true // "Nicholas" // błąd!!!

Właściwość name definiujemy w standardowy sposób , ale od razu ustawiamy jej atrybut [[Enumerable]] na false . Metoda propertyIs Enumerable() zwraca false , ponieważ odnosi się do nowej wartości atrybutu [[Enumerable]]. Następnie określamy właściwość name jako niekonfigurowalną , przez co niemożliwe staje się jej usunięcie, ponieważ nie może być w żaden sposób zmieniona. Po próbie usunięcia nadal znajduje się w obiekcie person1 .

Obiekty

61

W ostatnim fragmencie kodu staramy się przedefiniować właściwość name, tak by znów była konfigurowalna. Ponowne wywołanie metody Object.defineProperty()  nie przyniesie jednak oczekiwanych rezul-

tatów, ponieważ została zablokowana możliwość zmiany atrybutów właściwości. Ponadto w trybie ścisłym zostanie zgłoszony błąd. Podobnie stanie się na przykład przy próbie zmiany właściwości danych na właściwość funkcji dostępowych. UWAGA Jeśli JavaScript działa w trybie ścisłym, próba usunięcia niekon-

figurowalnej właściwości powoduje zgłoszenie błędu. W trybie zwykłym operacja nie zostaje wykonana, ale błąd nie jest zgłaszany.

Atrybuty właściwości danych Właściwości danych posiadają dwa atrybuty, których nie mają właściwości funkcji dostępowych. Pierwszy z nich to [[Value]], który przechowuje wartość właściwości. Atrybut ten jest automatycznie wypełniany w chwili dodawania właściwości do obiektu. Wszystkie wartości są przechowywane w tym atrybucie, nawet jeśli są funkcjami. Drugi z atrybutów to [[Writable]], który jest wartością logiczną wskazująca, czy do właściwości można wpisać wartość. Wszystkie właściwości mają ten atrybut domyślnie ustawiony na true. Dzięki tym dwóm dodatkowym atrybutom i metodzie Object.define Property() można w pełni zdefiniować właściwość danych, nawet jeśli jeszcze nie dodaliśmy jej do obiektu. Ilustrują to dwa poniższe fragmenty kodu. var person1 = { name: "Nicholas" };

Ten fragment pojawiał się już wielokrotnie, więc wiesz, że definiujemy w nim obiekt person1 z właściwością name o wartości "Nicholas". To samo można osiągnąć za pomocą poniższego kodu: var person1 = {}; Object.defineProperty(person1, "name", { value: "Nicholas", enumerable: true,

62

Rozdział 3

configurable: true, writable: true });

Po wywołaniu metoda Object.defineProperty() sprawdza, czy podana właściwość już istnieje. Jeśli nie istnieje, dodaje ją do obiektu i ustawia jej atrybuty zgodnie z definicjami z deskryptora. W omawianym przykładzie właściwość name nie istniała wcześniej w obiekcie, więc jest tworzona. Jeśli do definiowania nowej właściwości jest używana metoda Object. defineProperty(), trzeba koniecznie ustalić wartość wszystkich atrybutów, które chcemy ustawić na true, ponieważ domyślną wartością właściwości typu Boolean jest false. W poniższym kodzie zostanie utworzona właściwość name, której nie można wyliczać, konfigurować i zapisywać, ponieważ odpowiadające tym cechom atrybuty nie zostały jawnie ustawione na true: var person1 = {}; Object.defineProperty(person1, "name", { value: "Nicholas" }); console.log("name" in person1); // true console.log(person1.propertyIsEnumerable("name")); // false delete person1.name; console.log("name" in person1);

// true

person1.name = "Greg"; console.log(person1.name);

// "Nicholas"

Z właściwością name nie można wiele zrobić. Tak naprawdę można tylko odczytać jej wartość, ponieważ pozostałe możliwości zostały zablokowane. Jeśli modyfikuje się istniejącą właściwość, zmianie ulegną tylko te atrybuty, które zostały przekazane w deskryptorze. UWAGA Jeśli JavaScript działa w trybie ścisłym, próba zmiany wartości

właściwości, w której nie można zapisywać, powoduje zgłoszenie błędu. W trybie zwykłym operacja nie zostaje wykonana, ale błąd nie jest zgłaszany.

Obiekty

63

Atrybuty właściwości funkcji dostępowych Właściwości funkcji dostępowych mają również dwa dodatkowe atrybuty. Ponieważ w tego typu właściwościach nie są zapisywane wartości, atrybuty [[Value]] i [[Writable]] nie są potrzebne. Niezbędne są za to atrybuty [[Get]] i [[Set]], które przechowują funkcje gettera i settera. Tak jak w przypadku literału obiektu wystarczy zdefiniować jedną z nich, by właściwość została utworzona. UWAGA Przy próbie utworzenia właściwości zawierającej wymieszane

atrybuty obu rodzajów właściwości zostanie zgłoszony błąd. Dzięki tym atrybutom i metodzie Object.defineProperty() w dowolnej chwili do każdego obiektu można dodać funkcje dostępowe. Podobnie jak w przypadku właściwości danych, możemy też ustawić atrybuty konfigurowalności i wyliczalności. Przypatrzmy się jednemu z wcześniejszych przykładów: var person1 = { _name: "Nicholas", get name() { console.log("Odczytuję właściwość name"); return this._name; }, set name(value) { console.log("Właściwość name ustawiam na %s", value); this._name = value; } };

Nic nie stoi na przeszkodzie, by zapisać to w taki sposób: var person1 = { _name: "Nicholas" }; Object.defineProperty(person1, "name", { get: function() { console.log("Odczytuję właściwość name"); return this._name;

64

Rozdział 3

}, set: function(value) { console.log("Właściwość name ustawiam na %s", value); this._name = value; }, enumerable: true, configurable: true });

Zwróć uwagę, że w obiekcie przekazywanym metodzie Object.define Property() nie są stosowane literały funkcji dostępowych. Funkcje są wartościami właściwości o kluczach get i set. Dzięki dwóm pozostałym atrybutom ([[Enumerable]] i [[Configurable]]) istnieje możliwość zmiany zachowania funkcji dostępowych. Można na przykład zdefiniować właściwość, której nie da się konfigurować, wyliczać i zapisywać: var person1 = { _name: "Nicholas" }; Object.defineProperty(person1, "name", { get: function() { console.log("Odczytuję właściwość name");  return this._name; } }); console.log("name" in person1); // true console.log(person1.propertyIsEnumerable("name")); // false delete person1.name; console.log("name" in person1); // true person1.name = "Greg"; console.log(person1.name);

// "Nicholas"

Właściwość name jest właściwością funkcji dostępowych, w której został zdefiniowany jedynie getter . Nie ma settera, a możliwość modyfikowania atrybutów została zablokowana, więc wartość name może być tylko odczytywana.

Obiekty

65

UWAGA Jeśli właściwość funkcji dostępowych nie ma zdefiniowanego

settera, przy próbie zmiany wartości w trybie ścisłym zgłaszany jest błąd. Reakcja jest więc taka sama jak w przypadku właściwości funkcji dostępowych definiowanych za pomocą notacji literału obiektu. W trybie zwykłym operacja nie jest wykonywana, ale błąd nie jest zgłaszany. Próba odczytania właściwości funkcji dostępowych, w której jest zdefiniowany jedynie setter, zawsze zwraca undefined.

Definiowanie wielu właściwości Możliwe jest również dodanie do obiektu wielu właściwości naraz. Służy do tego metoda Object.defineProperties(), która przyjmuje dwa argumenty: obiekt, do którego chcemy dodać właściwości, oraz obiekt zawierający definicje wszystkich właściwości. Kluczami w obiekcie definicji są nazwy właściwości, a wartościami — obiekty deskryptorów definiujące atrybuty tych właściwości. W poniższym przykładzie definiujemy dwie właściwości: var person1 = {}; Object.defineProperties(person1, { 

// właściwość do przechowywania danych _name: { value: "Nicholas", enumerable: true, configurable: true, writable: true },



// właściwość funkcji dostępowych name: { get: function() { console.log("Odczytuję właściwość name"); return this._name; }, set: function(value) { console.log("Właściwość name ustawiam na %s", value); this._name = value; }, enumerable: true, configurable: true }

});

66

Rozdział 3

W tym przykładzie _name to właściwość służąca do przechowywania danych , natomiast name to właściwość funkcji dostępowych . Za pomocą metody Object.defineProperties() można zdefiniować dowolną liczbę właściwości. Można też w tym samym czasie zmieniać istniejące właściwości i tworzyć nowe. Metoda Object.defineProperties() daje te same efekty co wielokrotne wywołanie Object.defineProperty().

Pobieranie atrybutów właściwości Do odczytywania atrybutów właściwości służy metoda Object.getOwn PropertyDescriptor(). Jak wynika z nazwy, działanie metody ogranicza się tylko do właściwości instancji. Metoda przyjmuje dwa argumenty: obiekt, w którym znajduje się właściwość, oraz nazwę właściwości. Jeśli dana właściwość istnieje, metoda zwraca obiekt deskryptora z czterema polami: configurable, enumerable i dwoma pozostałymi, zależnymi od rodzaju właściwości. W obiekcie znajdą się nawet te atrybuty, które nie zostały jawnie ustawione. Kod z poniższego przykładu tworzy właściwość i sprawdza jej atrybuty: var person1 = { name: "Nicholas" }; var descriptor = Object.getOwnPropertyDescriptor(person1, "name"); console.log(descriptor.enumerable); console.log(descriptor.configurable); console.log(descriptor.writable); console.log(descriptor.value);

// true // true // true // "Nicholas"

Właściwość name jest definiowana w literale obiektu. W wyniku wywołania metody Object.getOwnPropertyDescriptor() jest zwracany obiekt z polami enumerable, configurable, writable i value, chociaż odpowiadające im atrybuty nie zostały ustawione za pomocą metody Object.define Property().

Obiekty

67

Zapobieganie modyfikowaniu obiektu Obiekty, podobnie jak właściwości, mają wewnętrzne atrybuty wpływające na ich zachowanie. Jednym z tych atrybutów jest [[Extensible]], który określa, czy obiekt może być modyfikowany. Wszystkie tworzone obiekty mają ten atrybut domyślnie ustawiony na true, czyli są rozszerzalne, a więc w dowolnej chwili można do nich dodawać nowe właściwości. Mechanizm ten był wykorzystywany w wielu dotychczasowych przykładach. Przez ustawienie atrybutu [[Extensible]] na false blokujemy możliwość dodawania nowych właściwości. Można to zrobić na trzy sposoby.

Zapobieganie rozszerzaniu Jednym ze sposobów tworzenia nierozszerzalnych obiektów jest użycie metody Object.preventExtensions(), która przyjmuje jeden argument: obiekt, który ma się stać nierozszerzalny. Po wykonaniu tej operacji do obiektu nie będzie już można dodać żadnej właściwości. Aby sprawdzić stan atrybutu [[Extensible]], należy użyć metody Object.isExtensible(). Działanie obu metod ilustruje poniższy przykład. var person1 = { name: "Nicholas" };  console.log(Object.isExtensible(person1)); // true  Object.preventExtensions(person1); console.log(Object.isExtensible(person1));

// false

 person1.sayName = function() { console.log(this.name); }; console.log("sayName" in person1);

// false

Po utworzeniu obiektu person1 sprawdzamy atrybut [[Extensible]] , a następnie wyłączamy możliwość rozszerzania . W związku z tym próba dodania metody sayName()  nie powiedzie się.

68

Rozdział 3

UWAGA W trybie ścisłym próba dodania właściwości do nierozszerzalne-

go obiektu prowadzi do zgłoszenia błędu. W trybie zwykłym błąd nie jest zgłaszany. W związku z tym podczas korzystania z nierozszerzalnych obiektów powinno się używać trybu ścisłego, by być informowanym o nieprawidłowościach.

Pieczętowanie obiektów Drugim sposobem na utworzenie obiektu niedającego się rozszerzać jest jego zapieczętowanie (ang. seal). Tego typu obiekt jest nierozszerzalny, a wszystkie jego właściwości są niekonfigurowalne. Oznacza to, że nie tylko nie można dodawać nowych właściwości, ale zablokowana jest również możliwość usuwania i modyfikowania typu istniejących właściwości. Jeśli obiekt jest zapieczętowany, można jedynie odczytywać i zapisywać jego właściwości. Aby utworzyć zapieczętowany obiekt, należy użyć metody Object.seal(). Ustawia ona atrybut [[Extensible]] obiektu i atrybuty [[Configurable]] wszystkich właściwości na false. Do sprawdzenia, czy obiekt jest zapieczętowany, służy metoda Object.isSealed(): var person1 = { name: "Nicholas" }; console.log(Object.isExtensible(person1)); console.log(Object.isSealed(person1));

// true // false

 Object.seal(person1);  console.log(Object.isExtensible(person1)); // false console.log(Object.isSealed(person1)); // true  person1.sayName = function() { console.log(this.name); }; console.log("sayName" in person1);

// false

 person1.name = "Greg"; console.log(person1.name);

// "Greg"

 delete person1.name; console.log("name" in person1);

// true

Obiekty

69

console.log(person1.name);

// "Greg"

var descriptor = Object.getOwnPropertyDescriptor(person1, "name"); console.log(descriptor.configurable); // false

Zaczynamy od zapieczętowania obiektu person1 , więc blokujemy możliwość dodawania i usuwania właściwości. Ponieważ zapieczętowanych obiektów nie można rozszerzać, metoda Object.isExtensible() zwraca false , a próba dodania metody sayName()  kończy się niepowodzeniem. Mimo że udało się zmienić wartość właściwości name , jej usunięcie jest niemożliwe . Jeśli znasz jakiś klasyczny język obiektowy, taki jak Java czy C++, mechanizm pieczętowania możesz porównać z tworzeniem instancji klasy. Do utworzonego obiektu nie można dodawać nowych właściwości, ale — jeśli właściwość zawiera jakiś obiekt — można go zmienić. Dzięki mechanizmowi pieczętowania można więc osiągnąć podobny stopnień kontroli nad obiektami jak za pomocą klas. UWAGA Kiedy pracujesz z zapieczętowanymi obiektami, korzystaj z trybu

ścisłego, tak by otrzymywać informacje o błędach.

Zamrażanie obiektów Ostatnim sposobem na tworzenie nierozszerzalnych obiektów jest ich zamrażanie (ang. freeze). Po zamrożeniu nie można dodawać i usuwać właściwości, zmieniać ich typu, a także zapisywać wartości do właściwości danych. Zamrożony obiekt jest więc zapieczętowanym obiektem z właściwościami ustawionymi na tryb tylko do odczytu. Tej operacji nie można wycofać, więc do końca działania programu obiekty pozostają w stanie, w jakim były w chwili zamrożenia. Do zamrażania obiektów służy metoda Object.freeze(), a do sprawdzenia, czy obiekt jest zamrożony, metoda Object.isFrozen(). Oto przykład: var person1 = { name: "Nicholas" }; console.log(Object.isExtensible(person1)); console.log(Object.isSealed(person1)); console.log(Object.isFrozen(person1));

70

Rozdział 3

// true // false // false

 Object.freeze(person1);  console.log(Object.isExtensible(person1)); // false  console.log(Object.isSealed(person1)); // true console.log(Object.isFrozen(person1)); // true person1.sayName = function() { console.log(this.name); }; console.log("sayName" in person1);

// false

 person1.name = "Greg"; console.log(person1.name);

// "Nicholas"

delete person1.name; console.log("name" in person1); console.log(person1.name);

// true // "Nicholas"

var descriptor = Object.getOwnPropertyDescriptor(person1, "name"); console.log(descriptor.configurable); // false console.log(descriptor.writable); // false

W powyższym przykładzie zamrożony jest obiekt person1 . Tego typu obiekty są również zapieczętowane i nie można ich rozszerzać, więc metoda Object.isExtensible() zwraca false , a metoda Object.isSealed() zwraca true . Właściwość name nie może zostać zmieniona, więc próba przypisania imienia "Greg" się nie powiedzie , a w wyniku jej odczytania uzyskamy "Nicholas". UWAGA Zamrożony obiekt jest wykonaną w danej chwili migawką obiektu.

Sytuacje, w których taka możliwość się przydaje, są nieliczne i z całą pewnością nie należy nadużywać tego mechanizmu. Podobnie jak w przypadku nierozszerzalnych obiektów tu również powinno się stosować tryb ścisły.

Podsumowanie Obiekty w JavaScripcie najlepiej sobie wyobrazić jako tablice asocjacyjne, a ich właściwości jako pary klucz-wartość. Do właściwości można się odwołać za pomocą notacji z kropką lub z nawiasami kwadratowymi.

Obiekty

71

W dowolnej chwili można dodać nową właściwość (wystarczy przypisać jej wartość) oraz usunąć istniejącą (za pomocą operatora delete). Aby sprawdzić, czy obiekt zawiera określoną właściwość, można użyć operatora in na nazwie właściwości i obiekcie. Jeśli właściwość, którą sprawdzamy, należy do instancji (czyli jest „własną” właściwością), można użyć obecnej w każdym obiekcie metody hasOwnProperty(). Wszystkie właściwości są domyślnie wyliczalne, a więc zostaną uwzględnione przez pętlę for-in oraz metodę Object.keys(). Istnieją dwa rodzaje właściwości: danych oraz funkcji dostępowych. Właściwości danych przechowują dane i można w nich zapisywać oraz z nich odczytywać wartości. Jeśli wartością właściwości jest funkcja, nazywa się ją metodą. Z kolei właściwości funkcji dostępowych nie przechowują wartości, ale dają możliwość zdefiniowania funkcji settera i gettera, które wykonują określone działania. Właściwości obu rodzajów można tworzyć bezpośrednio za pomocą notacji literału obiektu. Właściwości mają zestaw określonych atrybutów, które definiują sposób ich działania. Oba rodzaje właściwości mają atrybuty [[Enumerable]] i [[Configurable]]. Właściwości danych mają jeszcze atrybuty [[Writable]] i [[Value]], a właściwości funkcji dostępowych — [[Get]] i [[Set]]. Atrybuty [[Enumerable]] i [[Configurable]] są domyślnie ustawione na true we wszystkich właściwościach, natomiast we właściwościach danych na true jest ustawiony jeszcze atrybut [[Writable]]. Stan atrybutów można zmieniać za pomocą metod Object.defineProperty() i Object.define Properties(). Do odczytu atrybutów służy metoda Object.getOwnProperty Descriptor(). Istnieją trzy sposoby na zablokowanie właściwości obiektu. Jeśli użyjemy metody Object.preventExtensions(), do obiektu nie będzie można dodać nowych właściwości. Z kolei za pomocą metody Object.seal() można zapieczętować obiekt, czyli zablokować możliwość rozszerzania obiektu i konfigurowania jego właściwości. Ostatnia z metod — Object. freeze() — zamraża obiekt, czyli pieczętuje go i blokuje możliwość modyfikowania wartości jego właściwości. Podczas korzystania z opisanych tu technik należy zachować ostrożność i trzeba koniecznie stosować tryb ścisły, tak by były zgłaszane (w postaci komunikatów o błędach) wszelkie nieprawidłowości.

72

Rozdział 3

4 Konstruktory i prototypy

KONSTRUKTORY I PROTOTYPY TO TE ELEMENTY JĘZYKA JAVASCRIPT, KTÓRYCH ZNAJOMOŚĆ NIE JEST NIEZBĘDNA, ALE Z CAŁĄ PEWNOŚCIĄ ICH DOGŁĘBNE ZROZUMIENIE

pozwala w pełni docenić i wykorzystać ten język. Ponieważ w JavaScripcie nie ma klas, to właśnie na konstruktory i prototypy spada ciężar zaprowadzenia w obiektach porządku uzyskiwanego w innych językach dzięki klasom. Jednak to, że niektóre mechanizmy przypominają działanie klas, nie znaczy, że funkcjonują w ten sam sposób. Z tego rozdziału dowiesz się dokładnie, czym są konstruktory i prototypy w JavaScripcie, a także jaka jest ich rola podczas tworzenia obiektów.

Konstruktory Konstruktor jest funkcją, której używa się z operatorem new do utworzenia obiektu. Do tej pory używaliśmy konstruktorów wbudowanych w język JavaScript, takich jak Object, Array czy Function. Zaletą stosowania

konstruktorów jest to, że tworzone za ich pomocą obiekty mają te same właściwości i metody. Jeśli chcemy utworzyć wiele podobnych obiektów, możemy zdefiniować własne konstruktory, a więc tak naprawdę własne typy referencyjne. Ponieważ konstruktor jest zwykłą funkcją, definiuje się go dokładnie w ten sam sposób. Jedyna różnica, wynikająca z konwencji, a nie mechanizmów języka, polega na zapisywaniu nazw konstruktorów wielką literą, a nie — jak w przypadku zwykłych funkcji — małą. Spójrz na poniższą pustą definicję funkcji Person: function Person() { // celowo pusta }

Ta funkcja jest konstruktorem, ale składniowo nie różni się od zwykłej funkcji. Tym, co ją odróżnia, jest nazwa Person zapisana wielką literą. Po zdefiniowaniu konstruktora możemy utworzyć instancje, czyli na przykład dwa poniższe obiekty: var person1 = new Person(); var person2 = new Person();

Jeśli konstruktor jest bezparametrowy, można pominąć nawiasy: var person1 = new Person; var person2 = new Person;

Mimo że funkcja konstruktora Person nic nie zwraca, zmienne person1 i person2 zawierają instancje typu Person. Odpowiada za to operator new, który automatycznie tworzy obiekty podanego typu i je zwraca. Oznacza to, że do sprawdzenia typu tak utworzonego obiektu można użyć operatora instanceof, co ilustruje poniższy przykład: console.log(person1 instanceof Person); console.log(person2 instanceof Person);

// true // true

Ponieważ oba obiekty — person1 i person2 — zostały utworzone z wykorzystaniem konstruktora Person, w obu przypadkach operator instanceof zwraca true. 74

Rozdział 4

Typ instancji można również sprawdzić za pomocą właściwości constructor. Jest ona składnikiem wszystkich obiektów i zawiera refe-

rencję do funkcji konstruktora, za pomocą której obiekt został utworzony. W przypadku ogólnych obiektów (ang. generic), czyli tworzonych z wykorzystaniem literału obiektu lub konstruktora Object, właściwość constructor ma wartość Object. Dla obiektów własnych typów właściwość constructor wskazuje zdefiniowaną funkcję konstruktora. W poniższym przykładzie właściwość constructor obiektów person1 i person2 ma wartość Person: console.log(person1.constructor === Person); console.log(person2.constructor === Person);

// true // true

W obu przypadkach jest wyświetlana wartość true, ponieważ oba obiekty zostały stworzone za pomocą konstruktora Person. Mimo że każda instancja jest powiązana ze swoim konstruktorem, do sprawdzania typu obiektu lepiej stosować operator instanceof. Wynika to z tego, że właściwość constructor może zostać nadpisana, a więc sygnalizowany przez nią typ może czasem nie odpowiadać rzeczywistości. Oczywiście pusta funkcja konstruktora nie jest zbyt przydatna. Cały sens definiowania konstruktora polega na tym, by tworzone za jego pomocą obiekty miały te same właściwości i metody. Aby to uzyskać, trzeba je dodać wewnątrz funkcji do obiektu this, co ilustruje poniższy przykład: function Person(name) {  this.name = name;  this.sayName = function() { console.log(this.name); }; }

Konstruktor Person przyjmuje jeden parametr — name — i przypisuje jego wartość do właściwości name obiektu this . Konstruktor dodaje również do obiektu metodę sayName() . Obiekt this jest automatycznie tworzony przez operator new w chwili wywoływania konstruktora i jest referencją do tworzonej instancji typu (czyli w tym przypadku this jest instancją typu Person). Z funkcji konstruktora nie musimy zwracać żadnej wartości, ponieważ odpowiada za to operator new.

Konstruktory i prototypy

75

Teraz można użyć konstruktora Person do utworzenia obiektów z zainicjalizowaną właściwością name: var person1 = new Person("Nicholas"); var person2 = new Person("Greg"); console.log(person1.name); console.log(person2.name);

// "Nicholas" // "Greg"

person1.sayName(); person2.sayName();

// wypisuje "Nicholas" // wypisuje "Greg"

Każdy z utworzonych obiektów ma swoją właściwość name, więc metoda sayName() wyświetla w konsoli wartość zapisaną w obiekcie, na którym jest wywoływana. UWAGA Z konstruktora można zwrócić wartość (standardowo za pomocą return). Jeśli zwracana wartość jest obiektem, zostanie użyta zamiast instancji zwracanej domyślnie przez operator new, natomiast jeśli jest

typu prostego, zostanie zignorowana. Dzięki konstruktorom można inicjalizować instancje typów w spójny sposób i, co ważne, wszystkie operacje przygotowujące są przeprowadzane przed oddaniem obiektu do użytku. W konstruktorze można też użyć funkcji Object.defineProperty(), by mieć pełną kontrolę nad sposobem tworzenia pól: function Person(name) { Object.defineProperty(this, "name", { get: function() { return name; }, set: function(newName) { name = newName; }, enumerable: true, configurable: true });

76

Rozdział 4

this.sayName = function() { console.log(this.name); }; }

W tej wersji konstruktora Person pole name jest właściwością funkcji dostępowych, które wykorzystują parametr name do przechowania bieżącej wartości. Jest to możliwe dzięki temu, że parametry są traktowane dokładnie tak samo jak lokalne zmienne. Należy pamiętać, by zawsze wywoływać konstruktor za pomocą operatora new. W przeciwnym przypadku istnieje ryzyko zmiany globalnego obiektu, a nie nowo tworzonego. Zastanówmy się, co się dzieje w poniższym kodzie: var person1 = Person("Nicholas");

// uwaga: brakuje operatora new

console.log(person1 instanceof Person); console.log(typeof person1); console.log(name);

// false // "undefined" // "Nicholas"

W sytuacji gdy konstruktor Person jest wywoływany jak zwykła funkcja (czyli bez operatora new), this jest globalnym obiektem1. Zmienna person1 nie zawiera wartości (czyli jest typu undefined), ponieważ funkcja Person nic jawnie nie zwraca, a nie został zastosowany operator new. W takim przypadku Person jest zwykłą funkcją bez instrukcji return. Przypisanie wartości do this.name tworzy tak naprawdę właściwość name w globalnym obiekcie, i to tam jest zapisywana wartość przekazana do funkcji Person. W rozdziale 6. są opisane możliwe rozwiązania tego problemu, a także wiele wzorców dotyczących obiektów. UWAGA W trybie ścisłym próba wywołania konstruktora Person bez użycia operatora new spowodowałaby wystąpienie błędu. Wynika to z tego, że w tym trybie do this nie jest przypisywany globalny obiekt, więc wartość this to undefined. Próba dodania właściwości do undefined

musi się zakończyć niepowodzeniem.

1

Globalny obiekt zależy od środowiska, w którym jest uruchamiany kod JavaScript; jeżeli jest to przeglądarka, globalnym obiektem jest window — przyp. tłum.

Konstruktory i prototypy

77

Konstruktory pozwalają na tworzenie obiektów o tych samych właściwościach, ale trzeba wiedzieć, że nie da się za ich pomocą wyeliminować redundancji kodu, czyli istnienia wielu egzemplarzy tego samego kodu. W prezentowanym wyżej przykładzie każda instancja ma własną metodę sayName(), chociaż ciało tej funkcji nie ulega zmianie. Oznacza to, że jeżeli utworzymy sto obiektów, będzie istniało sto kopii funkcji, która wykonuje dokładnie taką samą operację, ale na innych danych. Znacznie lepszym rozwiązaniem byłoby doprowadzenie do sytuacji, w której wszystkie instancje danego typu współdzielą jedną metodę, a ona korzysta z właściwości this.name konkretnego obiektu. Na szczęście da się to zrobić — służą do tego prototypy.

Prototypy Prototypy można traktować jak przepisy na obiekt. Prawie wszystkie funkcje (z wyjątkiem niektórych wbudowanych) mają właściwość prototype, używaną podczas tworzenia nowych instancji. Prototyp jest współdzielony przez wszystkie obiekty danego typu, które, co istotne, mają dostęp do właściwości prototypu. Na przykład metoda hasOwnProperty() jest zdefiniowana w prototypie ogólnego typu Object, ale można z niej korzystać w dowolnym obiekcie, tak jakby to była metoda tego obiektu. Ilustruje to poniższy przykład: var book = { title: "The Principles of Object-Oriented JavaScript" }; console.log("title" in book); console.log(book.hasOwnProperty("title")); console.log("hasOwnProperty" in book); console.log(book.hasOwnProperty("hasOwnProperty")); console.log(Object.prototype.hasOwnProperty("hasOwnProperty"));

// true // true // true // false // true

Mimo że w obiekcie book nie ma definicji metody hasOwnProperty(), można ją wywołać poprzez book.hasOwnProperty(). Wynika to stąd, że definicja ta znajduje się w prototypie typu Object (Object.prototype). Operator in zwracając true, bierze pod uwagę właściwości prototypu oraz właściwości instancji.

78

Rozdział 4

IDENTYFIKOWANIE WŁAŚCIWOŚCI PROTOTYPU Aby sprawdzić, czy dana właściwość należy do prototypu, należy się posłużyć następującym kodem: function hasPrototypeProperty(object, name) { return name in object && !object.hasOwnProperty(name); } console.log(hasPrototypeProperty(book, "title")); // false console.log(hasPrototypeProperty(book, "hasOwnProperty")); // true Właściwość należy do prototypu, jeśli znajduje się w obiekcie (operator in), ale metoda hasOwnProperty() zwraca dla niej wartość false.

Właściwość [[Prototype]] Każda instancja przechowuje informację o swoim prototypie. Służy do tego wewnętrzna właściwość [[Prototype]], która przechowuje wskaźnik na wykorzystywany obiekt prototypu. Gdy za pomocą operatora new tworzy się nowy obiekt, właściwość prototype konstruktora jest przypisywana do właściwości [[Prototype]] nowo tworzonego obiektu. Na rysunku 4.1, prezentującym ten mechanizm, widać, że udaje się uniknąć wielokrotnego powtarzania kodu funkcji, ponieważ instancje danego typu zawierają referencje do tego samego prototypu. Wartość właściwości [[Prototype]] można odczytać za pomocą metody Object.getPrototypeOf(), której przekazuje się obiekt. W poniższym przykładzie sprawdzamy prototyp pustego ogólnego obiektu:  var object = {}; var prototype = Object.getPrototypeOf(object); console.log(prototype === Object.prototype);

// true

W każdym ogólnym obiekcie, takim jak ten z przykładu , właściwość [[Prototype]] zawsze wskazuje prototyp Object.prototype.

Konstruktory i prototypy

79

Rysunek 4.1. Właściwości [[Prototype]] obiektów person1 i person2 wskazują ten sam prototyp UWAGA Niektóre silniki JavaScriptu obsługują właściwość __proto__ dostęp-

ną we wszystkich obiektach. Dzięki niej można odczytywać i zapisywać wewnętrzną właściwość [[Property]]. Właściwość jest obsługiwana w przeglądarkach Firefox, Safari, Chrome oraz w środowisku Node.js; najprawdopodobniej znajdzie się też w standardzie ECMAScript 6. Aby sprawdzić, czy jakiś obiekt jest prototypem innego, można użyć metody isPrototypeOf(): var object = {}; console.log(Object.prototype.isPrototypeOf(object));

// true

Ponieważ w tym przykładzie object jest zwykłym obiektem, jego prototypem jest Object.prototype, w związku z czym metoda isPrototypeOf() powinna zwrócić true. Podczas odczytywania właściwości obiektu silnik JavaScriptu najpierw szuka jej wśród właściwości instancji. Jeśli ją znajdzie, zwraca jej wartość. W przeciwnym razie — czyli gdy w danej instancji właściwość

80

Rozdział 4

nie istnieje — jest ona poszukiwana w obiekcie prototypu. I znów, jeśli taka właściwość istnieje, zwracana jest jej wartość, natomiast jeśli jej nie ma, zwracany jest typ undefined. Przypatrzmy się poniższemu przykładowi, w którym tworzymy obiekt bez żadnych właściwości: var object = {};  console.log(object.toString());

// "[object Object]"

object.toString = function() { return "[object Custom]"; };  console.log(object.toString());

// "[object Custom]"

// usuwa właściwość instancji delete object.toString;  console.log(object.toString());

// "[object Object]"

// bez efektu - usuwanie działa tylko na właściwościach instancji delete object.toString; console.log(object.toString()); // "[object Object]"

Metoda toString() należy do prototypu i domyślnie zwraca łańcuch "[object Object]" . Po zdefiniowaniu własnej funkcji i przypisaniu jej do właściwości toString() w instancji object to właśnie ona będzie wywoływana . Właściwość instancji przesłania właściwość prototypu, więc ta druga nie jest już używana. Sytuacja zmieni się po usunięciu właściwości instancji — wtedy znów będzie wywoływana właściwość prototypu . (Trzeba pamiętać, że operator delete działa tylko na właściwościach instancji, więc nie da się za jego pomocą usuwać właściwości prototypu). Działanie powyższego przykładu zostało przedstawione na rysunku 4.2. Ten przykład ilustruje jeszcze jeden istotny problem — z poziomu instancji nie można przypisać wartości do właściwości prototypu. Jak widać na środkowym diagramie z rysunku 4.2, przypisanie wartości do toString tworzy właściwość w instancji, a odpowiadająca jej właściwość prototypu pozostaje niezmieniona.

Konstruktory i prototypy

81

Rysunek 4.2. Obiekt bez właściwości instancji (na górze) posiada tylko właściwości prototypu. Dodanie właściwości toString() do obiektu (w środku) skutkuje przesłonięciem właściwości prototypu. Sytuację tę można odwrócić poprzez usunięcie właściwości instancji (na dole)

Używanie prototypów z konstruktorami Dzięki swoim cechom prototypy doskonale nadają się do definiowania metod wspólnych dla wszystkich instancji danego typu. Ponieważ w zdecydowanej większości wypadków metody wykonują te same operacje we wszystkich instancjach, nie ma powodu, by każdy z obiektów przechowywał własną kopię metody. Znacznie lepszym wyjściem jest umieszczenie metod w prototypie i korzystanie z obiektu this w celu odwołania się do bieżącej instancji. Przeanalizujmy to na poniższym przykładzie konstruktora Person: function Person(name) { this.name = name; }  Person.prototype.sayName = function() { console.log(this.name); };

82

Rozdział 4

var person1 = new Person("Nicholas"); var person2 = new Person("Greg"); console.log(person1.name); console.log(person2.name);

// "Nicholas" // "Greg"

person1.sayName(); person2.sayName();

// wypisuje "Nicholas" // wypisuje "Greg"

W tym przykładzie metoda sayName() została umieszczona w prototypie , a nie w konstruktorze. Mimo to obiekty działają dokładnie tak samo jak wcześniej, czyli tak, jakby metoda należała do instancji. Gdy metoda sayName() jest wywoływana na obiektach person1 i person2, wartości this są przypisywane referencje do nich, więc metoda działa na polach odpowiednich instancji. W prototypie można zapisać dane innych typów, ale należy uważać na typ referencyjny. Ponieważ wartości są współdzielone przez wszystkie instancje, zmiany dokonane w jednym obiekcie mają wpływ na wartość dostępną w innym. Problem ten ilustruje poniższy przykład: function Person(name) { this.name = name; } Person.prototype.sayName = function() { console.log(this.name); };  Person.prototype.favorites = []; var person1 = new Person("Nicholas"); var person2 = new Person("Greg"); person1.favorites.push("pizza"); person2.favorites.push("komosa"); console.log(person1.favorites); // "pizza,komosa" console.log(person2.favorites); // "pizza,komosa"

Właściwość favorites  jest zdefiniowana w prototypie, a to oznacza, że person1.favorites i person2.favorites wskazują tę samą tablicę. Dodanie do niej wartości z poziomu dowolnego obiektu skutkuje zmianą Konstruktory i prototypy

83

tablicy znajdującej się w prototypie. W wielu wypadkach jest to niepożądane, więc trzeba bardzo uważać na to, co się umieszcza w prototypie. Właściwości można dodawać do prototypu po jednej na raz, ale czasami wygodniejszym wyjściem jest zastosowanie literału obiektu i zastąpienie prototypu: function Person(name) { this.name = name; } Person.prototype = {  sayName: function() { console.log(this.name); },  toString: function() { return "[Person " + this.name + "]"; } };

W prototypie definiujemy dwie metody: sayName()  i toString() . Ten wzorzec jest dosyć popularny, ponieważ unika się dzięki niemu potrzeby wielokrotnego wpisywania kodu Person.prototype, a uzyskuje się dużą czytelność kodu. Trzeba jednak wiedzieć o jednym efekcie ubocznym takiego rozwiązania: var person1 = new Person("Nicholas"); console.log(person1 instanceof Person); console.log(person1.constructor === Person);  console.log(person1.constructor === Object);

// true // false // true

Korzystanie z takiej notacji powoduje zmianę właściwości constructor w taki sposób, że wskazuje Object , a nie Person. Dzieje się tak dlatego, że właściwość constructor znajduje się w prototypie, a nie w instancji, więc w chwili tworzenia funkcji jej właściwość prototype jest tworzona z właściwością constructor wskazującą funkcję. W tym wzorcu obiekt prototypu jest w całości nadpisywany, co prowadzi do tego, że właściwość constructor pochodzi z nowo utworzonego ogólnego obiektu przypisanego do Person.prototype. Aby tego uniknąć, trzeba przywrócić właściwości constructor prawidłową wartość:

84

Rozdział 4

function Person(name) { this.name = name; } Person.prototype = {  constructor: Person, sayName: function() { console.log(this.name); }, toString: function() { return "[Person " + this.name + "]"; } }; var person1 = new Person("Nicholas"); var person2 = new Person("Greg"); console.log(person1 instanceof Person); // true console.log(person1.constructor === Person); // true console.log(person1.constructor === Object); // false console.log(person2 instanceof Person); // true console.log(person2.constructor === Person); // true console.log(person2.constructor === Object); // false

W przykładzie wartość właściwości constructor zostaje ustawiona w prototypie . Dobrym zwyczajem jest umieszczanie jej jako pierwszej, by nie zapomnieć o jej dołączeniu. Chyba najciekawszym aspektem powiązania między konstruktorami, prototypami i instancjami jest to, że nie ma bezpośredniego połączenia między instancją i konstruktorem. Istnieją za to połączenia między instancją i prototypem oraz między prototypem i konstruktorem. Relacje te ilustruje rysunek 4.3. Taki sposób powiązania omawianych elementów ma swoje konsekwencje — przerwanie powiązania między instancją i prototypem powoduje zerwanie zależności między instancją i jej konstruktorem.

Konstruktory i prototypy

85

Rysunek 4.3. Instancja jest połączona z konstruktorem za pośrednictwem prototypu

Modyfikowanie prototypów Ponieważ wszystkie instancje określonego typu odwołują się do wspólnego prototypu, można je wszystkie naraz zmodyfikować. Właściwość [[Prototype]] zawiera referencję do prototypu, więc wszystkie wprowadzone w nim zmiany są od razu widoczne w każdej instancji korzystającej z tego prototypu. Można więc dodać do prototypu nową właściwość, a stanie się ona dostępna we wszystkich instancjach, co widać w poniższym przykładzie: function Person(name) { this.name = name; } Person.prototype = { constructor: Person, 

sayName: function() { console.log(this.name); },



toString: function() { return "[Person " + this.name + "]"; }

};

86

Rozdział 4

 var person1 = new Person("Nicholas"); var person2 = new Person("Greg"); console.log("sayHi" in person1); console.log("sayHi" in person2);

// false // false

// dodajemy nową metodę  Person.prototype.sayHi = function() { console.log("Cześć"); };  person1.sayHi(); person2.sayHi();

// wypisuje "Cześć" // wypisuje "Cześć"

Typ Person zawiera na początku tylko dwie metody: sayName()  i toString() . Tworzymy dwie instancje typu Person , a następnie do prototypu dodajemy metodę sayHi() . Od tego momentu na obu obiektach można wywołać tę metodę . Silnik JavaScriptu szuka właściwości za każdym razem, gdy je wywołujemy, więc z punktu widzenia kodu rozbudowa prototypu odnosi natychmiastowy skutek. Możliwość modyfikowania prototypów ma interesujące właściwości w przypadku zapieczętowanych i zamrożonych obiektów. Metody Object. seal() i Object.freeze() operują jedynie na instancji i jej właściwościach. Do zamrożonego obiektu nie można dodać nowych właściwości ani zmienić istniejących, ale można oczywiście zrobić to z właściwościami prototypu i w ten sposób rozszerzać obiekty. Ilustruje to poniższy przykład: var person1 = new Person("Nicholas"); var person2 = new Person("Greg");  Object.freeze(person1);  Person.prototype.sayHi = function() { console.log("Cześć"); }; person1.sayHi(); person2.sayHi();

// wypisuje "Cześć" // wypisuje "Cześć"

W przykładzie są tworzone dwie instancje typu Person. Pierwsza (person1) zostaje zamrożona , a druga pozostaje zwykłym obiektem. Po dodaniu

Konstruktory i prototypy

87

do prototypu metody sayHi()  można ją wywołać na obu instancjach, co w pewien sposób przeczy obostrzeniom stanu zamrożenia. Wynika to stąd, że [[Prototype]] jest właściwością instancji, więc jest zamrożona, ale wskazywany przez nią obiekt już nie. UWAGA W praktyce tego typu rozwiązań nie stosuje się zbyt często. Ważne

jest jednak to, by zrozumieć powiązania między obiektami i ich prototypami, a dziwaczne przykłady, takie jak powyższy, pomagają zilustrować te mechanizmy.

Prototypy wbudowanych obiektów Być może zastanawiasz się, czy prototypy umożliwiają również modyfikowanie wbudowanych obiektów, które są składnikiem silnika JavaScriptu. Odpowiedź brzmi: tak. Wszystkie wbudowane obiekty mają konstruktory, a więc mają również prototypy, które można modyfikować. Aby dodać na przykład nową metodę, z której można korzystać we wszystkich tablicach, należy jedynie zmodyfikować prototyp Array.prototype: Array.prototype.sum = function() { return this.reduce(function(previous, current) { return previous + current; }); }; var numbers = [ 1, 2, 3, 4, 5, 6 ]; var result = numbers.sum(); console.log(result);

// 21

W przykładzie do prototypu Array.prototype dodajemy metodę sum(), która sumuje wszystkie elementy tablicy i zwraca wynik. Ponieważ jest to metoda prototypu, tablica numbers ma do niej dostęp. Wewnątrz funkcji sum() obiekt this odnosi się w tym przypadku do tablicy numbers, która jest instancją typu Array, więc można w niej użyć innych metod tego typu, na przykład reduce(). Być może pamiętasz, że typy proste (łańcuchy znaków, liczby i wartości logiczne) mają swoje typy opakowujące, dzięki którym można je traktować jak obiekty. Jeśli zmodyfikujemy ich prototypy, możemy dodać do nich nowe funkcjonalności, co ilustruje poniższy przykład:

88

Rozdział 4

String.prototype.capitalize = function() { return this.charAt(0).toUpperCase() + this.substring(1); }; var message = "witaj, świecie!"; console.log(message.capitalize());

// "Witaj, świecie!"

Do prototypu typu String dodajemy nową metodę capitalize(). Typ String jest typem opakowującym typ prosty reprezentujący łańcuch znaków. Zmodyfikowanie jego prototypu skutkuje tym, że wszystkie łańcuchy znaków zyskają tę metodę. UWAGA Modyfikowanie wbudowanych typów może i jest ciekawe, po-

nieważ możemy poeksperymentować z rozszerzaniem funkcjonalności języka, ale nie powinniśmy tego robić w środowisku produkcyjnym. Programiści oczekują, że wbudowane typy zachowują się w określony sposób i mają stały zestaw metod. Zmiana tego stanu rzeczy może powodować zamieszanie i problemy.

Podsumowanie Konstruktory są zwykłymi funkcjami wywoływanymi przez operator new. Własne konstruktory definiuje się wtedy, gdy chce się tworzyć wiele obiektów o tych samych właściwościach. Aby zidentyfikować obiekt utworzony za pomocą konstruktora, można użyć operatora instanceof lub właściwości constructor. Każda funkcja ma właściwość prototype, która wskazuje prototyp definiujący właściwości współdzielone przez wszystkie obiekty tworzone za pomocą określonego konstruktora. Współdzielone metody i właściwości są definiowane w prototypie, a pozostałe — w konstruktorze. Właściwość constructor tak naprawdę znajduje się w prototypie, ponieważ jest współdzielona przez wiele instancji. Prototyp obiektu jest przechowywany w wewnętrznej właściwości [[Prototype]]. Jest to referencja, a nie kopia. Jeśli zmodyfikuje się prototyp, zmiana zostanie uwzględniona we wszystkich instancjach, co wynika ze sposobu działania silnika JavaScriptu. Przy próbie dostępu do właściwości w pierwszej kolejności jest przeszukiwany obiekt. Jeśli właściwość o danej nazwie nie została znaleziona, przeszukiwany jest prototyp. Dzięki takiemu

Konstruktory i prototypy

89

mechanizmowi prototyp może być modyfikowany, a wprowadzone w nim zmiany są od razu widoczne w obiektach zawierających referencję do tego prototypu. Wbudowane obiekty również posiadają prototypy, które można modyfikować. Nie powinno się jednak tego robić podczas tworzenia konkretnych aplikacji. Możliwość ta jest za to przydatna podczas eksperymentów i testowania nowych funkcjonalności.

90

Rozdział 4

5 Dziedziczenie

NAUKA

TWORZENIA OBIEKTÓW TO PIERWSZY KROK

W STRONĘ ZROZUMIENIA PROGRAMOWANIA OBIEKTO WEGO. DRUGI KROK TO ZGŁĘBIENIE TAJNIKÓW DZIEdziczenia. W klasycznych językach obiektowych klasy dziedziczą właściwości po innych klasach. W JavaScripcie dziedziczenie odbywa się bezpośrednio między obiektami, a więc bez udziału klas. Mechanizm stojący za tego typu dziedziczeniem już znasz — to prototypy.

Łańcuchy prototypów i Object.prototype W JavaScripcie wbudowany mechanizm dziedziczenia nosi nazwę łańcuchowania prototypów (ang. prototype chaining) lub dziedziczenia prototypowego (ang. prototypal inheritance). Z poprzedniego rozdziału już wiesz, że właściwości prototypów są automatycznie dostępne w instancjach, co jest pewną formą dziedziczenia — obiekty dziedziczą po prototypie. Ponieważ prototyp również jest obiektem, ma własny prototyp, po którym

dziedziczy. To jest właśnie łańcuch prototypów — obiekt dziedziczy po swoim prototypie, ten prototyp dziedziczy po innym itd. Wszystkie obiekty, włączając w to zdefiniowane samodzielnie, automatycznie dziedziczą po Object, chyba że to zmienimy (więcej na ten temat znajdziesz w dalszej części rozdziału). A dokładniej, wszystkie obiekty dziedziczą po Object.prototype. Każdy obiekt zdefiniowany za pomocą literału obiektu ma właściwość [[Prototype]] ustawioną na Object.prototype, a to oznacza, że dziedziczy po nim właściwości. Ilustruje to poniższy przykład: var book = { title: "JavaScript. Zasady programowania obiektowego" }; var prototype = Object.getPrototypeOf(book); console.log(prototype === Object.prototype);

// true

Obiekt book ma prototyp Object.prototype. Nie musieliśmy wpisywać żadnego dodatkowego kodu, ponieważ jest to domyślne zachowanie języka. Dzięki temu powiązaniu obiekt book ma wszystkie metody prototypu Object.prototype.

Metody dziedziczone po Object.prototype W poprzednich przykładach niejednokrotnie korzystaliśmy z metod pochodzących z prototypu Object.prototype. Są one dziedziczone przez wszystkie inne obiekty:  hasOwnProperty() — sprawdza, czy w obiekcie istnieje właściwość instancji o podanej nazwie,  propertyIsEnumerable() — sprawdza, czy dana właściwość instancji jest wyliczalna,  isPrototypeOf() — określa, czy dany obiekt jest prototypem innego, 

valueOf() — zwraca wartość obiektu,



toString() — zwraca tekstową reprezentację obiektu.

Tych pięć metod pojawia się we wszystkich obiektach właśnie dzięki dziedziczeniu. Dwie ostatnie są szczególnie ważne, ponieważ pomagają w spójny sposób korzystać z obiektów w JavaScripcie. Czasem może zajść potrzeba ich przedefiniowania.

92

Rozdział 5

Metoda valueOf() Metoda valueOf() jest wywoływana za każdym razem, gdy na obiekcie jest stosowany operator. Domyślnie zwraca ona instancję obiektu. Typy opakowujące i typy proste przesłaniają tę metodę, tak by zwracała łańcuch znaków (w typie String), wartość logiczną (w typie Boolean) lub liczbę (w typie Number). Podobnie jest w przypadku typu Date, gdzie metoda valueOf() zwraca czas wyrażony w milisekundach (tak jak to robi metoda Date.prototype.getTime()). Dzięki temu można w prosty sposób porównywać czas: var now = new Date(); var earlier = new Date(2010, 1, 1);  console.log(now > earlier);

// true

Zmienna now przechowuje obiekt reprezentujący bieżący czas, a earlier — zdefiniowany czas z przeszłości. Kiedy zostanie zastosowany operator porównania (>) , na obu porównywanych obiektach jest wywoływana metoda valueOf(). Dzięki takiemu mechanizmowi daty można, na przykład, odejmować i uzyskać w ten sposób różnicę w czasie. Jeśli na zdefiniowanych przez nas obiektach chcemy stosować operatory, możemy zdefiniować własną wersję metody valueOf(). Trzeba jednak pamiętać, że nie zmieniamy w ten sposób działania operatorów, ale wartości, które są przez te operatory traktowane w standardowy sposób.

Metoda toString() Metoda toString() jest wywoływana w sytuacji, gdy metoda valueOf() zwraca referencję, a nie wartość typu prostego. Może być również wywołana na typie prostym, o ile silnik JavaScriptu oczekuje łańcucha znaków. Na przykład jeśli jednym z operandów operatora dodawania (+) jest łańcuch znaków, drugi operand zostaje automatycznie skonwertowany na łańcuch. W przypadku wartości typu prostego jest zwracana jej tekstowa reprezentacja (np. wartość true zostanie zamieniona na "true"), a dla referencji wywoływana jest metoda valueOf(). Jeśli ona również zwróci referencję, wywoływana jest na niej metoda toString(). Oto przykład: var book = { title: "JavaScript. Zasady programowania obiektowego" };

Dziedziczenie

93

var message = "book = " + book; console.log(message);

// "book = [object Object]"

Konstruujemy tu nowy łańcuch znaków poprzez połączenie łańcucha "Książka = " i zmiennej book. Ponieważ book jest obiektem, wywoływana jest jego metoda toString(). Jak wspomnieliśmy, metoda ta pochodzi z prototypu Object.prototype i w większości silników JavaScriptu domyślnie zwraca wartość "[object Object]". Jeśli taki wynik jest do zaakceptowania, nie ma potrzeby zmiany metody toString(). Są jednak sytuacje,

w których oczekujemy od niej zwrócenia bardziej szczegółowych informacji, jak choćby tytułu książki: var book = { title: "JavaScript. Zasady programowania obiektowego", toString: function() { return "[Książka " + this.title + "]" } }; var message = "book = " + book; // "book = [Książka JavaScript. Zasady programowania obiektowego]"  console.log(message);

W obiekcie book definiujemy własną metodę toString(), która zwraca więcej informacji  niż jej oryginalna wersja. Zwykle nie ma potrzeby tworzenia własnej metody toString(), ale dobrze wiedzieć, że taka możliwość istnieje.

Modyfikowanie prototypu Object.prototype Wszystkie obiekty dziedziczą domyślnie po Object.prototype, więc zmiany w obrębie tego prototypu są w nich wszystkich widoczne. To bardzo niebezpieczna sytuacja. W rozdziale 4. była mowa o niebezpieczeństwach płynących z modyfikowania wbudowanych prototypów. Odnosi się to szczególnie do Object.prototype. Zobaczmy, co dzieje się w poniższym kodzie: Object.prototype.add = function(value) { return this + value; };

94

Rozdział 5

var book = { title: "JavaScript. Zasady programowania obiektowego" }; console.log(book.add(5)); console.log("tytuł".add("koniec")); // w przeglądarce internetowej console.log(document.add(true)); HTMLDocument]true" console.log(window.add(5));

// "[object Object]5" // "tytułkoniec" // "[object // "[object Window]true"

Dodanie do prototypu Object.prototype metody add() skutkuje tym, że można ją wywołać na wszystkich obiektach, niezależnie od tego, czy to ma sens, czy nie. Ten problem dotyczył nie tylko programistów, ale też komitetu standaryzującego język — nowe metody trzeba było dodawać w różnych lokalizacjach, ponieważ umieszczenie ich w Object.prototype mogło wywołać trudne do przewidzenia konsekwencje. Innym aspektem tego problemu jest dodawanie do prototypu Object.prototype wyliczalnych właściwości. W poprzednim przykładzie metoda Object.prototype.add() jest wyliczalną właściwością, więc zostanie uwzględniona w pętli for-in: var empty = {}; for (var property in empty) { console.log(property); }

Tworzony tu pusty obiekt empty ma właściwość add, ponieważ znajduje się ona w prototypie i jest wyliczalna. Jeśli weźmiemy pod uwagę to, jak często w JavaScripcie korzysta się z instrukcji for-in, okaże się, że modyfikowanie prototypu Object.prototype wymusi stosowanie rozbudowanego kodu odrzucającego niechciane właściwości. Z tego powodu Douglas Crockford sugeruje1, by w pętlach for-in zawsze stosować metodę hasOwnProperty():

1

Douglas Crockford: Code Conventions for the JavaScript Programming Language (http://javascript.crockford.com/code.html).

Dziedziczenie

95

var empty = {}; for (var property in empty) { if (empty.hasOwnProperty(property)) { console.log(property); } }

Takie podejście rozwiązuje problem z niechcianymi właściwościami prototypu, ogranicza jednak możliwość zastosowania pętli for-in do właściwości instancji, co nie zawsze jest pożądane. Najlepszym wyjściem z sytuacji jest więc niemodyfikowanie prototypu Object.prototype.

Dziedziczenie obiektów Najmniej skomplikowane dziedziczenie zachodzi między obiektami. Wystarczy określić obiekt, który ma zostać prototypem nowego obiektu. W przypadku literałów obiektów właściwość [[Prototype]] jest domyślnie ustawiana na Object.prototype, ale można to zmienić za pomocą metody Object.create(). Metoda Object.create() przyjmuje dwa argumenty. Pierwszym jest obiekt, który ma się stać prototypem nowego obiektu. Drugi, opcjonalny argument to obiekt zawierający deskryptory właściwości, dokładnie taki sam jak w metodzie Object.defineProperties() (patrz rozdział 3.). Spójrzmy na poniższy przykład: var book = { title: "JavaScript. Zasady programowania obiektowego" }; // jest tym samym co: var book = Object.create(Object.prototype, { title: { configurable: true, enumerable: true, value: "JavaScript. Zasady programowania obiektowego", writable: true } });

96

Rozdział 5

Oba przedstawione fragmenty kodu robią dokładnie to samo. W pierwszym został zastosowany literał obiektu z pojedynczą właściwością title. Obiekt ten automatycznie dziedziczy po prototypie Object.prototype, a właściwość jest ustawiana domyślnie tak, że można ją konfigurować, wyliczać i zapisywać. W drugim fragmencie wykonywane są te same operacje, ale jest to przeprowadzane jawnie za pomocą metody Object.create(). Obiekty utworzone za pomocą tych dwóch konstrukcji zachowują się tak samo. Drugiego rozwiązania nie stosuje się jednak w wypadku dziedziczenia po Object.prototype, ponieważ to samo można uzyskać o wiele łatwiej. Znacznie ciekawsze jest zastosowanie go do utworzenia obiektu dziedziczącego po innym: var person1 = { name: "Nicholas", sayName: function() { console.log(this.name); } }; var person2 = Object.create(person1, { name: { configurable: true, enumerable: true, value: "Greg", writable: true } }); person1.sayName(); person2.sayName();

// wypisuje "Nicholas" // wypisuje "Greg"

console.log(person1.hasOwnProperty("sayName")); console.log(person1.isPrototypeOf(person2)); console.log(person2.hasOwnProperty("sayName"));

// true // true // false

Tworzymy tu obiekt person1 z właściwością name oraz metodą sayName(). Obiekt person2 dziedziczy po person1 zarówno właściwość name, jak i metodę sayName(). Jednak w obiekcie person2, tworzonym za pomocą metody Object.create(), również definiujemy właściwość name. W ten sposób przesłaniamy właściwość prototypu o tej samej nazwie. W związku z tym wywołanie person1.sayName() spowoduje wyświetlenie tekstu

Dziedziczenie

97

"Nicholas", a person2.sayName() — "Greg". Trzeba przy tym pamiętać, że metoda sayName() znajduje się tylko w obiekcie person1 i jest dziedziczona przez person2. Łańcuch dziedziczenia dla obiektu person2 jest w tym wypadku dłuższy niż dla person1, ponieważ person2 dziedziczy po obiekcie person1, a person1 — po Object.prototype, co widać na rysunku 5.1.

Rysunek 5.1. Łańcuch prototypów dla person2 składa się z obiektu person1 i prototypu Object.prototype Kiedy odwołujemy się do właściwości obiektu, silnik JavaScriptu rozpoczyna proces poszukiwania. Jeśli właściwość znajduje się w instancji (czyli jest „własną” właściwością), to ona zostaje użyta. W przeciwnym razie przeszukiwany jest obiekt wskazywany przez właściwość [[Prototype]]. Jeśli i tu jej nie ma, przeszukiwany jest prototyp prototypu i tak dalej, aż zostanie osiągnięty koniec łańcucha, którym zwykle jest Object.prototype z właściwością [[Prototype]] ustawioną na null. Za pomocą metody Object.create() można również tworzyć obiekty z właściwością [[Prototype]] równą null: var nakedObject = Object.create(null); console.log("toString" in nakedObject); console.log("valueOf" in nakedObject);

98

Rozdział 5

// false // false

W tym przykładzie nakedObject jest obiektem bez łańcucha prototypów, więc nie posiada metod takich jak toString() czy valueOf(). Jest zatem zupełnie „pusty” — nie ma żadnych właściwości. Takie rozwiązanie może się przydać, jeśli chcemy za wszelką cenę uniknąć kolizji nazw z dziedziczonymi prototypami. Sytuacji, w których może się to okazać przydane, nie jest za wiele i — co istotne — takich obiektów nie można używać tak samo jak tych, których prototypem jest Object.prototype. W sytuacji gdy użyjemy operatora na obiekcie nakedObject, otrzymamy komunikat o błędzie „Cannot convert object to primitive value” (obiektu nie można przekonwertować na wartość typu prostego). Ciekawe jest jednak to, że w JavaScripcie możliwe jest utworzenie obiektu nieposiadającego prototypu.

Dziedziczenie konstruktorów Podstawą mechanizmu dziedziczenia konstruktorów jest dziedziczenie obiektów. Jak wspomnieliśmy w rozdziale 4., zdecydowana większość funkcji ma właściwość prototype, która może być modyfikowana. Właściwość ta automatycznie jest ustawiana na ogólny obiekt dziedziczący po Object. prototype i posiada właściwość constructor. Silnik JavaScriptu wykonuje więc kilka operacji za nas: // piszemy to: function YourConstructor() { // initialization } // a silnik JavaScriptu wykonuje za nas te operacje: YourConstructor.prototype = Object.create(Object.prototype, { constructor: { configurable: true, enumerable: true, value: YourConstructor writable: true } });

Jak widać, właściwości prototype konstruktora jest przypisywana referencja do obiektu dziedziczącego po Object.prototype, a zatem wszystkie instancje YourConstructor będą dziedziczyć po Object.prototype. Tak Dziedziczenie

99

więc YourContructor jest podtypem typu Object, a Object jest supertypem typu YourConstructor. Ponieważ właściwość prototype pozwala na zapisywanie, można w prosty sposób zmodyfikować łańcuch prototypów, co zostało zilustrowane w poniższym przykładzie:  function Rectangle(length, width) { this.length = length; this.width = width; } Rectangle.prototype.getArea = function() { return this.length * this.width; }; Rectangle.prototype.toString = function() { return "[Rectangle " + this.length + "x" + this.width + "]"; }; // dziedziczy po Rectangle  function Square(size) { this.length = size; this.width = size; } Square.prototype = new Rectangle(); Square.prototype.constructor = Square; Square.prototype.toString = function() { return "[Square " + this.length + "x" + this.width + "]"; }; var rect = new Rectangle(5, 10); var square = new Square(6); console.log(rect.getArea()); console.log(square.getArea());

// 50 // 36

console.log(rect.toString()); console.log(square.toString());

// "[Rectangle 5x10]" // "[Square 6x6]"

console.log(rect instanceof Rectangle); console.log(rect instanceof Object);

100

Rozdział 5

// true // true

console.log(square instanceof Square); console.log(square instanceof Rectangle); console.log(square instanceof Object);

// true // true // true

Mamy tu dwa konstruktory: Rectangle  i Square . W Square nadpisaliśmy właściwość prototype instancją typu Rectangle. Do konstruktora Rectangle nie przekazaliśmy żadnych argumentów, ponieważ i tak nie zostałyby użyte ze względu na to, że w Square jest stosowany tylko jeden wymiar. Jeśli łańcuch wywołań zmienia się w taki sposób, trzeba sprawdzić, czy konstruktor nie rzuci błędu w sytuacji, gdy nie zostaną mu przekazane żadne argumenty (a wiele konstruktorów zawiera kod inicjalizujący, który ich wymaga), a także czy nie modyfikuje globalnego stanu (co może mieć miejsce, gdy konstruktor sprawdza liczbę utworzonych instancji). Właściwość constructor jest przywracana w prototypie Square. prototype po nadpisaniu oryginalnej wartości. Następnie są tworzone instancje rect typu Rectangle oraz square typu Square. Oba obiekty posiadają metodę getArea(), ponieważ dziedziczą po Rectangle.prototype. Jeśli zastosujemy operator instanceof, zmienna square okaże się instancją typu Square, a rect — typu Rectangle. Rysunek 5.2 ilustruje łańcuchy prototypów z tego przykładu. Prototyp Square.prototype nie musi być nadpisywany przez obiekt Rectangle, ponieważ konstruktor Rectangle nie wykonuje żadnych operacji, które byłyby niezbędne w typie Square. Jedynym istotnym elementem jest powiązanie prototypu Square.prototype z Rectangle.prototype, aby doszło do dziedziczenia. W związku z tym można uprościć nieco kod i zastosować w tym celu metodę Object.create(): // dziedziczy po Rectangle function Square(size) { this.length = size; this.width = size; } Square.prototype = Object.create(Rectangle.prototype, { constructor: { configurable: true, enumerable: true, value: Square, writable: true } });

Dziedziczenie

101

Rysunek 5.2. Łańcuchy prototypów obiektów rect i square pokazują, że oba dziedziczą po Rectangle.prototype i Object.prototype, ale tylko square dziedziczy po Square.prototype Square.prototype.toString = function() { return "[Square " + this.length + "x" + this.width + "]"; };

W tej wersji kodu prototyp Square.prototype jest nadpisywany nowym obiektem dziedziczącym po Rectangle.prototype, więc konstruktor Rectangle nie musi być w ogóle wywoływany. Nie trzeba się więc martwić o błędy, które mogłyby się pojawić w sytuacji wywołania konstruktora bez argumentów. Poza tą różnicą ten kod działa dokładnie tak samo jak poprzedni. Łańcuch prototypów pozostaje bez zmian, więc wszystkie instancje typu Square dziedziczą po Rectangle.prototype, a konstruktor jest przywracany dokładnie w ten sam sposób. UWAGA Zawsze trzeba sprawdzić, czy prototyp jest nadpisywany po do-

daniu do niego właściwości. W przeciwnym razie utracimy dodane metody.

102

Rozdział 5

Zawłaszczanie konstruktora Ponieważ w JavaScripcie dziedziczenie jest realizowane poprzez łańcuch prototypów, nie jest wymagane wywoływanie konstruktora supertypu. Jeśli jednak z jakiegoś powodu z konstruktora podtypu chcielibyśmy wywołać konstruktor supertypu, musimy zwrócić uwagę na sposób, w jaki działają funkcje w JavaScripcie. Omówione w rozdziale 2. metody call() i apply(), dzięki którym można wywoływać funkcje z różnymi wartościami this, są podstawą techniki nazywanej zawłaszczaniem konstruktora (ang. constructor stealing). Z konstruktora podtypu za pomocą jednej z metod — call() lub apply() — wywołujemy konstruktor supertypu, przekazując do niego nowo tworzony obiekt. W ten sposób „zawłaszczamy” ten konstruktor i wykorzystujemy do własnych celów: function Rectangle(length, width) { this.length = length; this.width = width; } Rectangle.prototype.getArea = function() { return this.length * this.width; }; Rectangle.prototype.toString = function() { return "[Rectangle " + this.length + "x" + this.width + "]"; }; // dziedziczy po Rectangle  function Square(size) { Rectangle.call(this, size, size); // opcjonalnie: dodajemy nowe właściwości lub przesłaniamy istniejące } Square.prototype = Object.create(Rectangle.prototype, { constructor: { configurable: true, enumerable: true, value: Square, writable: true } });

Dziedziczenie

103

Square.prototype.toString = function() { return "[Square " + this.length + "x" + this.width + "]"; }; var square = new Square(6); console.log(square.length); console.log(square.width); console.log(square.getArea());

// 6 // 6 // 36

Konstruktor Square  wywołuje konstruktor Rectangle i przekazuje mu this oraz dwukrotnie parametr size (jako length i width), dzięki czemu oba wymiary prostokąta (wysokość i szerokość) przyjmą tę samą wartość równą size. Takie rozwiązanie pozwala uniknąć potrzeby przedefiniowania właściwości konstruktora supertypu. Po jego wywołaniu można oczywiście dodać nowe właściwości lub przesłonić istniejące. Taki dwuetapowy proces przydaje się w sytuacjach, gdy do dziedziczenia dochodzi między dwoma samodzielnie zdefiniowanymi typami. Zawsze trzeba zmodyfikować prototyp konstruktora, a czasem konieczne jest wywołanie konstruktora supertypu z poziomu konstruktora podtypu. Na potrzeby dziedziczenia metod należy zmodyfikować prototyp, a ze względu na właściwości można zastosować technikę zawłaszczania konstruktora. Taki proces jest czasem określany jako pseudoklasyczne dziedziczenie (ang. pseudoclassical inheritance), ponieważ w dużym stopniu przypomina rozwiązania stosowane w językach bazujących na klasach.

Uzyskiwanie dostępu do metod supertypu Typ Square z poprzedniego przykładu ma własną metodę toString(), która przesłania toString() z prototypu. Bardzo często w podtypach tworzy się metody oferujące nową funkcjonalność, które przesłaniają metody z supertypu. A co jeśli mimo wszystko chcemy uzyskać dostęp do przesłoniętej metody supertypu? W innych językach można by na przykład zastosować konstrukcję super.toString(), ale w JavaScripcie nie jest to możliwe. Można za to odwołać się bezpośrednio do metody prototypu supertypu, wykorzystując w tym celu metodę call() lub apply():

104

Rozdział 5

function Rectangle(length, width) { this.length = length; this.width = width; } Rectangle.prototype.getArea = function() { return this.length * this.width; }; Rectangle.prototype.toString = function() { return "[Rectangle " + this.length + "x" + this.height + "]"; }; // dziedziczy po Rectangle function Square(size) { Rectangle.call(this, size, size); } Square.prototype = Object.create(Rectangle.prototype, { constructor: { configurable: true, enumerable: true, value: Square, writable: true } }); // wywołanie metody supertypu  Square.prototype.toString = function() { var text = Rectangle.prototype.toString.call(this); return text.replace("Rectangle", "Square"); };

W tej wersji kodu metoda Square.prototype.toString() wywołuje za pomocą call() metodę Rectangle.prototype.toString(). Przed zwróceniem wartości zamienia tylko łańcuch "Rectangle" na "Square". Takie podejście może się wydać nieodpowiednie w tak prostym przykładzie, ale to jedyny sposób na wywołanie metody supertypu.

Dziedziczenie

105

Podsumowanie W JavaScripcie dziedziczenie jest realizowane przez łańcuch prototypów, na który składają się obiekty powiązane przez właściwość [[Prototype]]. Wszystkie ogólne obiekty automatycznie dziedziczą po Object.prototype. Aby utworzyć obiekt dziedziczący po innym prototypie, należy użyć metody Object.create() i określić wartość właściwości [[Prototype]] nowo tworzonego obiektu. Dziedziczenie między samodzielnie zdefiniowanymi typami jest realizowane przez utworzenie łańcucha prototypów w konstruktorze. Ustawienie właściwości prototype konstruktora tworzy relację dziedziczenia między instancją tego typu i danym prototypem. Wszystkie instancje współdzielą ten sam prototyp, więc dziedziczą po tym samym obiekcie. Ta technika sprawdza się w przypadku dziedziczenia metod, ale nie można jej stosować do dziedziczenia właściwości instancji. Aby prawidłowo przeprowadzić dziedziczenie właściwości instancji, można użyć techniki zawłaszczania konstruktora. Technika ta polega na wywołaniu funkcji konstruktora za pomocą metody call() lub apply(), tak by w każdym wypadku inicjalizacja przebiegała na obiekcie podtypu. Połączenie techniki zawłaszczania konstruktora i łańcucha prototypów jest w JavaScripcie najczęściej stosowanym rozwiązaniem dziedziczenia między własnymi typami. Nazywa się je pseudoklasycznym dziedziczeniem, ponieważ przypomina rozwiązania stosowane w językach bazujących na klasach. Dostęp do metod supertypu można uzyskać poprzez bezpośrednie odwołanie do prototypu supertypu, co jest możliwe dzięki zastosowaniu metody call() lub apply().

106

Rozdział 5

6 Wzorce tworzenia obiektów

ISTNIEJE WIELE WZORCÓW TWORZENIA OBIEKTÓW, A TEN SAM EFEKT MOŻNA UZYSKAĆ RÓŻNYMI SPOSOBAMI. MOŻNA DEFINIOWAĆ WŁASNE TYPY ALBO SKORZYSTAĆ z obiektów ogólnego typu. Aby współdzielić zachowania między obiektami, można skorzystać z dziedziczenia albo zastosować inne techniki, takie jak na przykład domieszki. Można też użyć zaawansowanych mechanizmów języka JavaScript umożliwiających zabezpieczenie obiektów przed modyfikacją. Znajomość wzorców omówionych w tym rozdziale z pewnością okaże się pomocna w wyborze najlepszego i najwydajniejszego rozwiązania konkretnych problemów.

Prywatne i uprzywilejowane składniki obiektów Wszystkie właściwości obiektów w JavaScripcie są publiczne i nie ma wbudowanego mechanizmu, który umożliwiałby zabezpieczenie właściwości przed dostępem z zewnątrz obiektu. Zdarzają się jednak sytuacje, w których pożądane jest ukrycie pewnych danych. Jeśli na przykład obiekt posiada pole przechowujące jego wewnętrzny stan, zmodyfikowanie go w niekontrolowany sposób spoza obiektu mogłoby wywołać chaos w mechanizmie zarządzania stanem. Najprostszym, ale też najmniej skutecznym rozwiązaniem jest zastosowanie ogólnie przyjętej konwencji nazewniczej, w której nazwy właściwości traktowanych jako prywatne rozpoczyna się znakiem podkreślenia (np. this._name). Istnieją też inne, bardziej pewne rozwiązania, w których wybrane dane są faktycznie ukrywane i nie ma możliwości ich modyfikacji spoza obiektu.

Wzorzec modułu Za pomocą wzorca modułu (ang. module pattern) można tworzyć obiekty będące singletonami przechowującymi prywatne dane. Podstawą w tym wzorcu jest zastosowanie wyrażenia funkcji natychmiastowej (ang. immediately invoked function expression — IIFE) zwracającej obiekt. Wyrażenie to definiuje funkcję i od razu ją wywołuje. Można w niej umieścić lokalne zmienne, które nie są dostępne spoza tej funkcji, ale można je odczytywać i zapisywać w metodach zwracanego obiektu, ponieważ jest on zdefiniowany wewnątrz funkcji (wszystkie obiekty zdefiniowane w funkcji natychmiastowej mają wgląd do jej lokalnych zmiennych). Metody, które w ten sposób uzyskują dostęp do prywatnych danych, nazywa się metodami uprzywilejowanymi (ang. privilaged). Uproszczony schemat tego wzorca wygląda następująco: var yourObject = (function() { // prywatne zmienne return { // publiczne metody i właściwości };  }());

108

Rozdział 6

Zdefiniowana anonimowa funkcja jest natychmiast wykonywana (zwróć uwagę na dodatkową parę nawiasów na końcu funkcji ; w ten sposób wywołuje się anonimową funkcję). Oznacza to, że funkcja istnieje tylko przez chwilę, ponieważ zaraz po wykonaniu jest niszczona. Wyrażenia funkcji natychmiastowej są bardzo popularnym wzorcem w JavaScripcie, często rozwijanym do wzorca modułu. Wzorzec modułu pozwala na użycie zwykłych zmiennych jako właściwości obiektu, które nie są widoczne publicznie. Realizuje się to przez zastosowanie domknięcia (ang. closure), czyli funkcji mającej dostęp do danych spoza jej zasięgu. Na przykład jeśli w funkcji odwołujemy się do globalnego obiektu (takiego jak window w przeglądarce internetowej), uzyskujemy dostęp do zmiennej spoza własnego zasięgu. W przypadku wzorca modułu zmienne są zadeklarowane w funkcji natychmiastowej, podobnie jak sama funkcja, która je odczytuje lub zapisuje. Oto przykład: var person = (function() {  var age = 25; return { name: "Nicholas",  getAge: function() { return age; }, 

growOlder: function() { age++; } };

}()); console.log(person.name); console.log(person.getAge());

// „Nicholas” // 25

person.age = 100; console.log(person.getAge());

// 25

person.growOlder(); console.log(person.getAge());

// 26

W tym przykładzie obiekt person jest tworzony za pomocą wzorca modułu. Zmienna age  jest prywatną właściwością tego obiektu. Nie można się do niej bezpośrednio odwołać spoza obiektu, ale można to Wzorce tworzenia obiektów

109

zrobić za pomocą metod. W obiekcie znajdują się dwie uprzywilejowane metody: getAge() , która odczytuje i zwraca wartość zmiennej age, oraz growOlder() , która inkrementuje tę zmienną. Obie metody mogą bezpośrednio korzystać ze zmiennej age, ponieważ jest ona zdefiniowana w funkcji zewnętrznej względem zawierającego je obiektu. Istnieje jeszcze inna wersja wzorca modułu, tzw. wzorzec modułu z ujawnianiem (ang. revealing module pattern), w którym wszystkie zmienne i metody są umieszczone w funkcji natychmiastowej, a w zwracanym obiekcie znajdują się odwołania do nich. Aby zastosować ten wzorzec, kod z poprzedniego przykładu trzeba przekształcić do poniższej postaci: var person = (function() { var age = 25; function getAge() { return age; } function growOlder() { age++; } return { name: "Nicholas",  getAge: getAge, growOlder: growOlder }; }());

Zmienna age oraz funkcje getAge() i growOlder() są zdefiniowane jako lokalne właściwości funkcji natychmiastowej. Następnie referencje do funkcji getAge() i growOlder() są umieszczane w zwracanym obiekcie , co z zewnątrz można uznać za ich „ujawnienie”. W ten sposób uzyskuje się dokładnie ten sam efekt, co w przypadku klasycznego wzorca modułu. Niektórzy preferują jednak wzorzec modułu z ujawnianiem, ponieważ deklaracje wszystkich zmiennych i funkcji znajdują się w jednym miejscu.

Prywatne składniki w konstruktorach Wzorzec modułu doskonale się sprawdza w sytuacjach, gdy trzeba zdefiniować pojedyncze obiekty posiadające prywatne właściwości. Co jednak

110

Rozdział 6

zrobić, gdy potrzebujemy własnego typu z prywatnymi właściwościami? Można zastosować podobny wzorzec, ale trzeba go umieścić w konstruktorze. Dzięki temu będzie możliwe tworzenie instancji posiadających prywatne dane: function Person(name) { // ta zmienna jest dostępna tylko w konstruktorze Person var age = 25; this.name = name;  this.getAge = function() { return age; }; 

this.growOlder = function() { age++; };

} var person = new Person("Nicholas"); console.log(person.name); console.log(person.getAge());

// „Nicholas” // 25

person.age = 100; console.log(person.getAge());

// 25

person.growOlder(); console.log(person.getAge());

// 26

Konstruktor Person ma lokalną zmienną age, która jest używana przez metody getAge()  i growOlder() . Instancje tworzone za pomocą konstruktora Person dostają własną zmienną age oraz metody getAge() i growOlder(). W wielu aspektach to rozwiązanie przypomina wzorzec modułu, w którym konstruktor tworzy lokalny zasięg i zwraca obiekt this. Z lektury rozdziału 4. zapewne pamiętasz, że umieszczanie metod w instancji jest mniej efektywne niż wykorzystanie w tym celu prototypu, ale — jeśli chcemy mieć prywatne dane — jest to jedyny sposób. Jeśli prywatne dane mają być współdzielone przez wszystkie instancje (czyli tak jakby znajdowały się w prototypie), można zastosować mieszane rozwiązanie, które łączy wzorzec modułu i konstruktor:

Wzorce tworzenia obiektów

111

var Person = (function() { // wszystkie instancje współdzielą tę samą zmienną age  var age = 25; 

function InnerPerson(name) { this.name = name; } InnerPerson.prototype.getAge = function() { return age; }; InnerPerson.prototype.growOlder = function() { age++; }; return InnerPerson;

}()); var person1 = new Person("Nicholas"); var person2 = new Person("Greg"); console.log(person1.name); console.log(person1.getAge());

// „Nicholas” // 25

console.log(person2.name); console.log(person2.getAge());

// „Greg” // 25

person1.growOlder(); console.log(person1.getAge()); console.log(person2.getAge());

// 26 // 26

Konstruktor InnerPerson  jest zdefiniowany w funkcji natychmiastowej. Zmienna age  jest zdefiniowana poza konstruktorem, ale jest używana przez dwie metody. Z funkcji natychmiastowej jest zwracany konstruktor InnerPerson, ale w globalnym zasięgu staje się on konstruktorem Person. Wszystkie instancje typu Person współdzielą zmienną age, więc zmiana jej wartości w jednym obiekcie jest widoczna w pozostałych.

112

Rozdział 6

Domieszki Mimo że w JavaScripcie zazwyczaj używa się dziedziczenia pseudoklasycznego lub prototypowego, można też użyć pseudodziedziczenia opartego na domieszkach. Domieszki (ang. mixins) to konstrukcje, w których obiekty uzyskują dostęp do właściwości innego obiektu bez modyfikowania łańcucha prototypów. Pierwszy obiekt — odbiorca (ang. receiver) — otrzymuje właściwości drugiego — dostawcy (ang. supplier) — poprzez bezpośrednie ich skopiowanie. Domieszki tworzy się za pomocą funkcji: function mixin(receiver, supplier) { for (var property in supplier) { if (supplier.hasOwnProperty(property)) { receiver[property] = supplier[property] } } return receiver; }

Funkcja mixin() przyjmuje dwa argumenty: receiver (odbiorca) i supplier (dostawca). Jej zadaniem jest skopiowanie wszystkich wyliczalnych właściwości z obiektu dostawcy do odbiorcy. Realizujemy to za pomocą instrukcji for-in, która iteruje po właściwościach obiektu supplier i przypisuje ich wartości do właściwości o tych samych nazwach w obiekcie receiver. Trzeba pamiętać, że jest to płytka kopia, więc w przypadku właściwości zawierających obiekty zarówno w odbiorcy, jak i dostawcy znajdzie się referencja do tego samego obiektu. Ten wzorzec jest często wykorzystywany w JavaScripcie do dodawania do obiektów zachowań, które są już zdefiniowane w innych obiektach. Zamiast stosowania dziedziczenia można w ten sposób dodać na przykład obsługę zdarzeń do dowolnego obiektu. Załóżmy, że mamy już zdefiniowany własny typ obsługujący zdarzenia: function EventTarget(){ } EventTarget.prototype = { constructor: EventTarget,  addListener: function(type, listener){

Wzorce tworzenia obiektów

113

// tworzymy tablicę, jeśli jeszcze nie istnieje if (!this.hasOwnProperty("_listeners")) { this._listeners = []; } if (typeof this._listeners[type] == "undefined"){ this._listeners[type] = []; } this._listeners[type].push(listener); }, 

fire: function(event){ if (!event.target){ event.target = this; } if (!event.type){ // jeśli false throw new Error("Obiekt zdarzenia nie ma właściwości 'type'."); } if (this._listeners && this._listeners[event.type] instanceof Array){ var listeners = this._listeners[event.type]; for (var i=0, len=listeners.length; i < len; i++){ listeners[i].call(this, event); } } },



removeListener: function(type, listener){ if (this._listeners && this._listeners[type] instanceof Array){ var listeners = this._listeners[type]; for (var i=0, len=listeners.length; i < len; i++){ if (listeners[i] === listener){ listeners.splice(i, 1); break; } } } }

};

114

Rozdział 6

Typ EventTarget zapewnia podstawową funkcjonalność obsługi zdarzeń. Do dowolnego obiektu można dodawać  funkcje obsługi zdarzeń, usuwać je , a także je odpalać . Funkcje obsługi zdarzeń są zapisywane we właściwości _listeners, która jest tworzona podczas pierwszego wywołania metody addListener(), dzięki czemu łatwiej jest zastosować tu domieszki. Instancji typu EventTarget można użyć w następujący sposób: var target = new EventTarget(); target.addListener("message", function(event) { console.log("Komunikat: " + event.data); }); target.fire({ type: "message", data: "Witaj, świecie!" });

Obsługa zdarzeń jest w obiektach bardzo często potrzebna. Jeśli chcemy, by obiekty innych typów również obsługiwały zdarzenia, możemy zastosować kilka rozwiązań. Można na przykład utworzyć nową instancję typu EventTarget i dodać do niej pożądane właściwości: var person = new EventTarget(); person.name = "Nicholas"; person.sayName = function() { console.log(this.name); this.fire({ type: "namesaid", name: name }); };

W zmiennej person zostaje zapisana instancja typu EventTarget, a następnie są do niej dodawane potrzebne właściwości (name i sayName). Niestety obiekt person jest instancją EventTarget, a nie Object czy innego własnego typu (na przykład Person). Trzeba też ręcznie dodać właściwości, co powoduje spory narzut kodu. Zdecydowanie trzeba znaleźć lepsze rozwiązanie. Drugim sposobem rozwiązania problemu jest zastosowanie pseudoklasycznego dziedziczenia: function Person(name) { this.name = name; }

Wzorce tworzenia obiektów

115



Person.prototype = Object.create(EventTarget.prototype); Person.prototype.constructor = Person; Person.prototype.sayName = function() { console.log(this.name); this.fire({ type: "namesaid", name: name }); }; var person = new Person("Nicholas");

console.log(person instanceof Person); // true console.log(person instanceof EventTarget); // true

W tym wypadku mamy nowy typ Person, który dziedziczy po Event Target . Do prototypu Person można dodać dowolne metody. Nie jest to jednak zgrabne rozwiązanie, a poza tym można się zastanowić nad sensem powiązania obu typów — czy obiekt reprezentujący osobę (Person) jest naprawdę podtypem zdarzenia (EventTarget)? Zastosowanie tu domieszek pozwala na zmniejszenie ilości kodu potrzebnego do przypisania nowych właściwości do prototypu: function Person(name) { this.name = name; } 

mixin(Person.prototype, new EventTarget()); mixin(Person.prototype, { constructor: Person, sayName: function() { console.log(this.name); this.fire({ type: "namesaid", name: name }); } });

var person = new Person("Nicholas"); console.log(person instanceof Person); // true console.log(person instanceof EventTarget); // false

Prototyp Person.prototype zostaje uzupełniony właściwościami instancji typu EventTarget , dzięki czemu otrzymuje jego funkcjonalność.

116

Rozdział 6

Następnie prototyp Person.prototype jest uzupełniany właściwościami constructor i sayName(). Instancje typu Person nie są obiektami Event Target, ponieważ nie dochodzi tu do dziedziczenia. W niektórych sytuacjach może się okazać, że lepszym wyjściem jest zrezygnowanie z dziedziczonego konstruktora. Trzeba wtedy użyć domieszkowania bezpośrednio podczas tworzenia nowego obiektu: var person = mixin(new EventTarget(), { name: "Nicholas", sayName: function() { console.log(this.name); this.fire({ type: "namesaid", name: name }); } });

W tym kodzie nowa instancja EventTarget jest uzupełniana nowymi właściwościami, w wyniku czego powstaje obiekt person, w którym nie został zaburzony łańcuch prototypów. Podczas korzystania w ten sposób z domieszek trzeba pamiętać, że właściwości funkcji dostępowych z obiektu dostawcy stają się właściwościami danych w odbiorcy, więc można je przez przypadek nadpisać. Wynika to z tego, że właściwości odbiorcy są tworzone poprzez przypisanie, a nie za pomocą metody Object.defineProperty(), a więc do właściwości odbiorcy jest przypisywana bieżąca wartość odpowiadającej jej właściwości dostawcy. Ilustruje to poniższy przykład: var person = mixin(new EventTarget(), {  get name() { return "Nicholas" }, sayName: function() { console.log(this.name); this.fire({ type: "namesaid", name: name }); } }); console.log(person.name);

// „Nicholas”

 person.name = "Greg"; console.log(person.name);

// „Greg”

Wzorce tworzenia obiektów

117

W tym kodzie pole name jest zdefiniowane jako właściwość funkcji dostępowych z getterem . Oznacza to, że przypisanie wartości do tej właściwości nie powinno dać żadnego rezultatu. Ponieważ tego typu właściwość staje się w obiekcie odbiorcy (person) właściwością danych, można ją nadpisać nową wartością . W funkcji mixin() wartość właściwości name jest odczytywana z dostawcy i przypisywana do odbiorcy, ale nie dochodzi tu do definiowania nowej funkcji dostępowej, więc name staje się właściwością danych. Jeśli chcielibyśmy wprowadzić mechanizm kopiujący właściwości funkcji dostępowych bez zamiany ich na właściwości danych, musielibyśmy użyć następującej funkcji: function mixin(receiver, supplier) {  Object.keys(supplier).forEach(function(property) { var descriptor = Object.getOwnPropertyDescriptor(supplier, property);  Object.defineProperty(receiver, property, descriptor); }); return receiver; } var person = mixin(new EventTarget(), { get name() { return "Nicholas" }, sayName: function() { console.log(this.name); this.fire({ type: "namesaid", name: name }); } }); console.log(person.name);

// „Nicholas”

person.name = "Greg"; console.log(person.name);

// „Nicholas”

Ta wersja funkcji mixin() korzysta z metody Object.keys()  w celu pobrania tablicy wszystkich wyliczalnych właściwości instancji z obiektu

118

Rozdział 6

supplier. Do iterowania po właściwościach została użyta metoda forEach(). W każdej iteracji z obiektu supplier jest odczytywany deskryptor właściwości, a następnie za pomocą metody Object.defineProperty() jest ona dodawana do obiektu receiver . Dzięki temu przenoszone są

oryginalne właściwości, a nie ich wartości. W związku z tym właściwość name jest w obiekcie person właściwością funkcji dostępowych, więc nie można jej nadpisać. Ta wersja funkcji mixin() działa tylko w silnikach JavaScriptu zgodnych z ECMAScript 5. Jeśli tworzony kod ma działać w starszych silnikach, powinno się zastosować funkcję łączącą oba rozwiązania: function mixin(receiver, supplier) {  if (Object.getOwnPropertyDescriptor) { Object.keys(supplier).forEach(function(property) { var descriptor = Object.getOwnPropertyDescriptor(supplier, property); Object.defineProperty(receiver, property, descriptor); }); } else {  for (var property in supplier) { if (supplier.hasOwnProperty(property)) { receiver[property] = supplier[property] } } } return receiver; }

W funkcji mixin() sprawdzamy, czy istnieje metoda Object.getOwn PropertyDescriptor() , co jednoznacznie wskazuje, czy dany silnik jest zgodny z ECMAScript 5. Jeśli metoda istnieje, wykonywany jest kod zgodny z nowym standardem, a w przeciwnym razie — kod ograniczony do możliwości ECMAScript 3 . Dzięki temu funkcji można używać zarówno w nowych, jak i starszych silnikach JavaScriptu. UWAGA Trzeba pamiętać, że metoda Object.keys() zwraca jedynie wyli-

czalne właściwości. Jeśli mają być kopiowane wszystkie właściwości, należy użyć metody Object.getOwnPropertyNames().

Wzorce tworzenia obiektów

119

Zabezpieczenie zasięgu w konstruktorach Ponieważ konstruktory są funkcjami, można je wywołać bez użycia operatora new. Wpływa to jednak na wartość this i może wywołać trudne do przewidzenia problemy, ponieważ w standardowym trybie this jest powiązane z globalnym obiektem, a w trybie ścisłym w takich sytuacjach rzucany jest błąd. W rozdziale 4. był prezentowany następujący przykład: function Person(name) { this.name = name; } Person.prototype.sayName = function() { console.log(this.name); };  var person1 = Person("Nicholas");

// uwaga: brakujący operator new

console.log(person1 instanceof Person); // false console.log(typeof person1); // "undefined" console.log(name); // "Nicholas"

W tym wypadku name staje się globalną zmienną, ponieważ konstruktor Person jest wywoływany bez operatora new . Stanie się tak tylko w standardowym trybie, a w ścisłym zostałby rzucony błąd. To, że nazwa konstruktora zaczyna się wielką literą, zwykle świadczy o tym, że wymaga operatora new. Co jednak zrobić, jeśli chcemy umożliwić korzystanie z funkcji bez tego operatora? Wiele wbudowanych konstruktorów, takich jak Array czy RegExp, można wywołać z operatorem new lub bez niego, a to wszystko dzięki temu, że są bezpieczne pod względem zasięgu. Tego typu konstruktory, bez względu na sposób ich wywołania, zawsze zwracają ten sam obiekt. Jeśli funkcja zostaje wywołana z operatorem new, to this dostępne w tej funkcji reprezentuje nowo utworzony obiekt. Można więc użyć operatora instanceof, by sprawdzić, czy w wywołaniu funkcji został użyty operator new: function Person(name) { if (this instanceof Person) { // wywołanie z użyciem new

120

Rozdział 6

} else { // wywołanie bez użycia new } }

Zastosowanie tego wzorca pozwala na przeprowadzenie odpowiednich operacji w przypadku wywołania za pomocą operatora new i bez niego. Można oczywiście podejść do tych dwóch sytuacji inaczej, ale najczęściej chcemy wykonać w funkcji konstruktora te same operacje (nawet mimo przypadkowego pominięcia operatora new). Poprawiona, bezpieczna pod względem zasięgu, wersja konstruktora Person wygląda tak: function Person(name) { if (this instanceof Person) { this.name = name; } else { return new Person(name); } }

W tym konstruktorze w wypadku zastosowania operatora new właściwość name jest przypisywana standardowo. Jeśli zostanie pominięty operator new, zostaje on rekurencyjnie wywołany z prawidłowo utworzoną instancją. Dzięki temu dwa poniższe obiekty są funkcjonalnie identyczne: var person1 = new Person("Nicholas"); var person2 = Person("Nicholas"); console.log(person1 instanceof Person); console.log(person2 instanceof Person);

// true // true

Tworzenie nowych obiektów bez użycia operatora new jest możliwe na przykład w wielu bibliotekach, ponieważ ich twórcy zabezpieczają się przed omyłkowym pominięciem tego operatora. Nawet w silniku JavaScriptu wiele konstruktorów jest budowanych w ten sposób (np. Object, Array, RegExp czy Error).

Wzorce tworzenia obiektów

121

Podsumowanie W JavaScripcie istnieje wiele sposobów tworzenia i komponowania obiektów. Mimo że nie ma w tym języku wbudowanego mechanizmu ukrywania właściwości jako prywatnych, można tworzyć dane i funkcje, które są dostępne tylko z wnętrza zawierającego je obiektu. W celu ukrycia danych w singletonach można użyć wzorca modułu, w którym prywatne właściwości (dane i metody) są definiowane w funkcji natychmiastowej, dzięki czemu są dostępne tylko w nowo tworzonym obiekcie. Metody uprzywilejowane to metody mające dostęp do prywatnych danych obiektu. Można też tworzyć konstruktory zawierające prywatne pola danych. W tym celu można albo zdefiniować zmienne w ciele konstruktora, albo zastosować funkcję natychmiastową (w takim wypadku prywatne dane będą współdzielone przez wszystkie instancje tego konstruktora). Domieszki pozwalają na dodawanie funkcjonalności do obiektów bez zaburzania łańcucha dziedziczenia. Technika ta polega na skopiowaniu właściwości z jednego obiektu do innego, dzięki czemu obiekt-odbiorca otrzymuje dodatkową funkcjonalność. Inaczej niż w przypadku dziedziczenia, domieszki nie pozwalają na określenie w utworzonym obiekcie-odbiorcy pochodzenia danej właściwości. Z tego względu to rozwiązanie sprawdza się najlepiej w sytuacjach, gdy właściwości nie jest dużo. Jeśli jakiś obiekt ma uzyskać więcej funkcjonalności bądź istotna jest informacja o jej pochodzeniu, najlepszym rozwiązaniem jest zastosowanie dziedziczenia. Konstruktory bezpieczne pod względem zasięgu można wywoływać z operatorem new lub bez niego. Podstawą tego wzorca jest to, że w konstruktorze wywołanym z operatorem new w this znajduje się referencja do nowo utworzonej instancji. W związku z tym można uzależnić zachowanie konstruktora od tego, czy został użyty operator new.

122

Rozdział 6

Skorowidz A aggregation, Patrz: agregacja agregacja, 12 arity, Patrz: funkcja arność

B błąd, 24

C closure, Patrz: domknięcie constructor stealing, Patrz: konstruktor zawłaszczanie czas, 24

D dane referencja, Patrz: referencja typ prosty, Patrz: typ prosty właściwość, Patrz: właściwość danych data, 24 dereferencja, 23 domieszka, 107, 113, 116 domknięcie, 109 dostawca, 113

dziedziczenie, 12 konstruktorów, 99 obiektów, 96 prototypowe, 91 pseudoklasyczne, 104, 115

E ECMAScript 5, 35, 47, 60, 119 encapsulation, Patrz: kapsułkowanie enumerable, Patrz: właściwość wyliczalna

F funkcja, 12, 16, 21, 24, 28, 35, 37 anonimowa, 39 argumentowość, Patrz: funkcja arność arność, 40 deklaracja, 36, 37 dostępowa, 58, 59, 65, 66, 77, 118 właściwość, Patrz: właściwość funkcji dostępowej odczytująca, 58, 59, 64 parametr, 39, 40 porównująca, 38 przeciążanie, 41 reflect, 26

funkcja sygnatura, 41 właściwość, 40, 58, 64 zapisująca, 58, 60, 64, 66

G garbage collection, Patrz: mechanizm odśmiecania pamięci generic, Patrz: obiekt ogólny getter, Patrz: funkcja odczytująca

H hoisting, 36

I IIFE, Patrz: wyrażenie funkcji natychmiastowej immediately invoked function expression, Patrz: wyrażenie funkcji natychmiastowej inheritance, Patrz: dziedziczenie instrukcja for-in, 113 interfejs implementacja, 12

J język bazujący na klasach, 15 bazujący na obiektach, 16 obiektowy, 11, 12, 16

K kapsułkowanie, 12 klasa, 11, 12, 15 konstruktor, 73 Array, 120, 121 bezparametrowy, 74 dziedziczenie, 99 Error, 121 Function, 26 literał, 75

124

Skorowidz

nazwa, 21 new Object, 21, 23, 24 Object, 21, 51, 121 RegExp, 26, 120, 121 tworzenie, 74 wywoływanie, 77 z prywatną właściwością, 111 zasięg, 120 zawłaszczanie, 103 zwracanie wartości, 76

L literał, 17, 24 konstruktora, 75 liczby, 24 łańcucha, 24 null, 24 obiektu, 25, 51, 75 undefined, 24 wartości logicznej, 24 wyrażenia regularnego, 26

M mechanizm odśmiecania pamięci, 22 przenoszenia deklaracji na początek, 37 rzutowania, 53 metoda, 13, 21, 43 add, 95 apply, 46, 103 Array.isArray, 30 bind, 47 call, 45, 103 Delete, 55 hasOwnProperty, 55, 78, 92 isPrototypeOf, 92 Object.create, 96 Object.defineProperties, 66 Object.defineProperty, 60, 62, 63, 64, 65, 117, 119 Object.freeze, 70

Object.getOwnPropertyDescriptor, 67, 119 Object.getOwnPropertyNames, 119 Object.isSealed, 69 Object.keys, 57, 119 Object.preventExtensions, 68 propertyIsEnumerable, 57, 61, 92 przesłonięta, 104 Put, 52, 53, 58 Set, 52 sort, 38 toString, 55, 81, 92, 93 typów prostych, 20 uprzywilejowana, 108 valueOf, 92, 93 współdzielenie, 78 mixin, Patrz: domieszka module pattern, Patrz: wzorzec modułu

N Node.js, 13 notacja z kropką, 12, 27

O obiekt, 12, 22, 51, 92 arguments, 39, 40 atrybut Extensible, 68 bez właściwości, 81 dereferencja, Patrz: dereferencja dziedziczenie, 96 literał, Patrz: literał obiektu modyfikowanie, 23 nierozszerzalny, 68, 69, 70 ogólny, 75 pieczętowanie, 69, 87 rozszerzalny, 68 this, 44, 45, 46, 47 tworzenie, 51 wbudowany, 88, 89 właściwość, Patrz: właściwość obiektu wzorzec, 107

zamrożony, 70, 87 zdarzenie, 113, 115 zmiennych, 16 odbiorca, 113, 117 operator, 93 ==, 20 ===, 20 delete, 55, 81 in, 54, 55 instanceof, 28, 30, 74, 75 new, 22, 24, 74, 120 typeof, 19, 35 own property, Patrz: właściwość własna

P pakiet, 15 pętla for-in, 56, 57 plik nagłówkowy, 12 polimorfizm, 12 primitive, Patrz: typ prosty primitive wrapper types, Patrz: typ opakowujący typ prosty privilaged method, Patrz: metoda uprzywilejowana programowanie obiektowe, 13 prototyp, 78 łańcuchowanie, 91, 103, 113 modyfikowanie, 86 obiektu wbudowanego, 88, 89 Object.prototype, 92, 94 Person.prototype, 117 właściwość, Patrz: właściwość prototypu prototypal inheritance, Patrz: dziedziczenie prototypowe prototype chaining, Patrz: prototyp łańcuchowanie pseudoclassical inheritance, Patrz: dziedziczenie pseudoklasyczne pseudodziedziczenie oparte na domieszkach, 113

Skorowidz

125

R receiver, Patrz: odbiorca redundancja, 78 referencja, 12, 16, 21, 22, 28, 45, 93 wyrażenie, 24 revealing module pattern, Patrz: wzorzec modułu z ujawnianiem

S setter, Patrz: funkcja zapisująca słowo kluczowe function, 36 get, 59 set, 59 sterta, 16 stos, 16 supplier, Patrz: dostawca

T tablica asocjacyjna klucz, 52 klucz-wartość, 55 indeksowana numerycznie, 24 tryb standardowy, 120 ścisły, 62, 63, 66, 69, 71, 77, 120 zwykły, 62, 64, 66, 69 typ opakowujący, 88, 93 Boolean, 30 Number, 30 String, 30 typ prosty, 30 prosty, 16, 17, 21, 93 Boolean, 17, 31, 93 Date, 93 identyfikowanie, 19 metoda, Patrz: metoda typów prostych null, 17, 19

126

Skorowidz

Number, 17, 31, 93 String, 17, 20, 31, 93 undefined, 17 referencyjny, Patrz: referencja wbudowany, 24 Array, 24 Date, 24 Error, 24 Function, 24 instancja, 24 Object, 24 RegExp, 24 typowanie słabe, 13

W wartość this, 45, 46, 47, 120 weak typing, Patrz: typowanie słabe właściwość, 16, 21, 27, 35 atrybut, 62, 63 Configurable, 60, 61, 65, 69 Enumerable, 56, 60, 61, 65 Extensible, 69 Get, 64 odczytywanie, 67 Set, 64 Value, 62, 64 Writable, 62, 64 zmiana, 60 Call, 35 constructor, 75, 84 danych, 58 deskryptor, 61, 96 dodana, 54 dodawanie, 23, 66 funkcji dostępowej, 58, 59, 64 instancji, 57, 60 konfigurowalna, 60 length, 39, 40 lista, 57 name, 63 obiektu, 52, 68, 69 Prototype, 52, 54, 55, 57, 78, 79, 92, 99

prywatna, 111 usuwanie, 23, 55 własna, 54 wyliczalna, 56, 57, 60, 119 wskaźnik, 22 wyrażenie funkcji natychmiastowej, 108 funkcyjne, 36, 37 regularne, 26 konstruowane dynamicznie, 27 rozpoznawane jako funkcja, 35

wzorzec modułu, 108, 110 obiektu, Patrz: obiekt wzorzec

Z zdarzenie, 113, 115 zmienna referencyjna, 23 znak ==, 20 ===, 20 ucieczki, 27

Skorowidz

127

128

Skorowidz