Podsumowanie

Transkrypt

Podsumowanie
helion kopia dla: Dawid Karwot [email protected]
Tytuł oryginału: You Don't Know JS: this & Object Prototypes
Tłumaczenie: Robert Górczyński
ISBN: 978-83-283-2183-0
© 2016 Helion SA.
Authorized Polish translation of the English edition of You Don't Know JS:
this & Object Prototypes ISBN 9781491904152 © 2014 Getify Solutions, Inc.
This translation is published and sold by permission of O’Reilly Media, Inc.,
which owns or controls all rights to publish and sell the same.
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/tajejs_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
 Poleć książkę na Facebook.com
 Kup w wersji papierowej
 Oceń książkę
 Księgarnia internetowa
 Lubię to! » Nasza społeczność
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Spis treści
Przedmowa ........................................................................................ 5
Wprowadzenie ................................................................................... 7
1. this, czyli co? .................................................................................... 13
Dlaczego this?
Zamieszanie
Czym jest this?
Podsumowanie
13
15
22
22
2. To wszystko ma teraz sens! ............................................................... 23
Źródło wywołania funkcji
Tylko reguły
Wszystko w odpowiedniej kolejności
Wyjątki dotyczące wiązań
Leksykalne this
Podsumowanie
23
25
35
41
46
48
3. Obiekty ............................................................................................ 51
Składnia
Typ
Zawartość obiektu
Iteracja
Podsumowanie
51
52
55
77
82
3
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
4. Mieszanie obiektów „klas” ................................................................85
Teoria klas
Mechanika klas
Dziedziczenie klasy
Domieszki
Podsumowanie
85
89
92
98
106
5. Prototypy ....................................................................................... 107
[[Prototype]]
„Klasa”
Funkcje „klasy”
Dziedziczenie (prototypowe)
Łącza obiektu
Podsumowanie
107
113
113
123
131
137
6. Delegowanie .................................................................................. 139
Projekt oparty na delegowaniu
Klasy kontra obiekty
Prostszy projekt
Ładniejsza składnia
Introspekcja
Podsumowanie
140
153
158
165
168
171
A ES6 class ......................................................................................... 173
B Podziękowania ............................................................................... 183
Skorowidz ...................................................................................... 187
4

Spis treści
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Przedmowa
Kiedy czytałem tę książkę, przygotowując się do napisania przedmowy, byłem
zmuszony do przypomnienia sobie, jak wyglądała droga, która doprowadziła
mnie do poznania języka JavaScript, i ile zmieniło się przez tych piętnaście lat,
odkąd zajmuję się programowaniem w tym języku.
Gdy piętnaście lat temu zacząłem programować w JavaScript, używanie na
stronach internetowych technologii innych niż HTML, takich jak CSS lub
JavaScript, było określane jako DHTML, czyli Dynamic HTML. Wtedy jednak użyteczność języka JavaScript była zupełnie inna i przypominała raczej
dodawanie animowanych płatków śniegu na stronie internetowej lub dynamicznych zegarów wyświetlających godzinę w pasku stanu. Muszę przyznać,
że tak naprawdę na początku mojej kariery nie zwracałem zbyt wielkiej uwagi
na JavaScript, przede wszystkim ze względu na nowatorski charakter implementacji często spotykanych w internecie.
Dopiero w 2005 roku odkryłem na nowo JavaScript i zacząłem go postrzegać
jako prawdziwy język programowania, któremu należy się uważniej przyjrzeć.
Po zapoznaniu się z pierwszą wersją beta aplikacji Mapy Google dostrzegłem
potencjał kryjący się w tym języku. W tamtym czasie aplikacja Mapy Google
jako jedyna pozwalała na poruszanie się po mapie za pomocą myszki, na
zmianę poziomu powiększenia i wykonywanie żądań do serwera bez konieczności ponownego wczytywania strony — a to wszystko dzięki językowi
JavaScript. To przypominało czary!
5
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Kiedy coś wydaje się magią, oznacza to zwykle nowy rozdział i nową jakość.
Okazało się, że miałem rację. Mimo upływu czasu JavaScript nadal jest jednym
z podstawowych języków, których używam do programowania zarówno po
stronie klienta, jak i serwera. Szczerze mówiąc, nie wyobrażam sobie, aby
można było robić to za pomocą innego języka.
Patrząc na ostatnich piętnaście lat, żałuję tylko, że nie dałem językowi JavaScript większej szansy jeszcze przed 2005 rokiem. Po prostu nie przewidziałem,
że stanie się on prawdziwym językiem programowania, podobnie jak C++, C#,
Java i wiele innych.
Gdybym na początku mojej kariery miał do dyspozycji serię książek Tajniki
języka JavaScript, moja droga zawodowa być może potoczyłaby się zupełnie
inaczej. Cecha, za którą uwielbiam tę serię, to solidna dawka wiedzy o języku
JavaScript przedstawiona w wyczerpujący i zabawny sposób.
Pozycja Wskaźnik this i prototypy obiektów stanowi świetną kontynuację serii.
Nie tylko uzupełnia wiedzę przekazaną we wcześniejszej książce Zakresy
i domknięcia, ale niewątpliwie jeszcze ją poszerza o omówienie bardzo ważnych aspektów języka JavaScript, jakimi są słowo kluczowe this i prototypy.
Te dwa zagadnienia są konieczne do opanowania materiału przedstawionego
w kolejnych książkach serii, ponieważ stanowią podstawy prawdziwego programowania w JavaScript. Koncepcja dotycząca sposobu tworzenia obiektów,
łączenia ich ze sobą i rozszerzania jest niezbędna do zbudowania ogromnych
i skomplikowanych aplikacji w JavaScript. Bez wspomnianych koncepcji opracowanie złożonych aplikacji, takich jak Mapy Google, byłoby niemożliwe.
Muszę stwierdzić, że większa część programistów sieciowych prawdopodobnie
nigdy nie utworzyła żadnego obiektu JavaScript i dlatego traktuje ten język jako
oparty na zdarzeniach sposób połączenia przycisków z żądaniami wykonywanymi w technologii Ajax. W pewnym momencie swojej kariery też tak uważałem, ale po poznaniu prototypów i sposobów tworzenia obiektów w JavaScript
dostrzegłem ogrom możliwości, jakie znajdują się dosłownie na wyciągnięcie
ręki. Jeśli zaliczasz się do tej samej kategorii programistów, koniecznie przeczytaj tę książkę. Natomiast jeśli potrzebujesz jedynie odświeżenia wiadomości,
także i Ty znajdziesz tu wiele przydatnych informacji. Z pewnością nie rozczarujesz się. Zaufaj mi!
— Nick Berardi
nickberardi.com, @nberardi
6

Przedmowa
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Wprowadzenie
Jestem pewien, że zwróciłeś już na to uwagę — „JS” w tytule serii książek
nie jest tylko skrótem wyrazów oznaczających kurs języka JavaScript. Kurs
dotyczący dziwactw języka JavaScript to prawdopodobnie coś, na co wszyscy
czekamy!
Od pierwszych dni istnienia internetu język JavaScript zawsze był podstawową
technologią odpowiedzialną za dostarczanie interaktywnej treści użytkownikowi. Wprawdzie na początku cechami charakterystycznymi języka JavaScript
były na przykład migający kursor myszy lub irytujące wyskakujące okna, ale
teraz, niemal dwie dekady później, zarówno technologia, jak i możliwości
języka JavaScript zwiększyły się o kilka rzędów wielkości. Już tylko nieliczni
wątpią w to, że JavaScript jest sercem najbardziej rozpowszechnionej platformy
oprogramowania na świecie — internetu.
Jednak jako język JavaScript jest też nieustannie przedmiotem krytyki wynikającej po części z jego dziedzictwa, choć w znacznie większej mierze dotyczącej przyjętej filozofii projektowej. Krytykowana jest nawet jego nazwa
— jak to określił Brendan Eich — traktuje się go jako „głupszego młodszego
brata” porównywanego z jego „starszym i dojrzalszym bratem” — Javą.
Warto w tym miejscu dodać, że nazwa „JavaScript” powstała przypadkiem,
na styku polityki i marketingu. Te dwa języki są zupełnie odmienne, pomimo
że ich nazwy zawierają ciąg liter „Java”. JavaScript ma taki związek z Javą jak
ser z serpentyną.
Ponieważ JavaScript zapożyczył koncepcje i idiomy składni od kilku innych
języków — między innymi silne korzenie stylu proceduralnego stosowanego
w języku C, jak również subtelne elementy stylu funkcjonalnego pochodzące
7
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
z języków Scheme i Lisp — jest przystępny dla wielu programistów, nawet tych
z jedynie niewielkim lub nawet żadnym doświadczeniem w programowaniu.
Utworzenie programu typu „Witaj, świecie!” w JavaScript jest tak proste, że
trudno o lepszą zachętę do poznania i używania tego języka.
Choć JavaScript jest prawdopodobnie jednym z najłatwiejszych języków programowania i dość szybko można rozpocząć z nim pracę, to jego dziwactwa
powodują, że mało kto decyduje się na solidne jego opanowanie. Utworzenie
kompletnego programu w językach takich jak C i C++ wymaga dobrej ich
znajomości, natomiast pełny program w JavaScript można napisać, znając jedynie jego podstawy.
Istnieje tendencja, aby zaawansowane koncepcje głęboko zakorzenione w języku wyrażać w pozornie prosty sposób. Przykładem może być tutaj przekazywanie funkcji w postaci wywołań zwrotnych. To zachęca programistów
JavaScript do użycia języka w istniejącej postaci i nieprzejmowania się tym, co
dzieje się „pod maską”.
JavaScript to łatwy i prosty w użyciu język cieszący się szerokim uznaniem,
choć jednocześnie zawiera kolekcję skomplikowanych i zróżnicowanych mechanizmów. Bez wnikliwej analizy prawdziwe ich zrozumienie może się okazać trudne nawet dla najbardziej doświadczonych programistów JavaScript.
Na tym polega paradoks języka JavaScript — to jego pięta Achillesa i zarazem
wyzwanie, któremu próbujemy sprostać. Ponieważ JavaScript może być używany bez większej wiedzy na jego temat, programiści często nie starają się jej
posiąść.
Misja
Gdyby za każdym razem, gdy w języku JavaScript coś Cię zaskakuje lub wywołuje frustrację, Twoją odpowiedzią było zniechęcenie się i umieszczenie tego
języka na czarnej liście (jak to niektórzy mają w zwyczaju), to bardzo szybko
utraciłbyś możliwość korzystania z wielu jego zalet.
Chociaż część przedstawionego tutaj materiału powiela dobrze znaną książkę
JavaScript — mocne strony, zachęcam do potraktowania niniejszej jako przedstawiającej jasne strony, bezpieczne strony lub nawet komplementarne strony
języka JavaScript.
8

Wprowadzenie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Seria książek Tajniki języka JavaScript stawia przed Tobą inne wyzwania —
naukę i dokładne poznanie wszystkich aspektów języka JavaScript, a w szczególności jego ciemnych stron.
Chciałbym zmienić powszechną wśród programistów JavaScript tendencję do
uczenia się tego języka tylko w niezbędnym zakresie, bez konieczności podejmowania trudu i dokładnego zrozumienia, dlaczego zachowuje się w taki,
a nie inny sposób. Co więcej, uważam, że nie należy się wycofywać, gdy podczas nauki pojawiają się trudności.
Nie jestem usatysfakcjonowany (i Ty również nie powinieneś), kiedy coś
działa, a ja nie bardzo wiem dlaczego. Zachęcam Cię zatem do porzucenia
łatwych i prostych dróg na rzecz tych nieco wyboistych i z pozoru prowadzących na manowce, aby przekonać się, jak niezwykłym językiem jest JavaScript
i jakie efekty można otrzymać przy jego wykorzystaniu. Gdy masz taką wiedzę, żadna technologia, żaden framework lub krzykliwy nagłówek nigdy Cię
nie zaskoczą i zawsze będą zrozumiałe.
Wszystkie książki z tej serii są poświęcone kluczowym elementom języka,
które dość powszechnie są błędnie rozumiane lub nadinterpretowane. Materiał przedstawiany w książkach umożliwia dogłębne i wnikliwe zapoznanie się
z omawianym tematem. Lektura powinna pozostawić w Tobie silne przekonanie o zdobytej wiedzy, nie tylko teoretycznej, ale i praktycznej.
Język JavaScript, jaki poznałeś do tej pory, to prawdopodobnie tylko pewna
część wiedzy o nim, dostarczona Ci przez innych, być może nieco zniechęconych swym niepełnym zrozumieniem tematu. Taki JavaScript to zaledwie
cień prawdy. Wprawdzie jeszcze nie znasz tego języka, ale jeśli zgłębisz książki
z tej serii, na pewno go poznasz. Zapraszam więc do lektury. JavaScript czeka
na Ciebie!
Podsumowanie
JavaScript jest wspaniałym językiem. Jest prosty, gdy uczymy się go stopniowo,
i dużo trudniejszy, jeśli chcemy opanować go w całości (lub jedynie wystarczająco). Kiedy programiści napotykają trudności, zwykle obwiniają język,
a nie własny brak wiedzy. Seria książek Tajniki języka JavaScript ma to zmienić
i pomóc Ci docenić język, który powinieneś dobrze znać.
Podsumowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

9
0
Wiele przykładów przedstawionych w książce wykorzystuje nowoczesne (i wykraczające w przyszłość) funkcje wprowadzone
w specyfikacji ES6. Niektóre fragmenty kodu mogą nie działać
w oczekiwany sposób, jeśli zostaną uruchomione w starszych
(niezgodnych ze specyfikacją ES6) silnikach JavaScript.
Konwencje zastosowane w książce
W tej książce zastosowano następujące konwencje typograficzne:
Kursywa
Oznacza nowe i ważne pojęcia, adresy URL i e-mail, nazwy plików, rozszerzenia plików itd.
Czcionka o stałej szerokości
Użyta w przykładowych fragmentach kodu, a także w samym tekście, aby
odwołać się do pewnych poleceń bądź innych elementów programistycznych, takich jak nazwy zmiennych lub funkcji, baz danych, typów danych,
zmiennych środowiskowych, poleceń i słów kluczowych.
Pogrubiona czcionka o stałej szerokości
Użyta w celu wyeksponowania poleceń bądź innego tekstu, który powinien
być wprowadzony przez Czytelnika.
Pochylona czcionka o stałej szerokości
Wskazuje tekst, który powinien być zastąpiony wartościami podanymi
przez użytkownika bądź wynikającymi z kontekstu.
Taka ikona oznacza ogólną uwagę.
Użycie przykładowych kodów
Książka ta ma pomóc Ci w pracy. Ogólnie rzecz biorąc, można wykorzystywać przykłady z tej książki w swoich programach i dokumentacji. Nie trzeba
kontaktować się z nami w celu uzyskania zezwolenia, dopóki nie powiela się
znaczących ilości kodu. Na przykład pisanie programu, w którym znajdzie się
kilka fragmentów kodu z tej książki, nie wymaga zezwolenia, jednak sprzedawanie lub rozpowszechnianie płyty CD-ROM zawierającej przykłady z książki
10

Wprowadzenie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
wydawnictwa O’Reilly wymaga zezwolenia. Odpowiedź na pytanie przez cytowanie tej książki lub przykładowego kodu nie wymaga zezwolenia, ale już
włączenie znaczących ilości przykładowych kodów do dokumentacji produktu
Czytelnika takiego zezwolenia wymaga.
Jesteśmy wdzięczni za przypisy, ale nie wymagamy ich. Przypis zwykle zawiera
tytuł, autora, wydawcę i ISBN. Na przykład: Kyle Simpson, Tajniki języka
JavaScript. Wskaźnik this i prototypy obiektów, ISBN 978-83-283-2175-5,
Helion, Gliwice 2016.
Jeżeli jednak masz jakiekolwiek wątpliwości dotyczące użycia przykładowych
kodów, po prostu skontaktuj się z nami, korzystając z adresu permissions@
oreilly.com.
Użycie przykładowych kodów
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

11
0
12

Wprowadzenie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
ROZDZIAŁ 1.
this, czyli co?
Jednym z mechanizmów, które sprawiają najwięcej problemów w języku
JavaScript, jest słowo kluczowe this. Jest to specjalne słowo kluczowe, które jest
automatycznie definiowane w zakresie każdej funkcji. Jednak to, do czego się
ono odwołuje, może sprawiać trudności nawet najbardziej doświadczonym
programistom JavaScript.
Każda wystarczająco zaawansowana technologia staje się nie do odróżnienia
od magii.
— Arthur C. Clark
Wprawdzie mechanizm this w języku JavaScript nie jest aż tak bardzo zaawansowany, ale programiści często parafrazują powyższy cytat, wstawiając
słowo „skomplikowany” lub „zagmatwany”. Nie ulega wątpliwości, że może Ci
się wydawać, iż w działaniu mechanizmu this jest coś magicznego — aby go
odczarować, trzeba po prostu dogłębnie go zrozumieć.
Dlaczego this?
Skoro mechanizm this sprawia spore trudności nawet doświadczonym programistom JavaScript, być może zastanawiasz się, czy w ogóle jest on użyteczny.
Czy po prostu z istnienia mechanizmu this nie wynika więcej problemów niż
pożytku? Zanim przejdziemy do poznania sposobu, w jaki działa this, najpierw musisz się dowiedzieć, dlaczego ten mechanizm istnieje.
13
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Spróbujmy zilustrować motywację stojącą za utworzeniem mechanizmu this:
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Witaj, jestem " + identify.call( this );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Czytelnik"
};
identify.call( me ); // KYLE.
identify.call( you ); // CZYTELNIK.
speak.call( me ); // Witaj, jestem KYLE.
speak.call( you ); // Witaj, jestem CZYTELNIK.
Jeżeli zrozumienie powyższego fragmentu kodu pokazującego sposób działania this sprawia Ci trudność, nie przejmuj się tym! Wkrótce wszystko stanie się
jasne. Po prostu na chwilę odstaw na bok wcześniejsze pytania, co pozwoli nam
dokładniej przyjrzeć się, dlaczego istnieje mechanizm this.
W powyższym fragmencie kodu funkcje identify() i speak() mogą być wielokrotnie użyte względem wielu obiektów kontekstu (me i you). Dzięki temu
eliminujemy konieczność przygotowania oddzielnych wersji funkcji dla każdego obiektu.
Zamiast opierać się na słowie kluczowym this, równie dobrze można by wyraźnie przekazywać obiekt kontekstu w wywołaniach funkcji identify() i speak():
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Witaj, jestem " + identify( context );
console.log( greeting );
}
identify( you ); // CZYTELNIK.
speak( me ); // Witaj, jestem KYLE.
14

Rozdział 1. this, czyli co?
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Jednak mechanizm this zapewnia znacznie bardziej elegancki sposób niejawnego przekazywania odwołania do obiektu, więc efektem jest bardziej
przejrzysty projekt API oraz łatwiejsza możliwość wielokrotnego użycia.
Im bardziej skomplikowany wzorzec użycia, tym wyraźniej można dostrzec,
że przekazanie kontekstu w postaci jawnie podanego parametru najczęściej
oznacza znacznie większy bałagan niż przekazanie kontekstu this. Podczas
analizy obiektów i prototypów przekonasz się o użyteczności kolekcji funkcji,
które mogą automatycznie odwoływać się do właściwego obiektu kontekstu.
Zamieszanie
Wkrótce rozpocznę wyjaśnianie rzeczywistego sposobu działania this, jednak
wcześniej chciałbym położyć kres pewnym błędnym wyobrażeniom dotyczącym tego, jak mechanizm this w rzeczywistości nie działa.
Nazwa this1 może wywoływać pewne zamieszanie, gdy programista zbyt dosłownie próbuje potraktować jej znaczenie. Najczęściej są przyjmowane dwa
założenia, przy czym oba są nieprawidłowe.
Odniesienie do siebie
Pierwszą pokusą jest przyjęcie założenia, że this odwołuje się do samej funkcji.
Na takie założenie wpływ może mieć gramatyka.
Dlaczego pochodzące z wewnątrz odwołanie do danej funkcji może być użyteczne? Wśród najczęściej pojawiających się powodów jest na przykład rekurencja (wywołanie funkcji z wewnątrz) lub przygotowanie procedury obsługi
zdarzeń, która może być odłączona po jej pierwszym wywołaniu.
Programiści dopiero poznający mechanizmy JavaScript bardzo często traktują
wspomniane odniesienie do funkcji jako obiekt (wszystkie funkcje w JavaScript
są obiektami!) pozwalający na przechowywanie informacji o stanie (wartości
właściwości) między wywołaniami funkcji. Wprawdzie tego rodzaju możliwość
niewątpliwie istnieje i ma pewne ograniczone zastosowania, ale w pozostałej
części książki poznasz wiele innych i lepszych niż obiekt funkcji miejsc przeznaczonych do przechowywania informacji o stanie.
1
Dosłownie „to” — przyp. tłum.
Zamieszanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

15
0
Rozważmy jednak w tej chwili powyższy wzorzec, aby pokazać, że this
nie pozwala funkcji na pobranie odniesienia do samej siebie, jak można by
zakładać.
Przeanalizuj poniższy fragment kodu, w którym próbujemy określić liczbę
wywołań funkcji foo():
function foo(num) {
console.log( "foo: " + num );
// Próba określenia liczby wywołań funkcji foo().
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// Ile razy została wywołana funkcja foo()?
console.log( foo.count ); // 0 — Co u licha?
Wartość foo.count nadal wynosi 0, pomimo że cztery wywołania console.log()
wyraźnie wskazują na czterokrotne wywołanie funkcji foo(..). Frustracja
wynika ze zbyt dosłownej interpretacji znaczenia słowa kluczowego this
w poleceniu this.count++ .
Wykonanie polecenia foo.count = 0 faktycznie oznacza dodanie właściwości
o nazwie count do obiektu funkcji foo(). Jednak w przypadku odniesienia
this.count wewnątrz funkcji this tak naprawdę w ogóle nie odwołuje się
do obiektu funkcji. Dlatego też pomimo takich samych nazw właściwości
obiekty, do których następuje odwołanie, są inne — i to właśnie wywołuje
zamieszanie.
16

Rozdział 1. this, czyli co?
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Odpowiedzialny programista powinien w tym miejscu zadać
następujące pytanie: „Skoro przeprowadziłem inkrementację
właściwości count, ale innej, niż sądziłem, to którą właściwość
tak naprawdę inkrementowałem?”. Jeżeli zaczniesz się nad tym
pytaniem dokładnie zastanawiać, przekonasz się, że przypadkowo utworzyłeś zmienną globalną o nazwie count (w rozdziale 2.
dowiesz się, jak do tego doszło!), a jej bieżącą wartością jest NaN.
Oczywiście po dokonaniu tego zadziwiającego odkrycia pojawi się mnóstwo innych pytań, na przykład: „Jak to możliwe, że
powstała zmienna globalna i dlaczego ma wartość NaN zamiast
prawidłowej wartości licznika?”. Odpowiedzi na te pytania
znajdziesz w rozdziale 2.
Zamiast zatrzymać się w tym miejscu, dalej próbując zrozumieć, dlaczego słowo
kluczowe this nie zachowuje się w oczekiwany sposób, i szukać odpowiedzi
na zadane wcześniej trudne, choć ważne pytania, wielu programistów po prostu stara się uniknąć problemu poprzez zaimplementowanie zupełnie innego
rozwiązania. Przykładem alternatywnego podejścia może być utworzenie
obiektu przeznaczonego do przechowywania właściwości count:
function foo(num) {
console.log( "foo: " + num );
// Próba określenia liczby wywołań funkcji foo().
data.count++;
}
var data = {
count: 0
};
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// Ile razy została wywołana funkcja foo()?
console.log( data.count ); // 4
Zamieszanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

17
0
Wprawdzie powyższe podejście „rozwiązuje” problem, ale niestety po prostu
na zasadzie zignorowania tego prawdziwego, co wynika z braku zrozumienia,
co tak naprawdę oznacza this i na jakiej zasadzie działa. Zamiast tego mamy
powrót do komfortowej strefy znacznie lepiej znanego mechanizmu, czyli zakresu leksykalnego.
Zakres leksykalny jest doskonałym i użytecznym mechanizmem.
W tym miejscu w żaden sposób nie próbuję lekceważyć lub
deprecjonować jego użycia (patrz inna książka z tej serii zatytułowana Zakresy i domknięcia2). Jednak nieustanne zgadywanie, jak należy używać this, i zwykle jego błędne stosowanie to jeszcze nie powód do powrotu do zakresu leksykalnego.
Jeżeli będziesz tak postępował, nigdy nie dowiesz się, dlaczego
this działa inaczej, niż tego oczekujesz.
W celu odwołania się do obiektu funkcji z wewnątrz danej funkcji samo this
zwykle nie wystarcza. Najczęściej konieczne jest odwołanie się do obiektu
funkcji za pomocą identyfikatora leksykalnego (zmienna) wskazującego tę
funkcję.
Spójrz na poniższe przykłady dwóch funkcji:
function foo() {
foo.count = 4; // Tutaj 'foo' odwołuje się do tej funkcji.
}
setTimeout( function(){
// Funkcja anonimowa (pozbawiona nazwy)
// nie może się odwoływać do samej siebie.
}, 10 );
W pierwszej funkcji, określanej mianem „funkcji nazwanej”, foo jest odniesieniem, którego można użyć w celu odwołania się do danej funkcji z jej
wnętrza.
Jednak w przykładzie drugiej funkcji wywołanie zwrotne przekazane do
setTimeout(..) nie zawiera identyfikatora nazwy (mamy tutaj „funkcję anonimową”), więc nie ma prawidłowego sposobu odniesienia się do obiektu
tej funkcji.
2
K. Simpson, Tajniki języka JavaScript. Zakresy i domknięcia, Helion, 2016.
18

Rozdział 1. this, czyli co?
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Pewien dość stary i obecnie już uznany za przestarzały sposób
oparty na odwołaniu arguments.callee wewnątrz funkcji również
prowadzi do obiektu aktualnie wykonywanej funkcji. Wymienione odwołanie to zwykle jedyny sposób pozwalający na uzyskanie dostępu do obiektu funkcji anonimowej z jej wnętrza.
Jednak najlepsze podejście polega na unikaniu w ogóle stosowania funkcji anonimowych, przynajmniej jeśli zachodzi potrzeba odwoływania się do nich z ich wnętrza. Zamiast anonimowych najlepiej używać funkcji nazwanych. Podejście oparte
na arguments.callee jest uznawane za przestarzałe i nie powinno być stosowane.
Innym rozwiązaniem w przedstawionym przykładzie będzie użycie we wszystkich miejscach identyfikatora foo jako odwołania do obiektu funkcji i w ogóle
uniknięcie this. Tego rodzaju rozwiązanie działa bez problemów:
function foo(num) {
console.log( "foo: " + num );
// Próba określenia liczby wywołań funkcji foo().
foo.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// Ile razy została wywołana funkcja foo()?
console.log( foo.count ); // 4
Jednak powyższe podejście również ma efekt uboczny w postaci braku
rzeczywistego zrozumienia sposobu działania this, co wynika z oparcia się
całkowicie na leksykalnym zakresie zmiennej foo.
Zamieszanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

19
0
Kolejnym podejściem do problemu jest wymuszenie na this faktycznego
wskazania obiektu funkcji foo():
function foo(num) {
console.log( "foo: " + num );
// Próba określenia liczby wywołań funkcji foo().
// Uwaga: 'this' to teraz naprawdę JEST 'foo',
// co wynika ze sposobu wywołania 'foo' (patrz opis poniżej).
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// Używając 'call(..)', mamy pewność, że 'this'
// odwołuje się do obiektu funkcji ('foo').
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// Ile razy została wywołana funkcja foo()?
console.log( foo.count ); // 4
Zamiast unikać mechanizmu this, skwapliwie z niego korzystamy. Później
wyjaśnię dokładniej sposób działania przedstawionych powyżej technik, więc
nie przejmuj się, jeśli wszystko nie jest jeszcze jasne.
Zakres
Kolejnym błędnym wyobrażeniem dotyczącym znaczenia this jest przekonanie, że mechanizm ten w pewien sposób odwołuje się do zakresu funkcji.
To dość trudna kwestia, ponieważ w jednym sensie jest prawdziwa, zaś w innym nie. Dlatego też może być to dość mylące.
Chcę w tym miejscu wyraźnie powiedzieć, że this w żaden sposób nie odwołuje
się do zakresu leksykalnego funkcji. To prawda, że wewnętrznie zakres jest
pewnego rodzaju obiektem wraz z właściwościami dla każdego z dostępnych
20

Rozdział 1. this, czyli co?
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
identyfikatorów. Jednak „obiekt” zakresu nie jest dostępny dla kodu JavaScript
— to wewnętrzna część implementacji silnika.
Przeanalizuj poniższy fragment kodu, który (bez powodzenia!) próbuje przekroczyć granicę i użyć słowa kluczowego this w celu niejawnego odniesienia
się do zakresu leksykalnego funkcji:
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); //ReferenceError: niezdefiniowane.
W powyższym fragmencie kodu mamy więcej błędów. Wprawdzie może się
wydawać, że powyższy kod to jedynie przykład, ale tak naprawdę to pochodna
rzeczywistego kodu pojawiającego się często na publicznych forach pomocy.
To doskonała (choć jednocześnie smutna) ilustracja, pokazująca, jak bardzo
błędne mogą być założenia dotyczące this.
Przede wszystkim podejmowana jest próba odwołania się do funkcji bar()
za pomocą this.bar(). Przez zupełny przypadek to rzeczywiście działa, ale
szczegóły na ten temat poznasz wkrótce. Najbardziej naturalnym sposobem
wywołania funkcji bar() będzie pominięcie słowa kluczowego this i po prostu użycie leksykalnego odwołania się do identyfikatora.
Jednak programista tworzący tego rodzaju kod próbuje wykorzystać this
w celu przygotowania pomostu między leksykalnymi zakresami funkcji foo()
i bar(), aby funkcja bar() mogła uzyskać dostęp do zmiennej znajdującej się
w wewnętrznym zakresie foo(). Nie ma możliwości stworzenia wspomnianego pomostu. Nie można użyć słowa kluczowego this do wyszukania innego
elementu w zakresie leksykalnym. To jest po prostu niemożliwe.
Za każdym razem, gdy próbujesz połączyć operacje wyszukiwania w zakresach leksykalnych za pomocą słowa kluczowego this, powtarzaj sobie: tego
rodzaju połączenie nie istnieje.
Zamieszanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

21
0
Czym jest this?
Skoro poznałeś różne nieprawidłowe założenia dotyczące this, teraz możemy
skierować naszą uwagę na sposób, w jaki mechanizm this faktycznie działa.
Wcześniej dowiedziałeś się, że this to nie wiązanie utworzone przez programistę w trakcie tworzenia kodu, ale wiązanie powstające w czasie jego wykonywania. Kontekst tego wiązania jest oparty na warunkach istniejących w trakcie
wywołania funkcji. Wiązanie this nie ma nic wspólnego z deklaracją funkcji,
natomiast ma dużo wspólnego ze sposobem wywoływania danej funkcji.
Kiedy funkcja jest wywoływana, następuje utworzenie rekordu aktywacji,
nazywanego także kontekstem wykonywania. Wspomniany rekord zawiera
informacje o źródle wywołania funkcji (tak zwany stos wywołań), sposobie
wywołania funkcji, przekazanych parametrach itd. Jedną z właściwości omawianego rekordu jest odwołanie this, które będzie stosowane w trakcie wykonywania funkcji.
W następnym rozdziale dowiesz się, jak odszukać źródło wywołania funkcji,
co pozwoli na określenie, jak w trakcie jej wykonywania zostanie utworzone
wiązanie this.
Podsumowanie
Wiązanie this nieustannie wywołuje zamieszanie wśród programistów JavaScript, którzy nie poświęcili czasu na poznanie rzeczywistego sposobu działania tego mechanizmu. Zgadywanie, metoda prób i błędów, bezmyślne kopiowanie i wklejanie kodu z odpowiedzi znalezionych w serwisie Stack Overflow
— to nie są ani efektywne, ani odpowiednie sposoby wykorzystania tak ważnego
mechanizmu.
Aby poznać mechanizm this, w pierwszej kolejności musisz się dowiedzieć,
czym on nie jest, tak by wykluczyć wszelkie błędne założenia i wyobrażenia,
które mogą spowodować wymienione wcześniej problemy. Słowo kluczowe
this nie oznacza odwołania do bieżącej funkcji ani nie jest odniesieniem do
zakresu leksykalnego tej funkcji.
Mechanizm this to wiązanie powstające w trakcie wywoływania funkcji.
Dlatego też to, do czego się odwołuje this, jest określane w całości przez
źródło wywołania danej funkcji.
22

Rozdział 1. this, czyli co?
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
ROZDZIAŁ 2.
To wszystko ma teraz sens!
W rozdziale 1. przedstawiłem różne błędne wyobrażenia dotyczące słowa kluczowego this. Dowiedziałeś się, że this to wiązanie powstające w chwili wywołania danej funkcji i całkowicie oparte na źródle (sposobie) jej wywołania.
Źródło wywołania funkcji
W celu zrozumienia wiązania this konieczne jest poznanie koncepcji źródła
wywołania funkcji, czyli miejsca w kodzie, gdzie nastąpiło wywołanie danej
funkcji (a nie miejsca, gdzie jest ona zadeklarowana). Aby odpowiedzieć na
pytanie, do czego odwołuje się this, trzeba przejrzeć wszystkie źródła wywołania funkcji.
To zadanie, ogólnie rzecz biorąc, polega na odszukaniu wszystkich wywołań
funkcji. Jednak nie zawsze będzie to takie łatwe, ponieważ pewne wzorce tworzenia kodu mogą ukrywać faktyczne miejsce wywołania danej funkcji.
Bardzo ważne jest uwzględnienie tak zwanego stosu wywołań (czyli stosu funkcji wywołanych w celu przejścia do bieżącego momentu działania programu).
Interesujące nas źródło wywołania funkcji znajduje się w wywołaniu przed
aktualnie wykonywaną funkcją.
Poniżej przedstawiam przykład stosu wywołań i źródła wywołania funkcji:
function baz() {
// Stos wywołań to: 'baz', więc źródło wywołania
// funkcji znajduje się w zakresie globalnym.
console.log( "baz" );
bar(); // <-- Źródło wywołania funkcji 'bar'.
23
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
}
function bar() {
// Stos wywołań to: 'baz' -> 'bar', więc źródło wywołania
// funkcji znajduje się w 'baz'.
console.log( "bar" );
foo(); // <-- Źródło wywołania funkcji 'foo'.
}
function foo() {
// Stos wywołań to: 'baz' -> 'bar' -> 'foo',
// więc źródło wywołania funkcji znajduje się w 'bar'.
console.log( "foo" );
}
baz(); // <-- Źródło wywołania funkcji 'baz'.
Bądź ostrożny podczas analizy kodu i wyszukiwania rzeczywistych źródeł
wywołania funkcji (w stosie wywołań), ponieważ to jedyna kwestia, która ma
znaczenie dla wiązania this.
Stos wywołań można sobie wizualizować w myślach jako kolejne
wywołania funkcji. Takie podejście przedstawiłem w komentarzach w powyższym fragmencie kodu. Takie rozwiązanie jest
jednak nieco uciążliwe i łatwo popełnić w nim błąd. Innym
sposobem sprawdzenia stosu wywołań jest użycie narzędzia do
debugowania w przeglądarce internetowej. Większość nowoczesnych przeglądarek internetowych w komputerach stacjonarnych ma wbudowane narzędzia programistyczne obejmujące
między innymi debugger JavaScript. W przypadku poprzedniego fragmentu kodu w narzędziach programistycznych możesz
ustawić punkt kontrolny w pierwszym wierszu funkcji foo()
lub po prostu wstawić polecenie debugger; w pierwszym wierszu.
Po uruchomieniu strony debugger zatrzyma jej działanie we
wskazanym miejscu i pokaże listę funkcji wywołanych przed dotarciem do tego miejsca — w ten sposób otrzymasz upragniony
stos wywołań. Dlatego też jeśli próbujesz zdiagnozować wiązanie this, wykorzystaj narzędzia programistyczne do otrzymania
stosu wywołań. Następnie odszukaj drugie wywołanie od góry,
a poznasz rzeczywiste źródło wywołania funkcji.
24

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Tylko reguły
Teraz kierujemy naszą uwagę na sposób, w jaki źródło wywołania funkcji określa miejsce, do którego będzie się odwoływać słowo kluczowe this w trakcie
wykonywania danej funkcji.
Konieczne jest przeanalizowanie wszystkich źródeł wywołania funkcji i ustalenie, które z czterech reguł mają zastosowanie. Zacznę od wyjaśnienia
wszystkich czterech reguł niezależnie od siebie, a następnie przejdę do kolejności ich pierwszeństwa, jeśli wiele z nich można zastosować do źródła
wywołania funkcji.
Wiązanie domyślne
Pierwsza omawiana tutaj reguła wynika z najczęściej występującego przypadku
dla wywołań funkcji, czyli wywołania samodzielnej funkcji. Tę regułę można
potraktować jako domyślną i stosować wtedy, gdy nie uda się wykorzystać
żadnej innej.
Spójrz na poniższy fragment kodu:
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
Jeżeli jeszcze tego nie zrobiłeś, zwróć uwagę na zmienną zadeklarowaną w zakresie globalnym (var a = 2). To synonim właściwości obiektu globalnego
o takiej samej nazwie. Nie mamy tutaj do czynienia z kopią właściwości —
to są zupełnie różne właściwości. Możesz je potraktować jako dwie strony tej
samej monety.
Następnie zauważ, że w trakcie wywołania funkcji foo() polecenie this.a odwołuje się do zmiennej globalnej a. Dlaczego tak się dzieje? Ponieważ w omawianym przypadku nastąpiło zastosowanie wiązania domyślnego this, a zatem
this odwołuje się do obiektu globalnego.
Tylko reguły
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

25
0
Skąd wiemy, że tutaj ma zastosowanie reguła wiązania domyślnego? Przeanalizujemy źródło wywołania funkcji, aby poznać sposób wywołania foo(). W omawianym fragmencie kodu funkcja foo() jest wywoływana za pomocą zwykłego,
nieoznaczonego w żaden dodatkowy sposób odwołania funkcji. Ponieważ nie
mają tutaj zastosowania żadne inne reguły, mamy do czynienia z wiązaniem
domyślnym.
Po zastosowaniu trybu ścisłego obiekt globalny nie może być używany w wiązaniu domyślnym i dlatego wtedy this odnosi się do wartości niezdefiniowanej
(undefined):
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: 'this' odwołuje się do wartości 'undefined'.
Warto w tym miejscu wspomnieć o subtelnym, choć ważnym szczególe: ogólnie rzecz biorąc, reguły dotyczące wiązania this są określane całkowicie na podstawie źródła wywołania funkcji, ale dla obiektu globalnego można zastosować tylko wiązanie domyślne, jeśli zawartość foo() nie działa w trybie ścisłym.
Stan trybu ścisłego źródła wywołania funkcji foo() pozostaje nieistotny:
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
Celowe łączenie w kodzie trybów ścisłego i nieścisłego jest niemile widziane. Cały program powinien działać w jednym z wymienionych trybów. Czasami jednak zdarza się, że dołączona
jest biblioteka zewnętrzna pozostająca w innym trybie działania
niż kod programu. Dlatego też należy zachować ostrożność
i zwracać uwagę na te subtelne szczegóły dotyczące zgodności.
26

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Wiązanie niejawne
Inną regułą do uwzględnienia jest istnienie obiektu kontekstu dla źródła wywołania funkcji, określanego także mianem obiektu właściciela lub obiektu
zawierającego, choć wymienione wyrażenia alternatywne mogą się okazać
nieco mylące.
Spójrz na poniższy fragment kodu:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
Przede wszystkim zwróć uwagę na sposób zadeklarowania funkcji foo(), a następnie dodania jako właściwości odniesienia w obiekcie obj. Niezależnie od
tego, czy funkcja foo() została początkowo zadeklarowana w obj, czy dodana
później jako odniesienie (taki wariant zastosowałem w powyższym fragmencie
kodu), w żadnym z wymienionych przypadków funkcja foo() nie jest „w posiadaniu” obiektu obj ani „nie zawiera się” w nim.
Jednak to źródło wywołania funkcji używa kontekstu obj w celu odwołania się
do funkcji, można więc powiedzieć, że w trakcie wywoływania funkcji obiekt
obj jest właścicielem odwołania do funkcji lub je zawiera.
Niezależnie od nazwy nadanej temu wzorcowi w chwili wywołania funkcji
foo() mamy odwołanie do obj. Kiedy istnieje obiekt kontekstu dla odwołania
do funkcji, zgodnie z regułą wiązania niejawnego to właśnie ten obiekt powinien
być użyty dla wiązania this w danej funkcji. A ponieważ obiekt obj będzie
wskazywany przez this dla funkcji foo(), this.a jest synonimem obj.a.
Dla źródła wywołania funkcji znaczenie ma tylko odniesienie do właściwości
obiektu najwyższego (ostatniego) poziomu. Na przykład:
function foo() {
console.log( this.a );
}
var obj2 = {
Tylko reguły
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

27
0
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
Niejawna utrata
Jednym z najbardziej frustrujących zachowań związanych z tworzeniem wiązania this jest utrata przez funkcję wiązania niejawnego. Wiąże się to zwykle
z zastosowaniem domyślnego wiązania do obiektu globalnego lub wartości
undefined (w zależności od tego, czy zastosowany jest tryb ścisły).
Spójrz na poniższy fragment kodu:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // Odwołanie do funkcji/alias!
var a = "oops, global"; // 'a' jest również właściwością w obiekcie globalnym.
bar(); // "Ups! Obiekt globalny".
Choć wydaje się, że bar to odwołanie do obj.foo, tak naprawdę mamy do
czynienia z kolejnym odwołaniem do foo(). Co więcej, źródło wywołania
funkcji ma znaczenie — w omawianym przypadku to bar(), czyli zwykłe,
niczym nieudekorowane wywołanie, a tym samym zostało zastosowane
wiązanie domyślne.
Ze znacznie bardziej subtelnym, częściej występującym i dużo bardziej nieoczekiwanym przypadkiem mamy do czynienia, gdy rozważamy przekazanie
funkcji wywołania zwrotnego:
28

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// 'fn' to po prostu kolejne odwołanie do 'foo'.
fn(); // <-- Źródło wywołania funkcji!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // 'a' jest również właściwością w obiekcie globalnym.
doFoo( obj.foo ); // "Ups! Obiekt globalny".
Przekazanie parametrów to niejawne przypisanie, a ponieważ mamy do czynienia z przekazaniem funkcji, to nieuniknione jest niejawne przypisanie odwołania.
Efekt będzie dokładnie taki sam jak w przypadku poprzedniego fragmentu kodu.
Co zrobić w sytuacji, gdy przekazywane wywołanie zwrotne nie zostało utworzone przez Ciebie, ale jest wbudowane w język? Tutaj nie ma żadnej różnicy,
wynik pozostaje taki sam:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // 'a' jest również właściwością w obiekcie globalnym.
setTimeout( obj.foo, 100 ); // "Ups! Obiekt globalny".
Spójrz na poniższą teoretyczną pseudoimplementację setTimeout() dostarczoną jako funkcja wbudowana środowiska JavaScript:
function setTimeout(fn, delay) {
// Oczekiwanie przez podaną (wartość 'delay') liczbę milisekund.
fn(); // <-- Źródło wywołania funkcji!
}
Tylko reguły
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

29
0
Dość często zdarza się, że funkcje wywołań zwrotnych tracą swoje wiązania
this, tak jak to wcześniej przedstawiłem. Jednak mechanizm this może zaskoczyć także w inny sposób, gdy funkcja, do której przekazujemy wywołanie
zwrotne, celowo zmienia this dla danego wywołania. Procedury obsługi zdarzeń w popularnych bibliotekach JavaScript zmuszają, aby w przypadku wywołań zwrotnych słowo kluczowe this prowadziło na przykład do elementu
modelu DOM, który wywołał dane zdarzenie. Wprawdzie takie rozwiązanie
może być czasami użyteczne, ale w innych sytuacjach będzie doprowadzało do
szału. Niestety wspomniane narzędzia rzadko kiedy dają Ci wybór na tym polu.
Tak czy inaczej, w przypadku nieoczekiwanej zmiany this nie masz kontroli
nad sposobem wykonania odwołania funkcji wywołania zwrotnego, więc nie
masz (jeszcze) żadnej możliwości kontroli, czy źródło wywołania funkcji dostarczy oczekiwanego wiązania. Wkrótce dowiesz się, jak można „usunąć” ten
problem przez poprawę this.
Wiązanie jawne
W przypadku wiązania niejawnego — jak to wcześniej widziałeś — konieczne
było zmodyfikowanie obiektu, aby miał odwołanie do funkcji, a następnie użycie właściwości zawierającej odwołanie do funkcji w celu pośredniego (niejawnego) wiązania this z obiektem.
A co zrobić w sytuacji, gdy chcesz zmusić wywołanie funkcji do użycia określonego obiektu dla wiązania this, ale bez umieszczania w obiekcie właściwości
zawierającej odwołanie do funkcji?
Wszystkie funkcje języka mają pewne narzędzia dostępne za pomocą [[Pro
totype]] (więcej informacji na ten temat znajdziesz w dalszej części książki),
co jest niezwykle użyteczne do wykonania wspomnianego zadania. Szczególnie
interesują nas metody o nazwach call(..) i apply(..). Pod względem technicznym środowisko hosta JavaScript czasami dostarcza funkcje, które są na
tyle specjalne (na pewien sposób), że nie zawierają wspomnianej funkcjonalności. To tylko niewielka część funkcji. Ogromna większość dostarczanych
funkcji i praktycznie wszystkie tworzone przez Ciebie mają dostęp do metod
call(..) i apply(..).
Na czym polega działanie wymienionych metod narzędziowych? Jako pierwszy
parametr pobierają obiekt, do którego ma się odwoływać słowo kluczowe this.
30

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Następnie wywołują funkcję wskazywaną przez this. Ponieważ wyraźnie
wskazujemy, do czego ma się odwoływać this, mamy do czynienia z wiązaniem jawnym.
Spójrz na poniższy fragment kodu:
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
Wywołanie foo() wraz z jawnym wiązaniem dzięki wywołaniu foo.call(..)
pozwala na wymuszenie, aby słowo kluczowe this odwoływało się do obj.
Jeżeli jako wiązanie this przekażesz wartość typu prostego (na przykład string,
boolean lub number), wspomniana wartość zostanie opakowana obiektem (odpowiednio new String(..), new Boolean(..) i new Number(..)). Tego rodzaju
działanie jest często określane jako pakowanie.
W kontekście wiązania this nie ma różnicy między wywołaniami
call(..) i apply(..). Mogą się zachowywać różnie po zastosowaniu parametrów dodatkowych, ale takim wariantem nie
będziemy się tutaj zajmować.
Niestety wiązanie jawne samo w sobie nie oferuje żadnego rozwiązania wspomnianego wcześniej problemu, czyli „utraty” przez funkcję oczekiwanego wiązania this. Po prostu mamy do czynienia z utorowaniem drogi przez framework.
Wiązanie twarde
Jednak pewna odmiana wiązania jawnego będzie w stanie zagwarantować nam
otrzymanie oczekiwanego zachowania. Spójrz na poniższy fragment kodu:
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
Tylko reguły
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

31
0
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// W przypadku trwałego wiązania 'bar' nie będziemy mieli do czynienia z nadpisywaniem 'this'.
bar.call( window ); // 2
Przeanalizujmy teraz sposób działania powyższego wariantu. Tworzymy funkcję bar(), która wewnętrznie wywołuje foo.call(obj), a tym samym wymusza
wywołanie foo wraz z obj jako wiązaniem dla this. Niezależnie od tego, jak
później wywołasz funkcję bar(), zawsze będzie wywoływała foo wraz z obj.
Tego rodzaju wiązanie jest zarówno jawne, jak i trwałe, więc możemy je nazwać wiązaniem twardym.
Najbardziej typowym sposobem opakowania funkcji wiązaniem twardym jest
utworzenie „okna” przeznaczonego do przekazywania wszelkich argumentów
oraz pobierania wartości zwrotnej:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5
Innym sposobem wyrażenia powyższego wzorca jest utworzenie funkcji pomocniczej przeznaczonej do wielokrotnego użycia:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// Prosta funkcja pomocnicza bind().
function bind(fn, obj) {
return function() {
32

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
return fn.apply( obj, arguments );
};
}
var obj = {
a: 2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
Ponieważ wiązanie twarde jest dość często stosowanym wzorcem, począwszy
od wydania specyfikacji ES5, dostępna jest dla niego funkcja pomocnicza
Function.prototype.bind(), z której można korzystać w następujący sposób:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
Wartością zwrotną bind(..) jest nowa funkcja, która ma na stałe zdefiniowane wywołania do funkcji początkowej wraz z kontekstem this ustawionym
w podany sposób.
Kontekst wywołań funkcji API
Większość funkcji bibliotek oraz wiele nowych funkcji wbudowanych w język
JavaScript i środowisko hosta dostarcza dodatkowy parametr, nazywany zwykle kontekstem. Wymieniony parametr został opracowany jako rozwiązanie
pozwalające na uniknięcie konieczności użycia bind(..) w celu zagwarantowania, że funkcja wywołania zwrotnego skorzysta z podanego this.
Na przykład:
function foo(el) {
console.log( el, this.id );
}
Tylko reguły
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

33
0
var obj = {
id: "wspaniale"
};
// Użycie 'obj' jako 'this' dla wywołań 'foo(..)'.
[1, 2, 3].forEach( foo, obj );
// 1 wspaniale 2 wspaniale 3 wspaniale
Wewnętrznie te różne funkcje niemal na pewno używają wiązania jawnego
za pomocą call(..) i apply(..), co pozwala uniknąć konieczności ręcznego
wywoływania wymienionych metod narzędziowych.
Wiązanie za pomocą operatora new
Czwarta i ostatnia reguła dotycząca wiązania this wymaga przeanalizowania
bardzo często spotykanego błędnego wyobrażenia o funkcjach i obiektach
w języku JavaScript.
W tradycyjnych językach obiektowych konstruktor jest specjalną metodą dołączoną do klasy. Podczas tworzenia egzemplarza klasy za pomocą operatora
new następuje wywołanie wspomnianego konstruktora. Z reguły mamy do
czynienia z następującym wywołaniem:
something = new MyClass(..);
JavaScript ma operator new, a wzorzec kodu do użycia jest niemal identyczny
z tym, który spotykamy w innych językach obiektowych. Większość programistów przyjmuje więc założenie, że działanie mechanizmu w JavaScript jest
podobne, ale tak nie jest.
Przede wszystkim jednak uściślmy, czym tak naprawdę jest „konstruktor” w języku JavaScript. Konstruktor to po prostu funkcja, która ma zostać wywołana
po użyciu operatora new. Konstruktor nie jest dołączony do klasy ani nie odpowiada za tworzenie egzemplarza klasy. Nie jest to również funkcja specjalnego typu. Konstruktor to zwykła funkcja, która tak naprawdę została przechwycona przez użycie słowa kluczowego new w jej wywołaniu.
Na przykład rozważ funkcję Number(..) działającą w charakterze konstruktora.
Poniższy fragment pochodzi ze specyfikacji ES5.1:
15.7.2. Konstruktor Number
Kiedy funkcja Number() jest wywoływana jako część wyrażenia new, jest konstruktorem i inicjalizuje nowo utworzony obiekt.
34

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Tak więc praktycznie każda funkcja, włącznie z wbudowanymi funkcjami,
takimi jak Number(..) (patrz rozdział 3.), może być wywołana wraz z poprzedzającym ją słowem kluczowym new — w ten sposób to wywołanie funkcji
staje się wywołaniem konstruktora. Mamy tutaj subtelny i zarazem bardzo
ważny szczegół: tak naprawdę nie ma czegoś takiego jak „funkcja konstruktora”,
ale raczej wykonanie funkcji za pomocą wywołania konstruktora.
Kiedy mamy do czynienia z wywołaniem funkcji wraz z użyciem słowa kluczowego new, czyli z tak zwanym wywołaniem konstruktora, poniższe operacje
są wykonywane automatycznie:
1. Utworzenie (skonstruowanie) zupełnie nowego obiektu.
2. Nowo utworzony obiekt zostaje połączony z łańcuchem [[Prototype]].
3. Nowo skonstruowany obiekt ma ustawione wiązanie this prowadzące do
danego wywołania funkcji.
4. Jeżeli wartością zwrotną nie jest alternatywny obiekt, wywołanie wraz ze
słowem kluczowym new automatycznie zwróci nowo skonstruowany obiekt.
Dla aktualnie omawianego zagadnienia znaczenie mają kroki 1., 3. i 4. Wprawdzie na razie pominiemy krok 2., ale powrócimy do niego w rozdziale 5.
Spójrz na poniższy fragment kodu:
function foo(a) {
this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
W przypadku wywołania funkcji foo(..) wraz ze słowem kluczowym new wynikiem jest utworzenie nowego obiektu i określenie go jako wiązania this dla wywołania foo(..). Dlatego new stanowi ostatni sposób, na jaki wywołanie funkcji
może utworzyć wiązanie dla this. Ten rodzaj wiązania nazywamy wiązaniem new.
Wszystko w odpowiedniej kolejności
W poprzednim podrozdziale przedstawiłem cztery reguły wiązania this w wywołaniu funkcji. Twoje zadanie sprowadza się do odszukania źródła wywołania
funkcji i sprawdzenia, która z omówionych reguł ma zastosowanie w danym
przypadku. Mógłbyś w tym miejscu zapytać: co w sytuacji, gdy dla danego źródła
Wszystko w odpowiedniej kolejności
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

35
0
wywołania funkcji można zastosować wiele reguł? Przecież musi istnieć pewna kolejność pierwszeństwa dla reguł, prawda? Faktycznie, istnieje kolejność,
w jakiej stosowane są omówione wcześniej reguły.
Powinno być jasne, że wiązanie domyślne ma najniższy priorytet z wszystkich
czterech omówionych reguł, i dlatego na razie odkładamy to wiązanie na bok.
Które z wymienionych — wiązanie niejawne czy wiązanie jawne — ma pierwszeństwo? Przekonajmy się o tym:
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
Jak wynika z powyższego fragmentu kodu, wiązanie jawne ma pierwszeństwo
przed wiązaniem niejawnym, co oznacza, że w pierwszej kolejności należy
sprawdzić, czy zastosowanie ma wiązanie jawne, a dopiero później przeprowadzić sprawdzenie wiązania niejawnego.
Teraz pozostało nam określenie, jaki priorytet ma wiązanie new:
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
36

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
Z powyższego fragmentu kodu wynika, że wiązanie new ma pierwszeństwo
przed wiązaniem niejawnym. Jak sądzisz, czy wiązanie new ma, czy nie ma
pierwszeństwa przed wiązaniem jawnym?
Nie można używać razem operatora new i wywołań call()/apply().
Dlatego też wywołanie new foo.call(obj1) jest niedozwolonym
sposobem bezpośredniego sprawdzenia wiązania new i wiązania
jawnego. Nadal jednak można wykorzystać wiązanie twarde do
sprawdzenia, która z wymienionych dwóch reguł ma pierwszeństwo.
Zanim przejdziemy do kodu w poszukiwaniu odpowiedzi na powyższe pytanie,
zastanówmy się przez chwilę nad fizycznym mechanizmem działania wiązania
twardego. Funkcja Function.prototype.bind(..) tworzy nową funkcję opakowującą, w której został umieszczony kod zastępujący wiązanie this (niezależnie od tego, jakie ono będzie) wiązaniem dostarczonym przez nas. Dlatego
też może się wydawać oczywiste przyjęcie założenia, że wiązanie twarde (będące
formą wiązania jawnego) ma pierwszeństwo przed wiązaniem new, a tym samym nie może być nadpisane za pomocą użycia słowa kluczowego new.
Sprawdźmy to:
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
Wszystko w odpowiedniej kolejności
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

37
0
Hola, nie tak prędko! Wprawdzie bar ma wiązanie twarde do obj1, ale wywołanie new bar(3) nie powoduje zmiany wartości obj1.a na 3, jak można by tego
oczekiwać. Zamiast tego wywołanie wiązania twardego (obj1) do bar(..)
może być nadpisane za pomocą słowa kluczowego new. Ponieważ zostało zastosowane słowo kluczowe new, otrzymujemy z powrotem nowo utworzony
obiekt, któremu nadajemy nazwę baz. Jak można zobaczyć, baz.a faktycznie
ma wartość 3.
To może być zaskakujące, gdy powrócimy do wspomnianej wcześniej przykładowej funkcji pomocniczej bind(..):
function bind(fn, obj) {
return function() {
fn.apply( obj, arguments );
};
}
Jeżeli przeanalizujesz działanie kodu powyższej funkcji pomocniczej, zauważysz, że nie ma żadnego sposobu, aby wywołanie operatora new mogło nadpisać
wiązanie twarde do obj, jak to zostało zaobserwowane.
Wbudowana funkcja Function.prototype.bind(..) w wydaniu specyfikacji
ES5 jest jednak znacznie bardziej skomplikowana. Poniżej przedstawiam (nieco
zmodyfikowany) fragment skryptu typu polyfill znalezionego na stronie MDN
poświęconej bind(..):
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== "function") {
// Rozwiązanie najbliższe specyfikacji ECMAScript 5
// to wewnętrzna funkcja IsCallable.
throw new TypeError(
"Function.prototype.bind - nastąpiła próba wywołania " +
"tego, czego nie można wywołać."
);
}
var aArgs = Array.prototype.slice.call( arguments, 1 ),
fToBind = this,
fNOP = function(){},
fBound = function(){
return fToBind.apply(
(
this instanceof fNOP &&
oThis ? this : oThis
),
38

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
aArgs.concat(
Array.prototype.slice.call( arguments )
);
}
;
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
Przedstawiony powyżej skrypt polyfill dla bind(..) różni się
od funkcji bind(..) wbudowanej w ES5 przy założeniu, że
funkcje wiązania twardego będą używane wraz ze słowem kluczowym new (czytaj dalej, aby dowiedzieć się, dlaczego jest to
użyteczne rozwiązanie). Ponieważ skrypt typu polyfill nie może utworzyć funkcji bez .prototype, jak ma to miejsce w przypadku wbudowanej funkcji pomocniczej, mamy pewną rozbieżność w ocenie tego samego zachowania. Postępuj ostrożnie,
jeśli zamierzasz używać słowa kluczowego new z funkcją wiązania twardego i w tym celu opierasz się na skrypcie typu polyfill.
Fragment pozwalający na nadpisanie new przedstawia się następująco:
this instanceof fNOP &&
oThis ? this : oThis
// …i:
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
Nie zamierzam tutaj dokładnie wyjaśniać sposobu działania tej sztuczki (to nieco bardziej skomplikowane i wykracza poza zakres omawianego zagadnienia).
Działanie funkcji narzędziowej w zasadzie sprowadza się do ustalenia, czy wywołanie funkcji wiązania twardego nastąpiło z użyciem słowa kluczowego new
(co skutkuje skonstruowaniem nowego obiektu wskazywanego przez this).
Jeżeli tak, ten nowo utworzony obiekt będzie wskazywany przez this, a nie
wcześniej podany w wiązaniu twardym.
Dlaczego możliwość nadpisania wiązania twardego przez new można uznać za
użyteczną?
Wszystko w odpowiedniej kolejności
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

39
0
Podstawowym zastosowaniem dla tego rodzaju rozwiązania jest utworzenie
funkcji (która wraz z operatorem new może służyć do konstruowania obiektów),
praktycznie ignorującej this wiązania twardego, choć jednocześnie definiującej wybrane lub wszystkie argumenty funkcji. Jedną z możliwości bind(..)
jest to, że dowolne argumenty przekazane po pierwszym argumencie wiązania this są domyślnie argumentami funkcji podstawowej (technicznie jest to
określane mianem aplikacji częściowej). Na przykład:
function foo(p1,p2) {
this.val = p1 + p2;
}
// Poniżej używamy 'null', ponieważ nie przejmujemy się
// 'this' w wiązaniu twardym w przedstawionym przypadku.
// Poza tym i tak dojdzie do nadpisania 'this' przez 'new'!
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
Ustalenie this
Podsumujmy teraz reguły dotyczące określenia this na podstawie źródła
wywołania funkcji, zachowując kolejność pierwszeństwa tych reguł. Zadawaj
sobie poniższe pytania i zakończ operację po znalezieniu pierwszej dopasowanej reguły.
1. Czy funkcja została wywołana za pomocą new (wiązanie twarde)? Jeżeli tak,
wówczas this prowadzi do nowo skonstruowanego obiektu.
var bar = new foo()
2. Czy funkcja została wywołana za pomocą wywołania call() lub apply()
(wiązanie jawne), nawet gdy to wywołanie zostało ukryte wewnątrz bind
wiązanie twarde? Jeżeli tak, wówczas this prowadzi do wyraźnie podanego obiektu.
var bar = foo.call( obj2 )
3. Czy funkcja została wywołana wraz z kontekstem (wiązanie niejawne),
czyli z obiektem właściciela lub obiektem zawierającym? Jeżeli tak, wówczas this prowadzi do tego obiektu kontekstu.
var bar = obj1.foo()
40

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
4. W przeciwnym razie mamy domyślne this (wiązanie domyślne). W przypadku trybu ścisłego wybierz wartość undefined, natomiast w pozostałych
przypadkach obiekt globalny.
var bar = foo()
Tak to wygląda. I to już wszystko, co jest wymagane do zrozumienia reguł
wiązania this dla zwykłych wywołań funkcji. Cóż, prawie wszystko.
Wyjątki dotyczące wiązań
Jak zwykle istnieją pewne wyjątki dotyczące omówionych reguł.
W pewnych sytuacjach zachowanie dotyczące wiązania this może być zaskakujące. Na przykład oczekujesz konkretnego wiązania, a ostatecznie okazuje
się, że otrzymujesz wiązanie domyślne.
Zignorowane this
Jeżeli w wywołaniu call(), apply() lub bind() przekażesz wartość null bądź
undefined jako parametr wiązania this, wówczas wymienione wartości zostaną zignorowane i zamiast tego w trakcie danego wywołania nastąpi utworzenie wiązania domyślnego:
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
Mógłbyś w tym miejscu zapytać: jaki jest powód celowego przekazywania wartości takiej jak null dla wiązania this?
Bardzo często można się spotkać z użyciem wywołania apply(..) do przekazania tablic wartości jako parametrów wywołania funkcji. Podobnie funkcja
bind(..) może przekazywać parametry (ustalone wartości), co okazuje się
niezwykle użyteczne:
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
Wyjątki dotyczące wiązań
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

41
0
// Przekazanie tablicy jako parametrów.
foo.apply( null, [2, 3] ); // a:2, b:3
// Przekazanie danych za pomocą 'bind(..)'.
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
Oba przedstawione powyżej rozwiązania wymagają pierwszego parametru
w postaci wiązania this. Jeżeli dana funkcja nie przejmuje się this, potrzebna
jest pewna wartość wstawiana w miejsce zarezerwowane dla this. W takim
przypadku null wydaje się rozsądnym rozwiązaniem, jak pokazałem w powyższym fragmencie kodu.
Wprawdzie nie omówię tego w książce, ale specyfikacja ES6 definiuje operator ..., który syntaktycznie pozwala na „rozpowszechnienie” tablicy jako parametrów bez konieczności stosowania apply(..), na przykład w postaci foo(..[1,2]), co jest
równoznaczne z foo(1,2). Syntaktycznie unikamy wiązania this,
o ile nie jest potrzebne. Niestety specyfikacja ES6 nie oferuje
syntaktycznego odpowiednika rozwijania funkcji, więc parametr this wywołania bind(..) nadal wymaga uwagi.
Istnieje jednak nieco ukryte niebezpieczeństwo związane z użyciem wartości
null, gdy nie trzeba się przejmować wiązaniem this. Jeżeli kiedykolwiek zastosujesz takie rozwiązanie względem wywołania funkcji (na przykład wywołania funkcji biblioteki zewnętrznej, nad którą nie masz kontroli) i wspomniana funkcja używa odwołania do this, wówczas reguła wiązania domyślnego
oznacza powstanie przypadkowego odniesienia (co gorsza modyfikowalnego!)
do obiektu global (w przeglądarce internetowej to window).
Oczywiście tego rodzaju niebezpieczeństwo może prowadzić do różnych błędów,
które okazują się bardzo trudne do zdiagnozowania i wyśledzenia.
Bezpieczniejsze this
Prawdopodobnie bezpieczniejszym rozwiązaniem jest przekazanie jako this
konkretnie skonfigurowanego obiektu, co do którego mamy pewność, że nie
będzie powodować efektów ubocznych w aplikacji. Zapożyczając terminologię z dziedziny sieci komputerowych (i wojska), można powiedzieć o utworzeniu obiektu DMZ (strefy zdemilitaryzowanej) — to nic specjalnego, po
prostu zupełnie pusty obiekt (patrz rozdziały 5. i 6.).
42

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Jeżeli zawsze będziesz przekazywać obiekt DMZ dla zignorowanego wiązania
this, którym nie musisz się przejmować, to masz pewność, że wszelkie ukryte
lub nieoczekiwane użycie tego this będzie ograniczone jedynie do pustego
obiektu. Tym samym unikniesz efektów ubocznych, jakie mogłyby powstać
podczas korzystania z obiektu global w programie.
Ponieważ obiekt jest całkowicie pusty, przechowującej go zmiennej lubię nadawać nazwę ø (mały znak matematyczny oznaczający zbiór pusty). W wielu
układach klawiatur (na przykład w systemach Mac OS) ten symbol otrzymasz
po naciśnięciu klawiszy Option+o. Niektóre systemy pozwalają również na
przypisanie określonych symboli do wybranych klawiszy. Jeżeli nie lubisz
symbolu ø lub jego wpisanie nie jest wcale takie łatwe na Twojej klawiaturze,
możesz oczywiście nadać tej zmiennej dowolną nazwę.
Niezależnie od nazwy nadanej zmiennej najłatwiejszym sposobem utworzenia całkowicie pustego obiektu jest użycie wywołania Object.create(null) —
patrz rozdział 5. Wymienione wywołanie jest podobne do { }, ale odbywa się
bez delegowania do Object.prototype, więc otrzymujemy „bardziej pusty”
obiekt niż w przypadku użycia { }:
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// Nasz pusty obiekt DMZ.
var ø = Object.create( null );
// Przekazanie tablicy jako parametrów.
foo.apply( ø, [2, 3] ); // a:2, b:3
// Rozwinięcie funkcji za pomocą wywołania 'bind(..)'.
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
Przedstawione rozwiązanie jest nie tylko bezpieczniejsze pod względem
funkcjonalnym, ale jednocześnie pokazuje możliwość użycia symbolu jako
nazwy zmiennej. Pod względem semantycznym podana nazwa oznacza
„this ma być pustym obiektem” i jest znacznie czytelniejsza niż null. Warto
w tym miejscu przypomnieć ponownie, że obiektowi DMZ możesz nadać
dowolną nazwę.
Wyjątki dotyczące wiązań
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

43
0
Odwołania pośrednie
Inną kwestią, na którą trzeba zwrócić uwagę, jest możliwość (celowego bądź
nie!) utworzenia pośrednich odwołań do funkcji. W takim przypadku, gdy
zostanie wywołana wspomniana funkcja, zastosowana będzie reguła wiązania
domyślnego.
Pośrednie odwołania powstają najczęściej w wyniku przypisania:
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
W powyższym fragmencie kodu wartość wynikowa wyrażenia przypisania
p.foo = o.foo to odwołanie do obiektu funkcji podstawowej. Dlatego też
źródłem wywołania funkcji jest po prostu foo(), a nie p.foo() lub o.foo(),
jak można by tego oczekiwać. Z omówionych wcześniej reguł zastosowanie
ma reguła wiązania domyślnego.
Przypomnienie: niezależnie od sposobu wywołania funkcji za pomocą reguły
wiązania domyślnego stan trybu ścisłego dla zawartości wywołanej funkcji
powoduje, że odwołanie do this (a nie źródło wywołania funkcji) — określa
wartość wiązania domyślnego: obiekt global w przypadku trybu innego niż
ścisły lub undefined w przypadku trybu ścisłego.
Osłabienie wiązania
Wcześniej dowiedziałeś się, że wiązanie twarde to jedna ze strategii uniemożliwiających wywołaniu funkcji zastosowanie rozwiązania awaryjnego w postaci reguły wiązania domyślnego. Dzieje się tak, ponieważ w przypadku wiązania twardego mamy do czynienia z wymuszeniem użycia określonego this
(o ile nie będzie wykorzystane słowo kluczowe new do nadpisania this). Problem polega na tym, że wiązanie twarde znacznie zmniejsza elastyczność
funkcji i uniemożliwia ręczne nadpisanie this za pomocą wiązania niejawnego
lub nawet w trakcie kolejnych prób wiązania jawnego.
44

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Byłoby dobrze, gdyby istniał sposób pozwalający na dostarczenie innej wartości
domyślnej (niż global lub undefined) dla wiązania domyślnego przy jednoczesnym pozostawieniu funkcji możliwości zmiany wiązania this za pomocą
techniki wiązania niejawnego lub wiązania jawnego.
Możemy utworzyć funkcję narzędziową nazywaną wiązaniem miękkim, która
będzie emulowała wymienione powyżej zachowanie:
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this,
curried = [].slice.call( arguments, 1 ),
bound = function bound() {
return fn.apply(
(!this ||
(typeof window !== "undefined" &&
this === window) ||
(typeof global !== "undefined" &&
this === global)
) ? obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
Przedstawiona powyżej funkcja softBind(..) działa podobnie jak wbudowana w ES5 funkcja pomocnicza bind(..), z wyjątkiem oczekiwanego przez nas
zachowania w postaci wiązania miękkiego. Wskazana funkcja zostaje opakowana w logikę sprawdzającą this w chwili wywołania. Jeżeli this prowadzi
do global lub undefined, używany jest przygotowany wcześniej alternatywny
obiekt domyślny (obj). W przeciwnym razie this pozostaje niezmodyfikowane.
Ponadto omawiana tutaj funkcja pomocnicza dostarcza również alternatywne
rozwinięcie funkcji (patrz wcześniejszy fragment poświęcony bind(..)).
Oto przykład użycia przygotowanej funkcji pomocniczej softBind(..):
function foo() {
console.log("nazwa: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
Wyjątki dotyczące wiązań
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

45
0
var fooOBJ = foo.softBind( obj );
fooOBJ(); // nazwa: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // nazwa: obj2 <---- spójrz!!!
fooOBJ.call( obj3 ); // nazwa: obj3 <---- spójrz!
setTimeout( obj2.foo, 10 );
// nazwa: obj <---- rozwiązanie awaryjne polegające na użyciu wiązania miękkiego.
Przedstawiona powyżej wersja funkcji foo() może mieć ręcznie zmieniony element, do którego odwołuje się this, na przykład obj2 lub obj3, jak przedstawiłem w kodzie. W sytuacji, w której standardowo jest używane wiązanie domyślne,
omawiana funkcja stosuje rozwiązanie awaryjne, czyli odwołanie this do obj.
Leksykalne this
Zwykłe funkcje stosują się do czterech omówionych wcześniej reguł. Jednak w specyfikacji ES6 wprowadzono specjalny rodzaj funkcji, która nie używa wymienionych reguł — jest to tak zwana arrow function (funkcja z użyciem strzałek).
Funkcje typu arrow function nie są oznaczane za pomocą słowa kluczowego
function, ale przez tak zwany operator fat arrow, czyli =>. Zamiast czterech
standardowych reguł funkcja typu arrow function podczas określania wiązania this adaptuje je z zakresu nadrzędnego (funkcji lub zakresu globalnego).
Poniżej przedstawiłem zakres leksykalny funkcji typu arrow function:
function foo() {
// Zwrot funkcji typu arrow function.
return (a) => {
// Tutaj 'this' pod względem leksykalnym dziedziczy po 'foo()'.
console.log( this.a );
};
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
46

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, nie 3!
Utworzona w foo() funkcja typu arrow function w chwili jej wywołania leksykalnie przechwytuje wiązanie this zdefiniowane dla foo(). Ponieważ w funkcji foo() wiązanie this prowadzi do obj1, bar (odniesienie zwrócone przez
funkcję typu arrow function) również ma wiązanie this wskazujące obj1. Takie
leksykalne wiązanie funkcji typu arrow function nie może być nadpisane nawet
za pomocą słowa kluczowego new!
Najczęściej spotykany przypadek użycia to prawdopodobnie wywołania zwrotne,
takie jak procedury obsługi zdarzeń lub liczniki czasu:
function foo() {
setTimeout(() => {
// Tutaj 'this' pod względem leksykalnym dziedziczy po 'foo()'.
console.log( this.a );
},100);
}
var obj = {
a: 2
};
foo.call( obj ); // 2
Chociaż funkcje typu arrow function stanowią atrakcyjną alternatywę dla
użycia bind(..) w celu wywołania funkcji ze ściśle określonym this, to jednak trzeba zwrócić uwagę na jeden bardzo ważny szczegół: w zasadzie wyłączają tradycyjny mechanizm this, zastępując go znacznie rozleglejszym zakresem leksykalnym. W specyfikacji wcześniejszej niż ES6 mieliśmy dość
często stosowany wzorzec przeznaczony do tego celu, który jest praktycznie
nie do odróżnienia od wprowadzonych w ES funkcji typu arrow function:
function foo() {
var self = this; // Leksykalne przechwycenie 'this'.
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
Leksykalne this
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

47
0
Choć wydaje się, że self = this i funkcje typu arrow function są dobrymi
rozwiązaniami w przypadku chęci uniknięcia wywołania bind(..), to jednak
w praktyce oznaczają ucieczkę przed mechanizmem this zamiast próby jego
poznania i wykorzystania.
Jeżeli często korzystasz z podobnych rozwiązań, musisz mieć świadomość, że tym
samym odrzucasz mechanizm this i zastępujesz go leksykalnym self = this
lub sztuczkami z arrow function. W takim przypadku rozważ:
1. Użycie jedynie zakresu leksykalnego. Zapomnij o pozorach tworzenia kodu
w stylu this.
2. W pełni wykorzystaj mechanizm this, co oznacza użycie wywołania bind(..),
gdy to konieczne, a także unikanie sztuczek, takich jak self = this i funkcje
typu arrow function, które zapewniają leksykalne this.
Program może efektywnie używać obu stylów kodu (leksykalnego i this), ale
wewnątrz tej samej funkcji i w podobnych sytuacjach mieszanie dwóch mechanizmów to zwykle prosta droga do problemów z kodem.
Podsumowanie
Określenie wiązania this dla wykonywanej funkcji wymaga odszukania bezpośredniego źródła wywołania funkcji. Po przeanalizowaniu kodu dla źródła
wywołania funkcji można zastosować cztery reguły — dokładnie w podanej
kolejności:
1. Czy funkcja została wywołana za pomocą new? W takim przypadku użyj
nowo skonstruowanego obiektu.
2. Czy funkcja została wywołana za pomocą call() bądź apply() (lub
bind())? W takim przypadku użyj podanego obiektu.
3. Czy funkcja została wywołana wraz z obiektem kontekstu zawierającym
dane wywołanie? W takim przypadku użyj tego obiektu kontekstu.
4. Domyślnie zastosowana zostanie wartość undefined w trybie ścisłym,
a w przeciwnym razie — obiekt global.
48

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Zachowaj ostrożność, aby przypadkowo nie wywołać reguły wiązania domyślnego. Jeżeli chcesz w bezpieczny sposób zignorować wiązanie this, obiekt
DMZ, taki jak ø = Object.create(null), będzie skutecznie chronił obiekt
global przed nieoczekiwanymi efektami ubocznymi.
Zamiast czterech standardowych reguł wiązania wprowadzone w specyfikacji
ES6 funkcje typu arrow function używają zakresu leksykalnego dla wiązania
this. Oznacza to, że wspomniane funkcje dziedziczą wiązanie this (niezależnie
od jego rodzaju) po wywołaniu funkcji nadrzędnej. Dlatego też pod względem
syntaktycznym funkcje typu arrow function mogą być uznawane za odpowiednik self = this stosowany w kodzie przed wprowadzeniem specyfikacji ES6.
Podsumowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

49
0
50

Rozdział 2. To wszystko ma teraz sens!
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
ROZDZIAŁ 3.
Obiekty
W rozdziałach 1. i 2. wyjaśniłem, że wiązanie this prowadzi do różnych obiektów, w zależności od źródła wywołania funkcji. Mógłbyś w tym miejscu zapytać: czym dokładnie jest obiekt i dlaczego trzeba go wskazywać? Właśnie
tym zajmiemy się w tym rozdziale.
Składnia
Obiekt jest dostępny w dwóch postaciach: deklaracyjnej (literalnej) i konstrukcyjnej.
Literalna składnia obiektu przedstawia się następująco:
var myObj = {
key: value
// …
};
Z kolei obiekt skonstruowany prezentuje się jak poniżej:
var myObj = new Object();
myObj.key = value;
Formy konstrukcyjna i literalna powodują powstanie dokładnie tego samego
rodzaju obiektu. Jedyna różnica polega na możliwości umieszczenia w deklaracji literalnej jednej lub większej liczby par klucz-wartość, podczas gdy
w przypadku obiektów konstruowanych właściwości muszą być dodawane
pojedynczo.
51
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Postać konstrukcyjna jest niezwykle rzadko stosowana do tworzenia obiektów w pokazany powyżej sposób. Praktycznie zawsze używana jest składnia w postaci literalnej. To samo dotyczy
większości wbudowanych obiektów (więcej na ten temat dowiesz się w dalszej części rozdziału).
Typ
Obiekty są ogólnymi elementami konstrukcyjnymi, na bazie których zbudowano większość języka JavaScript. Obiekt to jeden z sześciu typów podstawowych (w specyfikacji nazywanych typami języka) w JavaScript:
 string,
 number,
 boolean,
 null,
 undefined,
 object.
Zwróć uwagę na pewien fakt: typy proste (string, boolean, number, null i un
defined) same w sobie nie są obiektami. Wprawdzie typ null jest czasami
określany jako typ obiektu, ale to niewłaściwe podejście wynikające z błędu
w języku, który powoduje że polecenie typeof null nieprawidłowo (i myląco)
zwraca ciąg tekstowy object. Tak naprawdę null jest oddzielnym typem prostym.
Bardzo często można się spotkać z nieprawdziwym stwierdzeniem, że „wszystko w JavaScript jest obiektem”. Nie ulega wątpliwości, że wymienione zdanie
jest nieprawdziwe.
Istnieje także kilka specjalnych podtypów obiektów, które określamy jako typy
złożone.
Z kolei function to podtyp obiektu (technicznie rzecz biorąc, jest to obiekt
możliwy do wywołania). Mówi się, że funkcje w JavaScript są „pierwszorzędne”,
ponieważ w zasadzie to zwykłe obiekty (wraz z semantyką pozwalającą na ich
wywoływanie), a więc mogą być obsługiwane w dokładnie taki sam sposób
jak każdy inny zwykły obiekt.
52

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Tablica to również pewna forma obiektu, choć wyposażona w dodatkowe
możliwości. Organizacja zawartości tablicy jest znacznie bardziej ustrukturyzowana niż w przypadku ogólnych obiektów.
Obiekty wbudowane
Istnieje kilka innych podtypów obiektu, zwykle określanych mianem obiektów
wbudowanych. W przypadku niektórych nazwa wyraźnie pokazuje bezpośrednie powiązanie z odpowiednikiem wśród typów prostych. Jednak w rzeczywistości relacje są znacznie bardziej skomplikowane (wkrótce powrócimy
do tego zagadnienia).
Obiekty wbudowane:
 String,
 Number,
 Boolean,
 Object,
 Function,
 Array,
 Date,
 RegExp,
 Error
wyglądają na rzeczywiste typy, a nawet klasy, o ile posłużymy się podobieństwem do innych języków programowania, na przykład Javy i jej klasy String.
Jednak w JavaScript są to po prostu wbudowane funkcje. Każda z nich może
być użyta w charakterze konstruktora (to znaczy wywołania funkcji wraz
z operatorem new, patrz rozdział 2.), a wynikiem jest nowo skonstruowany
obiekt podanego podtypu. Na przykład:
var strPrimitive = "To jest ciąg tekstowy.";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // Fałsz.
var strObject = new String( "To jest ciąg tekstowy." );
typeof strObject; // "object"
strObject instanceof String; // Prawda.
// Sprawdzenie podtypu obiektu.
Object.prototype.toString.call( strObject ); // [object String]
Typ
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

53
0
W dalszej części rozdziału szczegółowo przedstawię sposób działania wywołania
Object.prototype.toString(), ale teraz wystarczy wiedzieć, że można sprawdzić wewnętrzny podtyp za pomocą domyślnej metody bazowej toString().
Wynik działania metody ujawnia, że strObject jest obiektem faktycznie utworzonym przez konstruktor String.
Wartość prosta "To jest ciąg tekstowy." nie jest obiektem, ale literałem
typu prostego i jednocześnie niemodyfikowalną wartością. W celu przeprowadzenia operacji na tego rodzaju wartości — na przykład sprawdzenia wielkości, uzyskania dostępu do poszczególnych znaków itd. — konieczne jest
użycie obiektu String.
Na szczęście język automatycznie przeprowadza koercję typu prostego string
na obiekt String, gdy zachodzi taka potrzeba. To oczywiście oznacza, że prawie
nigdy nie trzeba wyraźnie tworzyć formy Object. Większość użytkowników
JavaScript zdecydowanie preferuje użycie formy literalnej wartości, gdy tylko
istnieje taka możliwość, zamiast formy skonstruowanego obiektu.
Spójrz na poniższy fragment kodu:
var strPrimitive = "To jest ciąg tekstowy.";
console.log( strPrimitive.length ); // 22
console.log( strPrimitive.charAt( 3 ) ); // "j"
W obu przypadkach wywołujemy metodę lub właściwość względem typu prostego (tutaj string), a silnik JavaScript automatycznie przeprowadza jej koercję
na obiekt String. Dlatego też operacja dostępu do właściwości i metoda działają
zgodnie z oczekiwaniami.
Ten sam rodzaj koercji jest przeprowadzany względem literalnej liczby typu
prostego, na przykład wartości 42, i obiektu opakowania new Number(42) podczas użycia metod takich jak 42.359.toFixed(2). Podobnie wygląda to w przypadku obiektów Boolean utworzonych na podstawie typu prostego boolean.
Dla typów prostych null i undefined nie ma obiektów opakowania, a jedynie
wartości wymienionych typów prostych. Z kolei wartości takie jak Date mogą
być tworzone wyłącznie za pomocą formy skonstruowanych obiektów, ponieważ nie mają odpowiedników w postaci form literalnych.
Object, Array, Function i RegExp (wyrażenia regularne) są obiektami niezależnie
od użytej formy. Jednak forma skonstruowanego obiektu w pewnych przypadkach
54

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
oferuje więcej opcji podczas tworzenia obiektu niż dostępne w trakcie użycia
formy literalnej. Ponieważ obiekty mogą być tworzone na dowolny z wymienionych sposobów, wybierana jest niemal zawsze prostsza forma literalna.
Formy skonstruowanego obiektu używaj tylko wtedy, jeśli zachodzi potrzeba
skorzystania z dodatkowych opcji.
Obiekty Error są rzadko wyraźnie tworzone w kodzie, najczęściej odbywa się
to automatycznie w trakcie zgłaszania wyjątków. Wprawdzie obiekt Error
można utworzyć za pomocą formy skonstruowanego obiektu new Error(..),
ale najczęściej jest to niepotrzebne.
Zawartość obiektu
Jak wcześniej wspomniałem, zawartość obiektu składa się z wartości (dowolnego typu) przechowywanych w nazwanych lokalizacjach, które określamy
mianem właściwości.
Trzeba koniecznie zwrócić uwagę na pewien fakt. Kiedy mówimy o zawartości, zwykle mamy na myśli te wartości, które faktycznie są przechowywane
wewnątrz obiektu, ale to tylko pozory. Silnik przechowuje wartości w sposób
zależny od implementacji i dlatego równie dobrze może nie przechowywać
wartości w pewnych kontenerach obiektów. Natomiast w kontenerze są przechowywane nazwy właściwości, które działają w charakterze wskaźników
(technicznie rzecz biorąc, są to odwołania) do miejsc rzeczywistego przechowywania wartości.
Spójrz na poniższy fragment kodu:
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2
W celu uzyskania dostępu do wartości znajdującej się w lokalizacji a obiektu
myObject konieczne jest użycie operatora . lub [ ]. Składnia .a jest zwykle
określana jako „dostęp do właściwości”, podczas gdy składnia ["a"] jako „dostęp do klucza”. W rzeczywistości obie zapewniają uzyskanie dostępu do tej
samej lokalizacji i pobiorą tę samą wartość (tutaj 2), a więc oba wymienione
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

55
0
wyrażenia mogą być używane zamiennie. Od tego miejsca w książce będę stosował pojęcie „dostęp do właściwości”, które jest znacznie bardziej rozpowszechnione.
Podstawowa różnica między dwoma wymienionymi składniami polega na tym,
że operator . wymaga podania nazwy właściwości zgodnej z Identifier, podczas gdy składnia [".."] może pobrać jako nazwę właściwości praktycznie
dowolny ciąg tekstowy zgodny z UTF-8/Unicode. Na przykład w celu uzyskania dostępu do właściwości o nazwie "Super-Fun!" należy użyć składni
["Super-Fun!"], ponieważ Super-Fun! nie jest poprawną nazwą właściwości
zgodną z Identifier.
Ponadto, skoro składnia [".."] używa wartości ciągu tekstowego do podania
lokalizacji, aplikacja może w sposób programowy przygotować wartość ciągu
tekstowego, na przykład:
var myObject = {
a: 2
};
var idx;
if (wantA) {
idx = "a";
}
// Później.
console.log( myObject[idx] ); // 2
Nazwy właściwości w obiektach zawsze są ciągami tekstowymi. Jeżeli dla nazwy właściwości użyjesz innej wartości niż string (typ prosty), w pierwszej
kolejności zostanie ona przekonwertowana na ciąg tekstowy. To dotyczy także
liczb, które są bardzo często używane w charakterze indeksów tablic. Dlatego
też zachowaj ostrożność i nie pomyl liczb z obiektami i tablicami:
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
56

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Nazwy obliczanych właściwości
Omówiona powyżej składnia myObject[..] dostępu do właściwości jest użyteczna, jeśli konieczne będzie użycie wartości obliczanego wyrażenia jako nazwy
klucza, na przykład myObject[prefiks + nazwa]. Jednak nie okaże się zbyt pomocna w trakcie deklarowania obiektów za pomocą składni literalnego obiektu.
W specyfikacji ES6 wprowadzono nazwy właściwości obliczanych pozwalających na podanie wyrażenia w nawiasie kwadratowym, umieszczanego w miejscu nazwy klucza w deklaracji literalnego obiektu:
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "Witaj,",
[prefix + "baz"]: "świecie!"
};
myObject["foobar"]; // Witaj,
myObject["foobaz"]; // świecie!
Najczęstszym sposobem wykorzystania nazw właściwości obliczanych będzie
prawdopodobnie typ Symbol, wprowadzony w specyfikacji ES6, którego jednak nie omawiam w tej książce. Pokrótce — jest to nowy typ prosty danych,
który ma przeciwną, niemożliwą do odgadnięcia wartość (technicznie rzecz
biorąc, to wartość string). Będziesz usilnie zniechęcany do pracy z rzeczywistą wartością typu Symbol (teoretycznie ta wartość może być inna w różnych
silnikach JavaScript). Dlatego też stosowana będzie nazwa Symbol, taka jak
Symbol.Cokolwiek (to tylko nazwa!).
var myObject = {
[Symbol.Something]: "Witaj, świecie!"
};
Właściwości kontra metody
Niektórzy programiści lubią dokonywać rozróżnienia podczas rozważania dostępu do właściwości w obiekcie, jeżeli pobierana wartość okazuje się funkcją.
Ponieważ kuszące może być potraktowanie funkcji jako należącej do obiektu,
a poza tym w innych językach programowania funkcje należące do obiektów
(czyli „klas”) są określane mianem „metod”, często można się spotkać ze stosowaniem określenia „dostęp do metody” zamiast „dostęp do właściwości”.
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

57
0
Co interesujące, w specyfikacji również istnieje wspomniane rozróżnienie.
Pod względem technicznym funkcje nigdy nie „należą” do obiektów. Dlatego
też automatyczne określanie mianem „metody” funkcji, która po prostu jest
wywoływana w obiekcie, wydaje się przykładem naciągania semantyki.
To prawda, że pewne funkcje zawierają odwołania do this, a czasami wspomniane odwołania mogą prowadzić do obiektów w źródle wywołania funkcji.
Jednak taki sposób użycia danej funkcji nie powoduje, że jest ona bardziej
metodą niż inne funkcje, ponieważ wiązanie this jest tworzone dynamicznie
w trakcie działania aplikacji, w źródle wywołania funkcji. Dlatego też relacja
z obiektem pozostaje w najlepszym razie tylko pośrednia.
Za każdym razem, gdy uzyskujesz dostęp do właściwości w obiekcie, masz
do czynienia z dostępem do właściwości, niezależnie od typu otrzymanej
wartości zwrotnej. Jeżeli skutkiem operacji dostępu do właściwości jest funkcja, w tym momencie nie stanie się ona w magiczny sposób metodą. Nie ma
nic specjalnego (poza możliwością utworzenia wiązania niejawnego this,
jak to wcześniej wyjaśniłem) w funkcji pochodzącej z operacji dostępu do
właściwości.
Na przykład:
function foo() {
console.log( "foo" );
}
var someFoo = foo; // Odwołanie zmiennej do 'foo'.
var myObject = {
someFoo: foo
};
foo; // function foo(){..}
someFoo; // function foo(){..}
myObject.someFoo; // function foo(){..}
W powyższym fragmencie kodu someFoo i myObject.someFoo to dwa oddzielne odwołania do tej samej funkcji. Żadne z nich nie wskazuje na nic specjalnego ani nic będącego „własnością” innego obiektu. Jeżeli funkcja foo() zawiera wewnątrz odwołanie this, niejawne wiązanie myObject.someFoo będzie
58

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
jedyną możliwą do zaobserwowania różnicą między tymi dwoma odwołaniami do tej samej funkcji. Nie ma sensu, aby którekolwiek z tych odwołań
nazywać metodą.
Prawdopodobnie znajdą się osoby przekonane, że funkcja staje się metodą
nie w trakcie jej definiowania, ale podczas działania programu, a dokładnie
w chwili jej wywołania, w zależności od jego sposobu (czy będzie to wywołanie
z kontekstem obiektu, czy bez niego — więcej informacji na ten temat znajdziesz w rozdziale 2.). Jednak nawet taka interpretacja jest nieco naciągana.
Najbezpieczniej będzie przyjąć założenie, że pojęcia „funkcja” i „metoda” są
używane w języku JavaScript wymiennie.
Specyfikacja ES6 zawiera nowe odwołanie super, które zwykle jest
używane wraz ze słowem kluczowym class (patrz dodatek A).
Sposób zachowania odwołania super (wiązanie statyczne zamiast późnego, jak w przypadku this) jeszcze bardziej podkreśla wagę tego, że funkcja wraz z odwołaniem super jest bardziej
metodą niż funkcją. To jednak tylko subtelne niuanse semantyczne (i mechaniczne).
Nawet po zadeklarowaniu wyrażenia funkcji jako części literału obiektu ta
funkcja w sposób magiczny nie będzie bardziej należeć do obiektu. Nadal istnieje po prostu wiele odwołań do tego samego obiektu funkcji:
var myObject = {
foo: function foo() {
console.log( "foo" );
}
};
var someFoo = myObject.foo;
someFoo; // function foo(){..}
myObject.foo; // function foo(){..}
W rozdziale 6. przedstawię wprowadzony w specyfikacji ES6
skrót dla składni foo: function foo() { .. } w literale obiektu.
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

59
0
Tablice
Tablice również używają formy dostępu [ ], ale jak wcześniej wspomniałem,
charakteryzują się znacznie bardziej strukturalną organizacją sposobu i miejsca przechowywania wartości (choć wciąż nie ma żadnych ograniczeń dotyczących typu przechowywanych wartości). W tablicach stosowane są indeksy
liczbowe, co oznacza, że wartości są przechowywane w lokalizacjach zwykle
nazywanych indeksami i określanych nieujemnymi liczbami całkowitymi,
takimi jak 0 lub 42:
var myArray = [ "foo", 42, "bar" ];
myArray.length; // 3
myArray[0]; // "foo"
myArray[2]; // "bar"
Tablice są obiektami i pomimo że poszczególne indeksy są dodatnimi liczbami całkowitymi, to wciąż istnieje możliwość dodania właściwości do tablicy:
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
Zwróć uwagę na fakt, że dodanie nazwanych właściwości (niezależnie od użytej
składni operatora . lub [ ]) nie zmienia podawanej wielkości tablicy.
Istnieje możliwość użycia tablicy jako zwykłego obiektu klucz-wartość bez dodawania do niej żadnych indeksów liczbowych, ale to kiepski pomysł, ponieważ tablice są zoptymalizowane pod kątem ich typowego przeznaczenia,
podobnie jak w przypadku zwykłych obiektów. Dlatego też używaj obiektów
do przechowywania par klucz-wartość, natomiast tablic do przechowywania
wartości w indeksach liczbowych.
Zachowaj ostrożność: jeżeli spróbujesz dodać właściwość do tablicy, ale nazwa
właściwości będzie się prezentowała jak liczba, zostanie ona potraktowana jako
numeryczny indeks (a tym samym zmodyfikuje zawartość tablicy):
60

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
Powielanie obiektów
Jedną z funkcji najczęściej interesujących programistów, którzy dopiero poznają JavaScript, jest powielanie obiektu. Do tego celu powinna być dostępna
wbudowana metoda copy(), prawda? Okazuje się jednak, że zadanie jest nieco bardziej skomplikowane, ponieważ nie do końca jest jasne, jaki algorytm
powinien zostać użyty w trakcie powielania obiektu.
Spójrz na przykład na poniższy obiekt:
function anotherFunction() { /*..*/ }
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // Odwołanie, a nie kopia!
c: anotherArray, // Inne odwołanie!
d: anotherFunction
};
anotherArray.push( anotherObject, myObject );
W jaki dokładnie sposób powinna zostać przedstawiona kopia myObject?
Przede wszystkim należy odpowiedzieć na pytanie o rodzaj kopii: czy jest
płytka, czy głęboka. Kopia płytka powoduje utworzenie nowego obiektu a jako
kopii wartości 2. Natomiast właściwości b, c i d są po prostu odwołaniami do
tych samych miejsc znajdujących się w pierwotnym obiekcie. Z kolei kopia
głęboka powoduje powielenie nie tylko obiektu myObject, ale również another
Object i anotherArray. Następnie pojawia się problem: obiekt anotherArray
zawiera odwołania do anotherObject i myObject, więc powinny być one powielone. Niestety mamy tylko odwołania do nich. W ten sposób dochodzimy
do problemu nieustannego powielania z powodu odwołań cyklicznych.
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

61
0
Czy należy wykryć stan odwołań cyklicznych i po prostu przerwać łańcuch,
pozostawiając przy tym najgłębiej umieszczony element nie w pełni powielony?
Czy powinien zostać wygenerowany błąd? A może trzeba zastosować jakieś
rozwiązanie pośrednie?
Co więcej, nie jest do końca jasne, co oznacza termin „funkcja powielająca”.
Możemy skorzystać z różnych obejść, takich jak serializacja toString() w kodzie źródłowym funkcji (który różni się między poszczególnymi implementacjami i nie jest niezawodny we wszystkich silnikach w zależności od typu
analizowanej funkcji).
Jak można udzielić odpowiedzi na te wszystkie trudne pytania? W poszczególnych frameworkach JavaScript ich autorzy zdecydowali się na własne interpretacje i podjęli pewne decyzje projektowe. Jednak które (o ile w ogóle)
interpretacje powinny być zaadaptowane w JavaScript jako standard? Przez
długi czas nie było jasnej odpowiedzi na to pytanie.
Jedno z rozwiązań opiera się na założeniu, że obiekty, których można bezpieczne używać wraz z formatem JSON (to znaczy mogą być serializowane
na ciągi tekstowe JSON, a następnie przywracane jako obiekty o takiej samej
strukturze i wartościach), mogą być łatwo powielone za pomocą poniższego
polecenia:
var newObj = JSON.parse( JSON.stringify( someObj ) );
Oczywiście konieczne jest upewnienie się, że obiekt może być bezpiecznie
stosowany wraz z formatem JSON. W pewnych sytuacjach będzie to wręcz
trywialne, z kolei w innych — całkowicie niewystarczające.
Jednocześnie koncepcja płytkiej kopii pozostaje całkiem zrozumiała i powoduje znacznie mniej problemów. Dlatego też w specyfikacji ES6 znalazło się
zdefiniowane wywołanie Object.assign(..), przeznaczone właśnie do tworzenia płytkiej kopii. Wywołanie Object.assign(..) jako pierwszy parametr
pobiera obiekt docelowy, natomiast kolejnymi parametrami są jeden lub
większa liczba obiektów źródłowych. Wywołanie przeprowadza iterację przez
wszystkie widoczne w typach wyliczeniowych klucze (patrz poniższy fragment
kodu) obiektu źródłowego i kopiuje je (tylko za pomocą przypisania operatora =)
do obiektu docelowego. Wartością zwrotną jest (mamy nadzieję) obiekt docelowy,
jak można zobaczyć poniżej:
62

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // Prawda.
newObj.c === anotherArray; // Prawda.
newObj.d === anotherFunction; // Prawda.
W kolejnej sekcji przedstawię deskryptory właściwości (czyli
cechy charakterystyczne właściwości) i pokażę użycie wywołania
Object.defineProperty(..). Jednak powielenie przeprowadzane przez Object.assign(..) jest wyłącznie przypisaniem
w stylu =, więc wszelkie specjalne cechy charakterystyczne właściwości (takie jak writable) w obiekcie źródłowym nie zostaną
zachowane w obiekcie docelowym.
Deskryptory właściwości
Przed wprowadzeniem specyfikacji ES5 język JavaScript nie oferował żadnego
bezpośredniego sposobu rozróżniania z poziomu kodu cech charakterystycznych właściwości, na przykład czy dana właściwość jest tylko do odczytu.
Jednak począwszy od ES5, wszystkie właściwości są opisywane w kategoriach
deskryptora właściwości.
Spójrz na poniższy fragment kodu:
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
Jak możesz zobaczyć, deskryptor właściwości (nazywany deskryptorem danych, ponieważ przechowuje jedynie dane) dla naszej właściwości a zwykłego
obiektu to znacznie więcej niż tylko jego wartość wynosząca 2. Właściwość
zawiera także trzy inne cechy charakterystyczne określone jako writable,
enumerable i configurable.
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

63
0
Wprawdzie widać wartości domyślne przypisywane cechom charakterystycznym właściwości podczas jej tworzenia, ale za pomocą wywołania Object.de
fineProperty(..) można dodać nową właściwość lub zmodyfikować istniejącą (o ile cecha configurable ma wartość true!) zgodnie z oczekiwaniami.
Na przykład:
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
Za pomocą defineProperty(..) w wyraźny sposób ręcznie dodaliśmy zwykłą,
normalną właściwość a do obiektu myObject. Jednak podejście ręczne nie jest
stosowane, o ile nie zachodzi potrzeba modyfikacji cech charakterystycznych
deskryptora w celu określenia zachowania innego niż domyślne.
Writable
Możliwość zmiany wartości właściwości jest kontrolowana przez cechę charakterystyczną o nazwie writable.
Spójrz na poniższy fragment kodu:
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // Brak możliwości zmiany wartości!
configurable: true,
enumerable: true
} );
myObject.a = 3;
myObject.a; // 2
Jak możesz zobaczyć, próba modyfikacji wartości (value) zakończyła się cichym niepowodzeniem. Po przejściu do trybu ścisłego (strict mode) otrzymamy błąd:
64

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // Brak możliwości zmiany wartości!
configurable: true,
enumerable: true
} );
myObject.a = 3; // Błąd TypeError.
Błąd TypeError wskazuje na brak możliwości zmiany wartości właściwości.
Metodami typu getter i setter zajmiemy się już wkrótce. Możesz
jednak zaobserwować, że writable: false oznacza brak możliwości zmiany wartości, co pod pewnymi względami jest odpowiednikiem zdefiniowania metody typu setter, która nie wykonuje operacji. W rzeczywistości nieprzeprowadzająca zapisu
metoda typu setter będzie musiała zgłosić błąd TypeError podczas próby jej wywołania, aby tym samym była w pełni zgodna
z writable: false.
Configurable
Jeżeli właściwość pozwala na jej konfigurację, można zmodyfikować jej definicję deskryptora za pomocą tej samej funkcji pomocniczej o nazwie define
Property(..):
var myObject = {
a: 2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty( myObject, "a", {
value: 4,
writable: true,
configurable: false, // Brak możliwości konfiguracji!
enumerable: true
} );
myObject.a; // 4
myObject.a = 5;
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

65
0
myObject.a; // 5
Object.defineProperty( myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true
} ); // Błąd TypeError.
Ostatnie wywołanie defineProperty(..) powoduje powstanie błędu TypeError
niezależnie od tego, czy jest używany tryb ścisły, jeśli podejmujesz próbę zmiany
definicji deskryptora we właściwości niepozwalającej na konfigurację. Zachowaj
ostrożność: jak możesz zobaczyć, zmiana w configurable wartości na false
jest operacją jednokrotną, która nie może być cofnięta!
Mamy jeszcze pewien wyjątek, którego istnienia trzeba być
świadomym: nawet jeśli właściwość jest aktualnie określona
jako configurable:false, to wartość writable zawsze można
zmienić z true na false i nie spowoduje to błędu, ale nie
można zrobić tego w drugą stronę, to znaczy zmienić z false
na true.
Kolejną restrykcją nakładaną przez configurable:false jest brak możliwości
użycia operatora delete w celu usunięcia istniejącej właściwości:
var myObject = {
a: 2
};
myObject.a; // 2
delete myObject.a;
myObject.a; // Wartość undefined.
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: true
} );
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
Jak możesz zobaczyć w powyższym fragmencie kodu, ostatnie wywołanie
delete zakończyło się (cichym) niepowodzeniem, ponieważ właściwość a nie
pozwala na zmianę konfiguracji.
66

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Operator delete jest używany jedynie do usunięcia właściwości obiektu (które
mogą być usunięte) bezpośrednio z danego obiektu. Jeżeli właściwość obiektu
jest ostatnim pozostającym odwołaniem do pewnego obiektu lub funkcji, jej
usunięcie za pomocą delete powoduje także wykasowanie wspomnianego
odwołania. W takim przypadku wskazywany przez to odwołanie obiekt lub
funkcja mogą być później usunięte przez mechanizm usuwania nieużytków.
To jednak nie oznacza, że można traktować operator delete jako narzędzie
do zwalniania zaalokowanej pamięci, jak ma to miejsce w innych językach
programowania (na przykład w C i C++). Operator delete jest przeznaczony
jedynie do usuwania właściwości obiektu.
Enumerable
Ostatnią (wprawdzie są jeszcze dwie inne, ale omówimy je pokrótce przy okazji poznawania metod typu getter i setter) omawianą tutaj cechą charakterystyczną deskryptora jest enumerable.
Nazwa prawdopodobnie nie pozostawia wątpliwości co do przeznaczenia tej
cechy charakterystycznej — jest nim określenie, czy właściwość będzie widoczna w pewnych typach wyliczeniowych, takich jak pętla for-in. Przypisanie enumerable wartości false powoduje, że właściwość nie będzie widoczna
we wspomnianych typach wyliczeniowych, nawet jeśli pozostaje w pełni dostępna. Z kolei przypisanie wartości true powoduje uwzględnienie właściwości
przez typy wyliczeniowe.
Wszystkie normalne właściwości zdefiniowane przez użytkownika mają domyślnie przypisaną wartość true dla enumerable, co prawdopodobnie będzie
oczekiwanym zachowaniem. Jeżeli masz właściwość specjalną, którą chcesz
ukryć przed typem wyliczeniowym, ustaw w niej enumerable:false.
Do tematu widoczności właściwości w typach wyliczeniowych wkrótce jeszcze
powrócimy, przygotuj się więc na kontynuację omawiania tego zagadnienia.
Niemodyfikowalność
Czasami zachodzi konieczność, aby pewne właściwości lub obiekty pozostawały niemodyfikowalne (przypadkowo lub celowo). W specyfikacji ES5 dodano obsługę modyfikowalności na wiele różnych sposobów.
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

67
0
Trzeba koniecznie zwrócić uwagę na fakt, że wszystkie omówione tutaj podejścia
powodują utworzenie płytkiej niemodyfikowalności. Oznacza to, że wpływają
jedynie na obiekt i cechy charakterystyczne jego bezpośrednich właściwości.
Jeżeli obiekt zawiera odwołanie do innego obiektu (tablicy, obiektu, funkcji
itd.), zawartość tego obiektu pozostaje bez zmian:
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]
W powyższym fragmencie kodu przyjąłem założenie, że obiekt myImmutableObject
został już utworzony i jest chroniony jako niemodyfikowalny. Jednak aby ochronić zawartość myImmutableObject.foo (czyli oddzielny obiekt — tablicę), należy ustawić foo jako obiekt niemodyfikowalny za pomocą jednej lub większej
liczby omówionych poniżej funkcjonalności.
W programach JavaScript często tworzy się głęboko zagnieżdżone niemodyfikowalne obiekty. Niewątpliwie niekiedy mają
miejsce wyjątkowe sytuacje, które wymagają zastosowania tego
rodzaju podejścia. Jednak ogólnie rzecz biorąc, jeśli okaże się,
że musisz szczelnie zamknąć lub zamrozić wszystkie obiekty,
warto zrobić krok wstecz w celu ponownego przeanalizowania
projektu programu i zmodyfikować go w taki sposób, aby zapewniał większą niezawodność w przypadku potencjalnych zmian
w wartościach obiektów.
Stała obiektu
Dzięki połączeniu writable:false i configurable:false można w zasadzie
utworzyć stałą (brak możliwości zmiany, przedefiniowania czy usunięcia) będącą właściwością obiektu. Przykład pokazałem poniżej:
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
68

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Uniemożliwienie rozszerzania
Jeżeli chcesz uniemożliwić dodawanie nowych właściwości do obiektu i jednocześnie pozostawić w spokoju pozostałe właściwości danego obiektu, wywołaj Object.preventExtensions(..):
var myObject = {
a: 2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // Wartość undefined.
Jeżeli nie został włączony tryb ścisły, próba utworzenia właściwości b zakończy się cichym niepowodzeniem. Z kolei w trybie ścisłym nastąpi zgłoszenie
błędu TypeError.
Szczelne opakowanie
Wywołanie Object.seal(..) powoduje utworzenie „szczelnie opakowanego”
obiektu. W praktyce oznacza to pobranie istniejącego obiektu i wywołanie
w nim Object.preventExtensions(..), a także oznaczenie wszystkich istniejących właściwości jako configurable:false.
Dlatego też uniemożliwiasz nie tylko dodawanie kolejnych właściwości, ale
również ponowną konfigurację lub usunięcie istniejących właściwości (choć
nadal można modyfikować ich wartości).
Zamrożenie
Wywołanie Object.freeze(..) tworzy zamrożony obiekt. W praktyce oznacza
to pobranie istniejącego obiektu i wywołanie w nim Object.seal(..), a także
oznaczenie wszystkich właściwości dostępu do danych jako writable:false,
co uniemożliwia zmianę ich wartości.
Takie podejście jest najwyższym poziomem niemodyfikowalności, który można
zastosować względem obiektu. Uniemożliwia on wprowadzenie jakichkolwiek zmian w obiekcie oraz wszystkich jego bezpośrednich właściwościach
(choć jak wcześniej wspomniałem, zawartość innych obiektów, do których
prowadzą odwołania, pozostaje niezmieniona).
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

69
0
Istnieje możliwość głębokiego zamrożenia obiektu przez wywołanie w nim
Object.freeze(..), a następnie przeprowadzenie rekurencyjnej iteracji przez
wszystkie (pozostające dotąd nienaruszone) obiekty, do których prowadzą
odwołania, i również wywołanie w nich Object.freeze(..). Zachowaj jednak
ostrożność, ponieważ powyższe działanie może mieć wpływ na inne (współdzielone) obiekty, których nie chciałeś zamrażać.
[[Get]]
Istnieje pewien subtelny, choć ważny szczegół dotyczący działania właściwości
dostępu. Spójrz na poniższy fragment kodu:
var myObject = {
a: 2
};
myObject.a; // 2
W powyższym kodzie myObject.a to właściwość dostępu, ale nie sprawdza
jedynie obiektu myObject w poszukiwaniu właściwości o nazwie a, jak mogłoby się wydawać.
Zgodnie ze specyfikacją przedstawiony powyżej fragment kodu w rzeczywistości
przeprowadza operację [[Get]] (coś w rodzaju wywołania funkcji [[Get]]())
w obiekcie myObject. Domyślna wbudowana operacja [[Get]] w obiekcie
najpierw sprawdza obiekt w poszukiwaniu właściwości o podanej nazwie
i, jeśli ją znajdzie, zwraca odpowiednią wartość.
Jednak algorytm [[Get]] definiuje jeszcze inne ważne zachowanie podejmowane w momencie, gdy nie zostanie znaleziona właściwość o podanej nazwie.
W rozdziale 5. dowiesz się, co się stanie później (przejście przez łańcuch
[[Prototype]], o ile taki istnieje).
Ważnym wynikiem operacji [[Get]] jest to, że jeśli nie będzie ona w stanie
odszukać żadnej wartości dla żądanej właściwości, zwróci wartość undefined:
var myObject = {
a: 2
};
myObject.b; // Wartość undefined.
70

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
To zachowanie jest odmienne od tego, w którym do zmiennych odwołujemy się
za pomocą ich identyfikatorów. W przypadku niemożliwego do rozwiązania
w akceptowalnym zakresie leksykalnym odwołania do zmiennej wynikiem nie
będzie wartość undefined, jak ma to miejsce w przypadku właściwości obiektu,
ale raczej nastąpi zgłoszenie błędu ReferenceError:
var myObject = {
a: undefined
};
myObject.a; // Wartość undefined.
myObject.b; // Wartość undefined.
Z perspektywy wartości nie ma żadnej różnicy między dwoma wymienionymi
odwołaniami — w obu przypadkach wynikiem jest wartość undefined. Jednak
operacja [[Get]] w tle, choć na pierwszy rzut oka subtelna, potencjalnie przeprowadza nieco więcej „pracy” w przypadku odwołania myReference.b niż
w przypadku odwołania myObject.a.
Analizując jedynie otrzymane wartości, nie można stwierdzić, czy właściwość
istnieje i faktycznie przechowuje wartość undefined, czy jednak właściwości
nie ma, a undefined to wartość domyślna zwrócona przez operację [[Get]]
z powodu braku możliwości zwrotu innej wartości. Wkrótce przekonasz się,
że można odróżnić dwa wymienione scenariusze.
[[Put]]
Ponieważ istnieje wewnętrznie zdefiniowana operacja [[Get]] przeznaczona
do pobierania wartości z właściwości, oczywiste jest, że mamy także domyślną
operację [[Put]].
Uznanie, że przypisanie wartości właściwości obiektu spowoduje wywołanie
operacji [[Put]] w celu utworzenia lub ustawienia danej właściwości, może
być kuszące, jednak sytuacja jest nieco bardziej skomplikowana.
W trakcie wywołania operacji [[Put]] jej zachowanie będzie uzależnione od
wielu różnych czynników, przede wszystkim od tego, czy właściwość już istnieje w obiekcie (ten czynnik ma największe znaczenie).
Jeżeli właściwość znajduje się w obiekcie, wówczas algorytm [[Put]] przeprowadzi następujące sprawdzenie:
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

71
0
1. Czy właściwość jest deskryptorem akcesora (patrz sekcja „Metody typu
getter i setter” w dalszej części rozdziału)? Jeżeli tak, należy wywołać
metodę typu setter, o ile istnieje.
2. Czy właściwość jest deskryptorem danych wraz z wartością false dla
writable? Jeżeli tak, następuje cicha awaria w trybie innym niż ścisły,
natomiast w trybie ścisłym zgłaszany jest błąd TypeError.
3. W przeciwnym razie następuje przypisanie wartości do istniejącej właściwości w zwykły sposób.
Jeżeli w danym obiekcie właściwość jeszcze nie istnieje, operacja [[Put]] staje
się nieco bardziej skomplikowana. Do takiego scenariusza powrócimy w rozdziale 5. podczas analizy [[Prototype]], co powinno nieco rozjaśnić sytuację.
Metody typu getter i setter
Domyślne operacje [[Put]] i [[Get]] w obiektach zachowują pełną kontrolę
nad sposobem przypisywania wartości istniejących lub nowych właściwości,
a także nad pobieraniem wartości z istniejących właściwości.
Być może w przyszłości, dzięki bardziej zaawansowanym funkcjonalnościom języka, będzie można nadpisywać domyślne operacje [[Put]] i [[Get]] dla całego obiektu (a nie jedynie dla
poszczególnych właściwości). Ten temat wykracza jednak poza zakres tematyczny książki i być może zostanie omówiony
w kolejnych pozycjach z serii Tajniki języka JavaScript.
Specyfikacja ES5 wprowadziła możliwość nadpisania części wspomnianych
operacji domyślnych nie na poziomie obiektu, ale na poziomie właściwości —
za pomocą metod typu getter i setter. Getter to właściwość, która w rzeczywistości wywołuje ukrytą funkcję w celu pobrania wartości. Z kolei setter
to właściwość, która tak naprawdę wywołuje ukrytą funkcję w celu przypisania wartości.
Podczas definiowania właściwości, aby zawierała ona getter, setter lub obie
możliwości, jej definicja staje się deskryptorem akcesora (przeciwieństwem
deskryptora danych). W przypadku deskryptorów akcesora cechy charakterystyczne value i writable są ignorowane. Zamiast nich JavaScript uwzględnia
cechy set i get właściwości, a także configurable i enumerable.
72

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Spójrz na poniższy fragment kodu:
var myObject = {
// Zdefiniowanie gettera dla 'a'.
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // Element docelowy.
"b",
// Nazwa właściwości.
{
// Deskryptor.
// Zdefiniowanie gettera dla 'b'.
get: function(){ return this.a * 2 },
// Upewniamy się, że 'b' będzie właściwością obiektu.
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
Niezależnie od tego, czy korzystamy ze składni literalnego obiektu (get a()
{ .. }), czy też z wyraźnej definicji za pomocą defineProperty(..), dochodzi do utworzenia w obiekcie właściwości nieprzechowującej w rzeczywistości
wartości. Próba uzyskania dostępu do tej właściwości automatycznie spowoduje wywołanie ukrytej funkcji getter, która zwróci wartość będącą wynikiem
operacji dostępu do danej właściwości:
var myObject = {
// Zdefiniowanie gettera dla 'a'.
get a() {
return 2;
}
};
myObject.a = 3;
myObject.a; // 2
Ponieważ dla a zdefiniowaliśmy jedynie getter, późniejsza próba przypisania
wartości a spowoduje, że operacja set nie zgłosi błędu, ale zakończy się cichym odrzuceniem przypisania. Nawet jeśli setter będzie poprawny, nasz niestandardowy getter ma zdefiniowany na stałe zwrot jedynie wartości 2, a tym
samym przeprowadzanie operacji set nie ma sensu.
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

73
0
Aby rozwiązanie było bardziej sensowne, właściwości powinny być definiowane
wraz z setterami, które nadpisują domyślną operację [[Put]] (czyli przypisanie)
dla poszczególnych właściwości zgodnie z oczekiwaniami. Praktycznie zawsze
będziesz deklarował zarówno getter, jak i setter (zdefiniowanie tylko jednego
z nich często prowadzi do nieoczekiwanego lub zaskakującego zachowania):
var myObject = {
// Zdefiniowanie gettera dla 'a'.
get a() {
return this._a_;
},
// Zdefiniowanie gettera dla 'a'.
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
W omówionym przykładzie podana wartość 2 przypisania
(operacja [[Put]] ) była w rzeczywistości przechowywana
w innej zmiennej _a_. Nazwa zmiennej jest jedynie konwencją
i nie ma żadnego specjalnego wpływu na jej zachowanie — to
zwykła właściwość, jak każda inna.
Istnienie
Pokazałem wcześniej, że dostęp do właściwości takiej jak myObject.a może się
zakończyć otrzymaniem wartości undefined, jeżeli przechowywana jest wyraźna wartość undefined lub właściwość w ogóle nie istnieje. A skoro wartość
pozostaje taka sama w obu przypadkach, to jak można je rozróżnić?
Istnieje możliwość sprawdzenia, czy obiekt zawiera określoną właściwość, bez
pobierania wartości tej właściwości:
var myObject = {
a: 2
};
("a" in myObject); // Prawda.
("b" in myObject); // Fałsz.
myObject.hasOwnProperty( "a" ); // Prawda.
myObject.hasOwnProperty( "b" ); // Fałsz.
74

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Operator in sprawdzi, czy właściwość znajduje się w obiekcie lub też na jakimkolwiek wyższym poziomie łańcucha [[Prototype]] (patrz rozdział 5.).
Z kolei wywołanie hasOwnProperty(..) sprawdza jedynie, czy myObject ma
właściwość, ale nie analizuje łańcucha [[Prototype]]. W rozdziale 5. powrócimy do ważnych różnic między tymi dwoma operacjami, wtedy też dokładniej
przeanalizujemy mechanizm [[Prototype]].
Wywołanie hasOwnProperty(..) jest dostępne dla wszystkich zwykłych obiektów za pomocą delegowania do Object.prototype (patrz rozdział 5.). Istnieje
jednak możliwość utworzenia obiektu pozbawionego połączenia z Object.pro
totype — to się odbywa za pomocą (omówionego również w rozdziale 5.)
wywołania Object.create(null). W takim przypadku wywołanie metody typu
myObject.hasOwnProperty(..) zakończy się niepowodzeniem.
W omawianym scenariuszu najbardziej niezawodny sposób przeprowadzenia
operacji sprawdzenia to wywołanie Object.prototype.hasOwnProperty.call
(myObject,"a"), które pożycza bazową metodę hasOwnProperty(..) i używa
wiązania jawnego (patrz rozdział 2.) względem myObject.
Może się wydawać, że operator in sprawdzi istnienie wartości
wewnątrz kontenera, ale tak naprawdę sprawdza istnienie nazwy właściwości. O tej różnicy trzeba koniecznie pamiętać
w kontekście tablic, ponieważ próba sprawdzenia za pomocą
wywołania 4 in [2, 4, 6] nie będzie działała zgodnie z oczekiwaniami.
Uwzględnienie w typach wyliczeniowych
Wcześniej, podczas omawiania cechy charakterystycznej enumerable deskryptora właściwości, pokrótce wyjaśniłem problem widoczności w typach
wyliczeniowych. Powróćmy jeszcze do tego zagadnienia i przeanalizujmy je
nieco dokładniej:
var myObject = { };
Object.defineProperty(
myObject,
"a",
// Właściwość 'a' będzie uwzględniona w typach wyliczeniowych, jak zwykle.
{ enumerable: true, value: 2 }
);
Zawartość obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

75
0
Object.defineProperty(
myObject,
"b",
// Właściwość 'b' nie będzie uwzględniona w typach wyliczeniowych.
{ enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // Prawda.
myObject.hasOwnProperty( "b" ); // Prawda.
// ……
for (var k in myObject) {
console.log( k, myObject[k] );
}
// "a" 2
Zwróć uwagę na pewien fakt: myObject.b istnieje i ma wartość, do której
można uzyskać dostęp. Mimo wszystko wymieniona właściwość nie jest
uwzględniana przez pętlę for-in (choć, co zaskakujące, jej istnienie jest ujawniane przez operator in). Widoczność w typach wyliczeniowych oznacza po
prostu, że dana właściwość zostanie uwzględniona w przypadku iteracji przez
właściwości obiektu.
Pętle for-in zastosowane w tablicach mogą być źródłem nieoczekiwanych wyników, ponieważ widoczność tablicy w typach
wyliczeniowych nie zawiera jedynie wszystkich liczbowych indeksów, ale również wszystkie właściwości, które mają przypisaną wartość true dla enumerable. Dlatego też dobrym rozwiązaniem będzie użycie pętli for-in tylko dla obiektów, natomiast
do iteracji tablic korzystaj z pętli for wraz z iteracją indeksów
liczbowych.
Spójrz na jeszcze inny sposób, na jaki można rozróżnić właściwości widoczne
i niewidoczne w typach wyliczeniowych:
var myObject = { };
Object.defineProperty(
myObject,
"a",
// Właściwość 'a' będzie uwzględniona w typach wyliczeniowych, jak zwykle.
{ enumerable: true, value: 2 }
);
76

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Object.defineProperty(
myObject,
"b",
// Właściwość 'b' nie będzie uwzględniona w typach wyliczeniowych.
{ enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable( "a" ); // Prawda.
myObject.propertyIsEnumerable( "b" ); // Fałsz.
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]
Wywołanie propertyIsEnumerable(..) sprawdza, czy podana nazwa właściwości istnieje bezpośrednio w obiekcie oraz czy została ona zdefiniowana jako
enumerable:true.
Wartością zwrotną wywołania Object.keys(..) jest tablica wszystkich właściwości widocznych w typach wyliczeniowych, podczas gdy wywołanie Object.getOwnPropertyNames(..) zwraca tablicę wszystkich właściwości, niezależnie od tego, czy są widoczne w typach wyliczeniowych, czy nie.
Podczas gdy różnica w działaniu in i hasOwnProperty(..) wiąże się ze
sprawdzeniem (lub jego brakiem) łańcucha [[Prototype]], oba wywołania
Object.keys(..) i Object.getOwnPropertyNames(..) sprawdzają jedynie
bezpośrednio podany obiekt.
Aktualnie nie ma żadnego wbudowanego w JavaScript sposobu na pobranie
listy wszystkich właściwości, które będą odpowiednikiem zbioru sprawdzanego
przez in (sprawdzenie wszystkich właściwości całego łańcucha [[Prototype]],
jak to dokładnie wyjaśnię w rozdziale 5.). Działanie tego rodzaju narzędzia
można zasymulować za pomocą rekurencyjnego poruszania się po łańcuchu
[[Prototype]] obiektu, a następnie przechwytywania na każdym poziomie listy
właściwości wygenerowanej przez wywołanie Object.keys(..), które zwraca
jedynie właściwości widoczne w typach wyliczeniowych.
Iteracja
Pętla for-in przeprowadza iterację przez listę właściwości widocznych w typach
wyliczeniowych w obiekcie (uwzględniając także jego łańcuch [[Prototype]]).
Co zrobić w sytuacji, gdy chcesz jedynie przeprowadzić iterację przez wartości?
Iteracja
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

77
0
W przypadku tablic zindeksowanych liczbowo iteracja przez wartości jest
zwykle przeprowadzana za pomocą standardowej pętli for, na przykład:
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log( myArray[i] );
}
// 1 2 3
Powyższy kod nie przeprowadza iteracji przez wartości, a jedynie indeksy, gdy
używany jest indeks w odwołaniu do wartości, jak myArray[i].
W specyfikacji ES5 dodano kilka funkcji pomocniczych do przeprowadzania
iteracji przez tablice. Przykładami wspomnianych funkcji są forEach(..),
every(..) i some(..). Każda z nich akceptuje funkcję wywołania zwrotnego
stosowaną dla każdego elementu tablicy, a ich działanie różni się jedynie sposobem reakcji na wartość otrzymaną z wywołania zwrotnego.
Funkcja forEach(..) przeprowadza iterację przez wszystkie wartości w tablicy i ignoruje wszelkie wartości zwrócone przez wywołania zwrotne. Z kolei
funkcja every(..) działa aż do końca lub do chwili, gdy wartością zwróconą
przez wywołanie zwrotne będzie false. Natomiast działanie some(..) jest
kontynuowane aż do końca lub do chwili, gdy wartością zwróconą przez wywołanie zwrotne będzie true.
Te specjalne wartości zwrotne wewnątrz wywołań every(..) i some(..) działają
na zasadzie podobnej do polecenia break wewnątrz zwykłej pętli for, ponieważ
powodują wcześniejsze zatrzymanie iteracji, zanim zostanie przeprowadzona
do końca.
Jeżeli przeprowadzasz iterację przez obiekt za pomocą pętli for-in, wartości
pobierasz jedynie pośrednio, ponieważ tak naprawdę masz do czynienia z iteracją przez właściwości obiektu widoczne w typach wyliczeniowych. Dostęp
do właściwości w celu pobrania ich wartości musi się odbyć ręcznie.
W przeciwieństwie do iteracji przez liczbowe indeksy tablicy
(na przykład w pętli for lub w inny sposób), kolejność iteracji przez
właściwości obiektu nie może być zagwarantowana i może być
różna w poszczególnych silnikach JavaScript. W trakcie wykonywania zadań wymagających zachowania spójności między różnymi
środowiskami uruchomieniowymi nie opieraj się na zaobserwowanej kolejności, ponieważ jest ona bardzo niepewna i zmienna.
78

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Co można zrobić w sytuacji, gdy zachodzi potrzeba bezpośredniej iteracji
przez wartości, a nie indeksy tablicy (lub właściwości obiektu)? Na szczęście
w specyfikacji ES6 dodano składnię pętli for-of przeznaczonej do iteracji
przez tablice (i obiekty, o ile obiekt definiuje własny niestandardowy iterator):
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1
// 2
// 3
Pętla for-of prosi o podanie iteratora obiektu (z wewnętrznej domyślnej
funkcji znanej jako @@iterator) iterowanego elementu, a następnie przeprowadza iterację przez kolejne wartości zwrotne uzyskane na skutek wywoływania metody next() obiektu, jednokrotnie dla każdej iteracji pętli.
Tablice mają wbudowaną funkcję @@iterator, a więc pętla for-of bez problemów działa w pokazany sposób. Spróbujmy teraz przeprowadzić ręczną
iterację przez tablicę za pomocą wbudowanej funkcji @@iterator, aby przekonać się, jak to działa:
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next();
it.next();
it.next();
it.next();
// { value:1, done:false }
// { value:2, done:false }
// { value:3, done:false }
// { done:true }
Wewnętrzną właściwość obiektu pobieramy w @@iterator za
pomocą oferowanej przez ES6 składni Symbol: Symbol.iterator.
Semantykę typu Symbol pokrótce przedstawiłem we wcześniejszej
części rozdziału (patrz sekcja „Nazwy obliczanych właściwości”)
— tutaj mamy dokładnie takie samo uzasadnienie jej użycia.
Do właściwości specjalnych zawsze należy się odwoływać za
pomocą nazwy odwołania Symbol zamiast przy użyciu wartości specjalnej, która może być przechowywana przez tę właściwość. Ponadto pomimo nazwy (@@iterator) nie jest to sam
obiekt iteratora, ale funkcja zwracająca jego obiekt — to bardzo
subtelny, ale jednocześnie ważny szczegół!
Iteracja
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

79
0
Jak można zobaczyć w poprzednim fragmencie kodu, wartość zwrotna pochodząca z wywołania next() jest obiektem w postaci { value: .., done: .. },
gdzie value to wartość bieżącej iteracji, natomiast done to wartość boolowska
wskazująca, czy istnieją jeszcze jakiekolwiek elementy do iteracji.
Zauważ, że została zwrócona wartość 3 wraz z done:false, co na pierwszy
rzut oka wydaje się dziwne. Konieczne jest wywołanie funkcji next() po raz
czwarty (co pętla for-of w poprzednim fragmencie kodu robi automatycznie), aby otrzymać wynik done:true i tym samym faktycznie poinformować
o zakończeniu iteracji. Powód takiego zachowania wykracza poza zakres tematyczny omawianego tutaj zagadnienia, ale mogę Ci powiedzieć, że wiąże
się z semantyką funkcji generatorów w ES6.
Kiedy tablica automatycznie przeprowadza iterację w pętli for-of, zwykłe
obiekty nie mają wbudowanej funkcji @@iterator. Powody tego celowego
pominięcia są znacznie bardziej skomplikowane i nie będziemy się nimi tutaj
zajmować. Ogólnie rzecz biorąc, zdecydowanie lepiej jest nie dodawać żadnej
implementacji, która mogłaby się okazać problematyczna w przyszłych typach obiektów.
Istnieje możliwość zdefiniowania własnej funkcji @@iterator dla dowolnego
obiektu, który ma być poddawany iteracji. Na przykład:
var myObject = {
a: 2,
b: 3
};
Object.defineProperty( myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function() {
var o = this;
var idx = 0;
var ks = Object.keys( o );
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.length)
};
}
};
}
80

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
} );
// Ręczna iteracja przez 'myObject'.
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// Iteracja przez 'myObject' za pomocą pętli 'for-of'.
for (var v of myObject) {
console.log( v );
}
// 2
// 3
Użyliśmy wywołania Object.defineProperty(..) w celu zdefiniowania własnej funkcji @@iterator (moglibyśmy między
innymi sprawić, by nie była ona widoczna w typach wyliczeniowych), ale wykorzystanie Symbol jako nazwy właściwości obliczonej (zostały omówione we wcześniejszej części rozdziału)
pozwala na zadeklarowanie jej bezpośrednio, na przykład
var myObject = { a:2, b:3, [Symbol.iterator]: function()
{ /* .. */ } }.
Za każdym razem, gdy pętla for-of wywołuje funkcję next() iteratora obiektu
myObject, wewnętrzny wskaźnik przesuwa się do przodu i zwraca kolejną wartość z listy właściwości obiektu (patrz przedstawiona nieco wcześniej uwaga
dotycząca kolejności iteracji wartości lub właściwości obiektu).
Przedstawiona powyżej iteracja to prosty przykład iteracji „wartość po wartości”,
ale oczywiście można definiować dowolnie skomplikowane iteracje przeznaczone dla niestandardowych typów danych. Własne iteratory w połączeniu
z wprowadzoną w specyfikacji ES6 pętlą for-of stanowią nowe narzędzie
syntaktyczne o dużych możliwościach, przeznaczone do operacji na obiektach
zdefiniowanych przez użytkownika.
Na przykład lista obiektów Pixel (wraz z wartościami współrzędnych x i y)
może ustalić kolejność iteracji na podstawie liniowej odległości od punktu
początkowego (0,0) lub filtrować punkty, które są zbyt daleko itd. Jeśli iterator po zakończeniu iteracji zwraca z wywołań next() oczekiwaną wartość
w postaci { value: .. } oraz { done: true }, oferowana przez ES6 pętla
for-of będzie w stanie przeprowadzić iterację przez obiekty.
Iteracja
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

81
0
W rzeczywistości można nawet zdefiniować iteratory działające w nieskończoność, które nigdy nie zakończą pracy i zawsze będą zwracały nową wartość
(na przykład losowo wygenerowaną liczbę, inkrementowaną wartość, unikalny
identyfikator itd.). Jednak prawdopodobnie nie będziesz używać tego rodzaju
iteratorów bez ich powiązania z pętlą for-of, ponieważ w przeciwnym razie
nigdy się nie skończą i zawieszą działanie programu.
var randoms = {
[Symbol.iterator]: function() {
return {
next: function() {
return { value: Math.random() };
}
};
}
};
var randoms_pool = [];
for (var n of randoms) {
randoms_pool.push( n );
// Nie kontynuuj działania bez powiązania z pętlą for-of!
if (randoms_pool.length === 100) break;
}
Ten iterator będzie w nieskończoność generować losowo wybrane liczby,
a więc ostrożnie decydujemy się na pobranie jedynie stu wartości, aby nie zawiesić działania programu.
Podsumowanie
Obiekty w JavaScript mają dwie formy: literalną (na przykład var a = { .. })
i skonstruowaną (na przykład var a = new Array(..)). Preferowana jest zazwyczaj pierwsza z nich, choć forma obiektu skonstruowanego — w pewnych
przypadkach — oferuje więcej możliwości podczas tworzenia obiektu.
Wielu programistów błędnie twierdzi, że w JavaScript wszystko jest obiektem.
Obiekty to jeden z sześciu (a nawet siedmiu, w zależności od perspektywy)
typów prostych. Obiekty mają podtypy, między innymi function, a ponadto
mogą być wyspecjalizowane w pewnych zadaniach, na przykład [object Array]
to wewnętrzna etykieta przedstawiająca tablicę jako podtyp obiektu.
82

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Obiekty są kolekcjami par klucz-wartość. Do wartości, podobnie jak do właściwości, dostęp można uzyskać za pomocą składni .nazwaWłaściwości lub
["nazwaWłaściwości"]. W trakcie uzyskiwania dostępu do właściwości silnik
w rzeczywistości wywołuje domyślną wewnętrzną operację [[Get]] (lub [[Put]]
w przypadku ustawiania wartości), która nie tylko wyszukuje właściwość bezpośrednio w obiekcie, ale również sprawdza łańcuch [[Prototype]] (patrz
rozdział 5.), jeśli właściwość nie zostanie znaleziona w obiekcie.
Właściwości mają pewne cechy charakterystyczne, które mogą być kontrolowane za pomocą deskryptorów właściwości. Przykładem mogą być tutaj cechy
writable i configurable. Ponadto obiekty mogą mieć zdefiniowaną modyfikowalność (dotyczy to także właściwości obiektu), kontrolowaną na różnych poziomach za pomocą wywołań Object.preventExtensions(..), Object.seal(..)
i Object.freeze(..).
Właściwości nie muszą zawierać wartości — mogą być również właściwościami akcesora, czyli getterami i setterami. Ponadto właściwość może być zarówno widoczna, jak i niewidoczna w typach wyliczeniowych, co decyduje o jej
ewentualnym uwzględnieniu podczas iteracji przeprowadzanej na przykład za
pomocą pętli for-in.
Istnieje również możliwości iteracji wartości w strukturach danych (tablicach,
obiektach itd.) za pomocą składni pętli for-of wprowadzonej w specyfikacji ES6.
Wymieniona pętla szuka wbudowanej lub niestandardowej funkcji @@iterator
obiektu zawierającej metodę next() przeznaczoną do posuwania się do przodu
jednorazowo o jedną wartość danych.
Podsumowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

83
0
84

Rozdział 3. Obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
ROZDZIAŁ 4.
Mieszanie obiektów „klas”
Kontynuujemy rozpoczęty w poprzednim rozdziale temat obiektów. Oczywiście, teraz skierujemy naszą uwagę na programowanie zorientowane obiektowo
(ang. object-oriented programming, OOP) z użyciem klas. Najpierw przyjrzymy
się klasom jako wzorcom projektowym, a następnie przejdziemy do mechaniki
klas: utworzenia egzemplarza, dziedziczenia i (względnego) polimorfizmu.
Na podstawie materiału przedstawionego w rozdziale zobaczysz, że omówione koncepcje nie mają bezpośredniego przełożenia na mechanizm obiektowy
w JavaScript, ale wielu programistów JavaScript podejmuje pewne wysiłki, na
przykład domieszki (ang. mixin) itd., w celu pokonania problemów związanych
ze wspomnianym mapowaniem.
W tym rozdziale poświęcę całkiem sporą ilość miejsca (pierwsza
połowa!) na przedstawienie teorii programowania zorientowanego obiektowo. Ostatecznie omówione idee powiążę z konkretnymi fragmentami kodu JavaScript w drugiej połowie rozdziału, w której zajmiemy się mechanizmem domieszek. Jednak
na początku czeka Cię wiele koncepcji i pseudokodu, więc postaraj się nie zgubić i nadążać za mną!
Teoria klas
Klasa i dziedziczenie opisują pewną formę organizacji i architektury kodu —
sposób modelowania rzeczywistych problemów w oprogramowaniu domen.
W programowaniu obiektowym z danymi są nierozerwalnie powiązane funkcje przeprowadzające na nich operacje (oczywiście różne, w zależności od typu
i natury samych danych!). Dlatego też poprawny projekt powinien pakować
85
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
(inaczej hermetyzować) dane wraz ze sposobem zachowania względem tych
danych. W informatyce mówi się czasami o strukturze danych.
Na przykład seria znaków przedstawiających słowo lub wyrażenie zwykle jest
nazywana ciągiem tekstowym. Znaki są danymi. Jednak prawie nigdy nie przejmujesz się danymi, a najczęściej oczekujesz wykonania pewnych zadań względem danych. Oznacza to, że przygotowane sposoby zastosowania konkretnych operacji na danych (obliczenie długości ciągu tekstowego, dołączenie
kolejnych znaków, wyszukanie znaków itd.) są opracowane w postaci metod
klasy String.
Każdy ciąg tekstowy jest po prostu egzemplarzem klasy, czyli elegancko przygotowanym pakietem zawierającym dane w postaci znaków oraz zachowanie
w postaci funkcji, które mogą być wywołane względem tych danych.
Klasy określają także sposób tak zwanej klasyfikacji określonych struktur danych. W tym celu daną strukturę traktujemy jako konkretny wariant znacznie
ogólniejszej definicji bazowej.
Przeanalizujmy teraz proces klasyfikacji na podstawie często stosowanego
przykładu. I tak samochód (ang. car) można potraktować jako konkretną implementację znacznie ogólniejszej „klasy” rzeczy, nazwanej na przykład pojazd
(ang. vehicle).
W oprogramowaniu przedstawiony związek będziemy modelować poprzez
zdefiniowanie dwóch klas o nazwach Vehicle i Car.
Definicja klasy Vehicle może zawierać takie możliwości jak napęd (silnik itd.),
przewóz osób itd., które są rodzajem zachowania. Funkcjonalności zdefiniowane w klasie Vehicle mają zastosowanie dla wszystkich (lub większości) różnego
rodzaju typów pojazdów (samolot, pociąg, samochód itd.).
W przypadku oprogramowania nieustanne definiowanie funkcjonalności „możliwość przewozu osób” dla poszczególnych typów pojazdów nie ma sensu.
Zamiast tego definiujemy ją tylko jednokrotnie w klasie Vehicle, a następnie
podczas definiowania klasy Car po prostu wskazujemy, że dziedziczy (inaczej
rozszerza) ona po definicji bazowej zawartej w Vehicle. Mówimy więc, że Car
to specjalizowana wersja ogólnej definicji Vehicle.
Podczas gdy klasy Vehicle i Car wspólnie definiują zachowanie za pomocą
metod, dane w egzemplarzu klasy będą przedstawiały na przykład unikatowy
numer VIN konkretnego samochodu.
86

Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Dlatego też w programowaniu pojawiły się klasy, dziedziczenie i inicjalizacja.
Kolejną kluczową koncepcją w teorii klas jest polimorfizm. Oznacza on, że
ogólne zachowanie zdefiniowane w klasie nadrzędnej może być nadpisane
w klasie potomnej, aby jeszcze dokładniej wyrazić zadanie konieczne do wykonania. W rzeczywistości względny polimorfizm pozwala nam odwoływać
się do zachowania bazowego z poziomu zachowania nadpisanego.
Teoria klas silnie sugeruje, że klasy nadrzędna i potomna współdzielą tę samą
nazwę metody dla określonego zachowania, a więc metoda w klasie potomnej
nadpisuje tę w klasie nadrzędnej. Jak się wkrótce przekonasz, tego rodzaju
podejście zastosowane w kodzie JavaScript może doprowadzić do frustracji
i zawodnie działającego kodu.
Wzorzec projektowy klasy
Być może nigdy nie traktowałeś klasy jako wzorca projektowego, ponieważ
w przypadku programowania obiektowego bardzo często omawiane są wzorce
projektowe takie jak iterator, obserwator, fabryka, singleton itd. Przedstawiając zagadnienia w taki właśnie sposób, prawie zawsze przyjmuje się założenie,
że klasy w programowaniu zorientowanym obiektowo to mechanika niskiego
poziomu, za pomocą której implementujemy wszystkie (wysokiego poziomu)
wzorce projektowe, ponieważ programowanie obiektowe stanowi podstawę
dla całego (prawidłowego) kodu.
Jeśli jesteś doświadczonym programistą, mogłeś się już spotkać z programowaniem proceduralnym jako sposobem opisania kodu składającego się z procedur (funkcji) wywołujących inne funkcje bez żadnej dodatkowej abstrakcji
wysokiego poziomu. Być może dowiedziałeś się także, że klasy są prawidłowym
sposobem przekształcenia proceduralnego stylu „kodu spaghetti” na doskonale
uformowany i zorganizowany kod.
Oczywiście jeśli masz doświadczenie w programowaniu funkcjonalnym (konstruktor Monad itd.), doskonale wiesz, że klasy to tylko jeden z najczęściej
stosowanych wzorców projektowych. Jednak niektórzy być może po raz
pierwszy spotykają się z pytaniem, czy klasy to naprawdę absolutna podstawa
podczas tworzenia kodu. Czy istnieje jakakolwiek alternatywna abstrakcja na
bazie kodu?
Teoria klas
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

87
0
Niektóre języki programowania (na przykład Java) nie dają żadnego wyboru,
a więc w ogóle nie mamy warstwy opcjonalnej — wszystko jest klasą. Z kolei
w innych językach programowania, takich jak C/C++ i PHP, mamy składnię
dla programowania zarówno proceduralnego, jak i zorientowanego obiektowo.
Wybór odpowiedniego stylu lub decyzja o ich łączeniu należy do programistów.
„Klasy” JavaScript
Gdzie można umieścić JavaScript pod tym względem? Od całkiem długiego
czasu w języku znajdziesz pewne elementy syntaktyczne przypominające klasy
(na przykład new i instanceof), zaś w najnowszym wydaniu specyfikacji (ES6)
pojawiły się pewne nowości, na przykład słowo kluczowe class (patrz dodatek A).
Czy to w praktyce oznacza, że JavaScript naprawdę ma klasy? Odpowiem głośno
i wyraźnie: NIE!
Ponieważ klasy są wzorcem projektowym, przy pewnym wysiłku (jak się przekonasz w pozostałej części rozdziału) można zaimplementować funkcjonalność,
która mniej więcej przypomina klasy. Za pomocą składni przypominającej
klasy JavaScript próbuje spełnić niezwykle silnie wyrażane przez programistów
żądanie możliwości projektowania z użyciem klas.
Wprawdzie mamy składnię przypominającą klasy, ale korzystanie z niej oznacza tak naprawdę jedynie walkę z mechaniką JavaScript za pomocą wzorca
projektowego klas, ponieważ w tle budowany przez Ciebie mechanizm działa
zupełnie inaczej. Syntaktyczne sedno i (wyjątkowo często używane) biblioteki
„klas” JavaScript wykonują ciężką pracę, ukrywając przed Tobą rzeczywistość.
Jednak wcześniej czy później i tak odkryjesz, że klasy w innych językach programowania nie są takie same jak „klasy” w JavaScript.
Wszystko sprowadza się do tego, że klasy są opcjonalnym wzorcem podczas
projektowania oprogramowania. Masz więc wybór, czy chcesz ich używać
w kodzie JavaScript, czy jednak nie. Ponieważ wielu programistów jest silnie
przywiązanych do projektowania oprogramowania z użyciem podejścia zorientowanego obiektowo, pozostałą część rozdziału przeznaczam na omówienie sposobów obsługi oferowanej przez JavaScript iluzji klas oraz bolesnych
kwestii, które na pewno napotkasz.
88

Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Mechanika klas
W wielu językach programowania obiektowego biblioteka standardowa dostarcza strukturę danych stosu (umieszczanie danych na stosie, usuwanie danych
ze stosu itd.) w postaci klasy Stack. Wymieniona klasa zawiera wewnętrzny
zbiór zmiennych przechowujących dane, a także zbiór publicznie dostępnych
zachowań (metod) dostarczanych przez klasę. Dzięki metodom kod zyskuje
możliwość interakcji z (ukrytymi) danymi (na przykład dodawania danych czy
ich usuwania).
Jednak w tego rodzaju językach programowania tak naprawdę nie operujemy
bezpośrednio na klasie Stack (o ile nie skorzystamy z odwołania do statycznego elementu składowego, co wykracza poza zakres tematyczny omawianego
tutaj zagadnienia). Klasa Stack stanowi zaledwie abstrakcyjne wyjaśnienie
tego, co dowolny stos powinien robić, ale tak naprawdę sama w sobie nie
jest stosem. Konieczne jest utworzenie egzemplarza klasy Stack, aby otrzymać
element konkretnej struktury danych, względem którego będzie można przeprowadzać operacje.
Tworzenie
Przy wyjaśnianiu znaczenia klas i egzemplarzy często wykorzystuje się metaforę powstawania budynku.
Architekt opracowuje wszystkie cechy charakterystyczne budynku, wskazując
jego szerokość i wysokość, liczbę okien i ich położenie, a nawet rodzaj materiału używanego do budowy ścian i dachu. W tym momencie architekt niekoniecznie przejmuje się miejscem postawienia budynku czy liczbą budynków,
które zostaną zbudowane na podstawie danego projektu.
Ponadto architekt nie zajmuje się zawartością projektowanego budynku —
meblami, malowaniem ścian, innym wyposażeniem itd. — a jedynie typem
struktury budynku.
Architektoniczna matryca to zaledwie plan budynku. Ten plan tak naprawdę
nie oznacza budynku, do którego można wejść. Do rzeczywistego postawienia
budynku potrzebny jest wykonawca — budowniczy. Wykonawca bierze plany
i, dokładnie się do nich stosując, buduje zdefiniowany budynek. Można powiedzieć, że przy budowaniu kopiuje się cechy charakterystyczne z planu.
Mechanika klas
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

89
0
Po zakończeniu budowy budynek stanowi fizyczną wersję planu — mamy nadzieję, że w praktyce będzie to perfekcyjna kopia. Następnie wykonawca może
przejść do kolejnej pustej lokalizacji i ponownie wykonać wszystkie czynności,
tym samym tworząc kolejną kopię.
Związek między budynkiem i planem jest pośredni. Plan można przeanalizować
w celu dokładnego zrozumienia struktury budynku, ponieważ bezpośrednie
oględziny budynku mogą się okazać niewystarczające. Po otworzeniu drzwi
można wejść do środka budynku — plan zawiera tylko narysowane na kartce
papieru linie pokazujące miejsce, w którym powinny się znaleźć drzwi.
Klasa jest matrycą. W celu otrzymania rzeczywistego obiektu, z którym następnie można prowadzić interakcje, konieczne jest zbudowanie (inaczej
zainicjalizowanie) czegoś na podstawie klasy. Efektem końcowym tego rodzaju „konstrukcji” jest obiekt, zwykle nazywany egzemplarzem. W przygotowanym egzemplarzu można bezpośrednio wywoływać metody oraz uzyskiwać dostęp do wszystkich publicznych danych i właściwości, jeśli zachodzi
taka potrzeba.
Tego rodzaju obiekt stanowi kopię wszystkich cech charakterystycznych dostarczanych przez klasę.
Prawdopodobnie nie oczekujesz, że po wejściu do budynku znajdziesz na ścianie oprawiony w ramkę plan, na podstawie którego powstał dany budynek.
Wspomniany plan zwykle znajduje się w biurze, w dokumentacji dotyczącej
danego budynku. Podobnie jest z programowaniem. Ogólnie rzecz biorąc, obiekt
egzemplarza nie jest używany w celu bezpośredniego dostępu do jego klasy i operowania na niej, ale najczęściej możliwe jest przynajmniej określenie, na podstawie której klasy powstał dany egzemplarz.
Rozważmy teraz, jaki jest bezpośredni związek klasy z egzemplarzem obiektu
(pomijając pośredni związek między obiektem i klasą), na podstawie której
został utworzony. Na rysunku 4.1 pokazałem, że egzemplarz obiektu klasy powstaje na skutek operacji kopiowania.
Jak możesz zobaczyć, strzałki są skierowane od lewej do prawej strony oraz
od góry do dołu. To pokazuje zachodzącą operację kopiowania — zarówno
pod względem koncepcji, jak i fizycznie.
90

Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Rysunek 4.1. Schemat dziedziczenia obiektów
Konstruktor
Egzemplarze klas są konstruowane za pomocą metody specjalnej klasy, zwykle
o takiej samej nazwie jak klasa. Tę metodę określamy mianem konstruktora.
Zadaniem konstruktora jest zainicjalizowanie wszystkich informacji (stanu)
wymaganych przez egzemplarz.
Spójrz na przykład na poniższy pseudokod klasy:
class CoolGuy {
specialTrick = nothing
CoolGuy( trick ) {
specialTrick = trick
}
showOff() {
output( "Oto moja sztuczka: ", specialTrick )
}
}
W celu utworzenia egzemplarza CoolGuy konieczne jest wywołanie konstruktora klasy:
Janek = new CoolGuy( "skakanka" )
Janek.showOff() // Oto moja sztuczka: skakanka
Mechanika klas
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

91
0
Zwróć uwagę na to, że klasa CoolGuy ma konstruktor o nazwie CoolGuy(),
który jest wywoływany w instrukcji new CoolGuy(..). W wyniku wywołania
konstruktora otrzymujemy obiekt (egzemplarz klasy), a następnie możemy
już wywołać metodę showOff(), która wyświetli informacje dotyczące sztuczki
wykonywanej przez dany egzemplarz CoolGuy.
Konstruktor klasy należy do klasy i prawie zawsze ma taką samą nazwę jak
ona. Ponadto konstruktor niemal zawsze będzie wywoływany po użyciu słowa
kluczowego new, które informuje silnik języka, że chcesz utworzyć nowy (ang.
new) egzemplarz klasy.
Dziedziczenie klasy
W językach programowania zorientowanego obiektowo można nie tylko zdefiniować klasę przeznaczoną do samodzielnego zainicjowania, ale również definiować inne klasy dziedziczące po tej pierwszej.
Pierwszą klasę najczęściej określa się mianem klasy nadrzędnej, podczas gdy
drugą — klasą potomną. Te wyrażenia oczywiście są metaforą rodzica i dziecka,
choć jest ona nieco naciągana, o czym się wkrótce przekonasz.
Kiedy rodzic ma biologiczne dziecko, genetyczne cechy rodzica są przenoszone
na dziecko. Oczywiście w większości biologicznych systemów reprodukcji
mamy dwoje rodziców, którzy w równym stopniu dostarczają geny. Jednak
na potrzeby przytoczonej tutaj metafory przyjmujemy założenie, że istnieje
tylko jeden rodzic.
Po pojawieniu się dziecka jest ono odrębną jednostką. Wprawdzie dziecko
pozostaje pod silnym wpływem rodzica na skutek dziedziczenia po nim, ale
jest unikatowe i niezależne. Jeżeli dziecko będzie miało rude włosy, to nie
oznacza, że rodzic miał lub będzie miał rude włosy.
Podobna sytuacja zachodzi w programowaniu. Po zdefiniowaniu klasy potomnej pozostaje ona odrębna od jej klasy nadrzędnej. Klasa potomna zawiera początkową kopię zachowania z klasy nadrzędnej, ale wszelkie odziedziczone zachowanie może zostać nadpisane, a nawet można tworzyć nowe.
Trzeba koniecznie pamiętać, że mówimy tutaj o klasach nadrzędnej i potomnej, które nie są fizycznymi przedmiotami. W tym miejscu metafora rodzica
i dziecka może być nieco myląca, ponieważ powinniśmy powiedzieć, że klasa
92

Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
nadrzędna jest jak DNA rodzica, natomiast klasa potomna — jak DNA dziecka.
Na podstawie DNA rodziców w wyniku różnych procesów biochemicznych
powstaje nowy człowiek, z którym będzie można na przykład porozmawiać.
Odłóżmy na bok biologicznych rodziców oraz dzieci i posłużmy się innym
typowym przykładem: różnych rodzajów pojazdów.
Powracamy do rozpoczętej we wcześniejszej części rozdziału analizy dotyczącej klas Vehicle i Car. Spójrz na przedstawiony poniżej pseudokod dla dziedziczonych klas:
class Vehicle {
engines = 1
ignition() {
output( "Włączenie silnika." );
}
drive() {
ignition();
output( "Sterowanie i poruszanie się do przodu!" )
}
}
class Car inherits Vehicle {
wheels = 4
drive() {
inherited:drive()
output( "Toczenie się na wszystkich ", wheels, " kołach!" )
}
}
class SpeedBoat inherits Vehicle {
engines = 2
ignition() {
output( "Włączenie ", engines, " silników." )
}
pilot() {
inherited:drive()
output( "Poruszanie się z łatwością po wodzie!" )
}
}
Dziedziczenie klasy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

93
0
W celu zachowania jasności i zwięzłości w powyższym fragmencie kodu pominąłem konstruktory klas.
Zdefiniowaliśmy klasę Vehicle, aby dostarczyć silnik, możliwość włączenia
zapłonu i rozpoczęcia jazdy. Jednak nigdy nie będziesz tworzyć tylko ogólnego
pojazdu — na tym etapie to jedynie abstrakcyjna koncepcja.
Dlatego też zdefiniowaliśmy dwa konkretne rodzaje pojazdów: Car i SpeedBoat.
Oba wymienione pojazdy dziedziczą ogólne cechy charakterystyczne po Vehicle,
ale jednocześnie mają kolejne cechy charakterystyczne dla przedstawianego typu pojazdu. Samochód potrzebuje czterech kół, natomiast szybka łódź dwóch
silników, co oznacza konieczność zachowania większej uwagi i pamiętania
o włączeniu obu silników.
Polimorfizm
Klasa Car definiuje własną metodę drive() nadpisującą metodę o tej samej
nazwie odziedziczoną po klasie Vehicle. Jednak nawet wtedy metoda drive()
klasy Car wywołuje inherited:drive(), co wskazuje, że klasa Car posiada
odwołanie do pierwotnej, nienadpisanej i dziedziczonej metody drive() .
Metoda pilot() klasy SpeedBoat również zawiera odwołanie do dziedziczonej
metody drive().
Ta technika jest nazywana polimorfizmem lub wirtualnym polimorfizmem.
Biorąc pod uwagę omawiane tutaj zagadnienia, tę technikę będziemy nazywać
względnym polimorfizmem.
Polimorfizm to znacznie obszerniejszy temat, niż tutaj przedstawiłem, ale słowo
„względny” w tym przypadku oznacza odwołanie się do jednego konkretnego
aspektu: możliwości odwołania się z dowolnej metody do innej (o tej samej lub
innej nazwie) znajdującej się na wyższym poziomie hierarchii dziedziczenia.
Wcześniej użyłem słowa „względna”, ponieważ absolutnie nie definiujemy,
na którym poziomie dziedziczenia (inaczej klasy) chcemy uzyskać dostęp.
Zamiast tego odwołanie względne oznacza: „szukaj jeden poziom wyżej”.
W wielu językach programowania zamiast słowa kluczowego inherited pojawiającego się w powyższym kodzie używane jest słowo kluczowe super, oznaczające superklasę, czyli klasę nadrzędną w stosunku do bieżącej.
94

Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Kolejny aspekt polimorfizmu wiąże się z tym, że nazwa metody może mieć
wiele definicji na różnych poziomach hierarchii dziedziczenia. Wspomniane
definicje są automatycznie odpowiednio wybierane podczas określania metody,
która powinna zostać wywołana.
W poprzednim przykładzie miałeś okazję zobaczyć dwa wystąpienia omawianego zachowania: metoda drive() jest zdefiniowana w klasach Vehicle i Car,
natomiast ignition() w Vehicle i SpeedBoat.
W tradycyjnych językach programowania obiektowego za pomocą słowa kluczowego super konstruktor klasy potomnej
otrzymuje bezpośrednie odwołanie do konstruktora klasy nadrzędnej. Tak faktycznie jest, ponieważ w prawdziwych klasach
konstruktor należy do danej klasy. Jednak w JavaScript mamy
odwrotną sytuację — „klasa” należy do konstruktora (odwołania
typu Foo.prototype...). Ponieważ w JavaScript związek między
elementem potomnym i nadrzędnym istnieje tylko między dwoma obiektami .prototype odpowiednich konstruktorów, same
konstruktory nie są bezpośrednio powiązane. Dlatego też nie
ma prostego sposobu na zapewnienie względnego odwołania
między konstruktorami (zajrzyj do dodatku A, gdzie znajdziesz
informacje o wprowadzonym w specyfikacji ES6 słowie kluczowym class, które rozwiązuje problem z super).
Interesującą implikację związaną z polimorfizmem można wyraźnie dostrzec
w przypadku funkcji ignition(). Wewnątrz metody pilot() tworzymy odwołanie względnego polimorfizmu do (odziedziczonej) po klasie Vehicle wersji
metody drive(). Jednak w metodzie drive() do ignition() odwołujemy się
po prostu za pomocą nazwy (brak odwołania względnego).
Która wersja metody ignition() zostanie użyta przez silnik JavaScript? Czy
będzie to wersja pochodząca z klasy Vehicle, czy raczej ze SpeedBoat? Silnik
JavaScript skorzysta z metody ignition() zdefiniowanej w klasie SpeedBoat.
Jeżeli utworzymy egzemplarz klasy Vehicle, a następnie wywołamy jej metodę
drive(), silnik języka JavaScript wykorzysta metodę ignition() zdefiniowaną
w klasie Vehicle.
Ujmując rzecz jeszcze inaczej, definicja metody ignition() stosuje polimorfizm
(zmienia się) w zależności od klasy (poziomu dziedziczenia), do której następuje odwołanie egzemplarza.
Dziedziczenie klasy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

95
0
To może się wydawać szczegółem czysto akademickim, jednak jego zrozumienie jest niezbędne, aby zauważyć podobne (choć inne) zachowanie mechanizmu [[Prototype]] w JavaScript.
Kiedy klasy są dziedziczone, to właśnie one (a nie egzemplarze utworzone na
ich podstawie!) zachowują możliwość względnego odwołania się do klasy nadrzędnej. Takie względne odwołanie jest zwykle nazywane super.
Czy pamiętasz wykres przedstawiony we wcześniejszej części rozdziału
(rysunek 4.2)?
Rysunek 4.2. Schemat dziedziczenia obiektów
Zwróć uwagę na utworzone egzemplarze (a1, a2, b1 i b2) oraz dziedziczenie
(Bar). Strzałki wskazują na operację kopiowania.
Pod względem koncepcyjnym wydaje się, że klasa potomna Bar może uzyskać
dostęp do metody w klasie nadrzędnej Foo za pomocą odwołania względnego
polimorfizmu (czyli poprzez super). Jednak w rzeczywistości klasa potomna
otrzymuje jedynie kopię zachowania odziedziczonego po klasie nadrzędnej.
Jeżeli klasa potomna nadpisze dziedziczoną metodę, wówczas zarówno pierwotna, jak i nadpisana wersja metody będą faktycznie obsługiwane, a więc obie
pozostaną dostępne.
Nie pozwól, aby polimorfizm skłonił Cię do przyjęcia założenia, że klasa potomna
jest połączona z jej klasą nadrzędną. Klasa potomna otrzymuje z klasy nadrzędnej
kopię tego, co jest jej niezbędne. Dziedziczenie klas oznacza kopiowanie.
96

Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Dziedziczenie wielokrotne
Czy przypominasz sobie nasze wcześniejsze rozważania dotyczące DNA rodzica i dziecka? Stwierdziłem wówczas, że wymieniona metafora jest nieco
naciągana, ponieważ biologicznie dziecko ma zwykle dwoje rodziców. Jeżeli
klasa mogłaby dziedziczyć po dwóch innych klasach, wówczas tego rodzaju
sytuacja byłaby bliższa metaforze rodziców i dzieci.
W pewnych językach programowania obiektowego można wskazać więcej niż
tylko jedną klasę nadrzędną, po której będzie dziedziczyła klasa potomna.
Wielokrotne dziedziczenie oznacza, że definicje wszystkich klas nadrzędnych
są kopiowane do klasy potomnej.
Na pierwszy rzut oka wydaje się, że to wspaniałe udogodnienie oferujące
możliwość łączenia w klasie wielu różnych funkcjonalności. Jednak z tego rodzaju rozwiązaniem wiążą się pewne komplikacje. Jeżeli obie klasy nadrzędne
zawierają metodę o nazwie drive(), do której wersji metody będzie się odwoływać klasa potomna? Czy zawsze trzeba będzie ręcznie podawać, którą
metodę drive() mamy na myśli, a tym samym utracimy użyteczność dziedziczenia polimorficznego?
Istnieje jeszcze inny wariant przedstawionego problemu, który nosi nazwę
problemu diamentu. Odwołuje się on do sytuacji, gdy klasa potomna D dziedziczy po dwóch klasach nadrzędnych (B i C), z których każda dziedziczy po
klasie nadrzędnej A. Jeżeli klasa A zawiera metodę drive(), a obie klasy B i C ją
nadpisują (polimorfizm), to do której wersji metody drive() — B:drive()
czy C:drive() — odwołuje się klasa D (patrz rysunek 4.3)?
Rysunek 4.3. Graficzne przedstawienie problemu diamentu
Dziedziczenie klasy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

97
0
Problemy mogą być jeszcze poważniejsze, niż tutaj przedstawiłem. W tym
miejscu jedynie o nich wspominam, aby podkreślić kontrast ze sposobem działania mechanizmów JavaScript.
Język JavaScript jest prostszy: nie oferuje natywnego mechanizmu przeznaczonego dla dziedziczenia wielokrotnego. Wielu programistów uznaje to za
zaletę, ponieważ uniknięcie komplikacji jest cenniejsze niż „zredukowana”
funkcjonalność. To jednak nie wstrzymuje innych programistów przed próbami symulowania dziedziczenia wielokrotnego na wiele sposobów, o czym
przekonasz się w kolejnym podrozdziale.
Domieszki
Mechanizm obiektowy w JavaScript nie przeprowadza automatycznego kopiowania zachowania podczas dziedziczenia lub tworzenia egzemplarzy klas.
Ujmując rzecz najprościej, w języku JavaScript nie ma klas, a jedynie obiekty.
Z kolei obiekt nie jest kopiowany do innych, ponieważ obiekty są jedynie łączone ze sobą (więcej informacji na ten temat znajdziesz w rozdziale 5.).
Skoro zachowanie zaobserwowane w innych językach programowania wskazuje na kopiowanie, spróbujmy teraz przeanalizować, jak programiści starają się
zasymulować brakujące im zachowanie kopiowania klas w JavaScript, używając
do tego domieszek. Wyróżniamy dwa rodzaje domieszek: jawne i niejawne.
Jawne domieszki
Powróćmy jeszcze na chwilę do wcześniejszego przykładu klas Vehicle i Car.
Ponieważ JavaScript nie będzie automatycznie kopiować zachowania z Vehicle
do Car, możemy samodzielnie utworzyć przeznaczoną do tego funkcję pomocniczą. Tego rodzaju funkcja znajduje się w wielu bibliotekach oraz frameworkach i bardzo często jest nazywana extend(), ale na potrzeby omawianego
zagadnienia nadamy jej nazwę mixin(..):
// Niezwykle uproszczony przykład domieszki mixin(..):
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// Kopiowanie klucza tylko wtedy, gdy nie istnieje w obiekcie docelowym.
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
98

Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition: function() {
console.log( "Włączenie silnika." );
},
drive: function() {
this.ignition();
console.log( "Sterowanie i poruszanie się do przodu!" );
}
};
var Car = mixin( Vehicle, {
wheels: 4,
drive: function() {
Vehicle.drive.call( this );
console.log(
"Toczenie się na wszystkich " + this.wheels + " kołach!"
);
}
} );
Subtelny, ale jednocześnie ważny szczegół: nie zajmujemy
się dłużej klasami, ponieważ w języku JavaScript nie ma klas.
Vehicle i Car to po prostu obiekty; pierwszy jest obiektem źródłowym, a drugi utworzoną na jego podstawie kopią.
W tym momencie Car ma kopię właściwości i funkcji zdefiniowanych w Vehicle.
Pod względem technicznym funkcje tak naprawdę nie są powielane, a zamiast
tego następuje skopiowanie odwołań do funkcji. Dlatego też Car ma właściwość
o nazwie ignition, będącą skopiowanym odwołaniem do funkcji ignition(),
a także właściwość engines wraz ze skopiowaną z Vehicle wartością 1.
Obiekt Car miał już właściwość drive (funkcja), a więc odwołanie do właściwości nie zostało nadpisane (spójrz na polecenie if w przedstawionej wcześniej
funkcji mixin(..)).
Domieszki
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99

99
0
Powracamy do polimorfizmu
Przeanalizujmy następujące polecenie Vehicle.drive.call( this ). Mamy
tutaj do czynienia z czymś, co nazywam wyraźnym pseudopolimorfizmem.
Przypomnij sobie wcześniej przedstawiony pseudokod zawierający wiersz
inherited:drive(), który nazwałem względnym polimorfizmem.
JavaScript nie ma (w specyfikacji wcześniejszej niż ES6, patrz dodatek A)
możliwości stosowania względnego polimorfizmu. Ponieważ obiekty Car
i Vehicle mają funkcję o takiej samej nazwie, drive(), w celu ich rozróżnienia
konieczne jest użycie odwołania absolutnego (nie względnego). Podajemy
więc najpierw nazwę obiektu (Vehicle), a następnie jego funkcji (drive()).
Jednak w przypadku wywołania Vehicle.drive() wiązaniem this dla podanej
funkcji będzie obiekt Vehicle, a nie Car (patrz rozdział 2.), co jest niezgodne
z naszymi oczekiwaniami. Dlatego też należy użyć .call( this ) — patrz
rozdział 2. — aby zagwarantować, że funkcja drive() zostanie wywołana
w kontekście obiektu Car.
Jeżeli identyfikator funkcji dla Car.drive() nie nałoży się na
(inaczej „przesłoni”, patrz rozdział 5.) Vehicle.drive(), nie
mamy do czynienia z metodą polimorfizmu. Dlatego też odwołanie do Vehicle.drive() zostanie skopiowane przez wywołanie mixin(..), dzięki czemu będziemy mieć bezpośredni
dostęp do funkcji za pomocą this.drive(). Przesłanianie nakładającego się wybranego identyfikatora to powód, dla którego
konieczne jest stosowanie znacznie bardziej skomplikowanego
podejścia pseudopolimorfizmu.
W językach programowania obiektowego oferujących względny polimorfizm
połączenie między obiektami Car i Vehicle jest tworzone tylko jednokrotnie,
na początku definicji klasy. W ten sposób mamy tylko jedno miejsce, w którym
zajmujemy się obsługą związków między obiektami.
Jednak z powodu dziwactw języka JavaScript wyraźny pseudopolimorfizm
(z powodu przesłaniania!) powoduje powstanie zawodnego ręcznego i wyraźnego
połączenia w każdej funkcji wymagającej tego rodzaju pseudo(polimorfizmu).
To oczywiście wiąże się ze zdecydowanie wyższym kosztem obsługi kodu. Co
więcej, wprawdzie wyraźny pseudopolimorfizm może symulować zachowanie
dziedziczenia wielokrotnego, to jednak z tego względu tylko niepotrzebnie
zwiększa stopień skomplikowania i zawodności rozwiązania.
100 
Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Wynikiem zastosowania powyższego podejścia jest zwykle znacznie bardziej
skomplikowany i trudniejszy w odczycie oraz w konserwacji kod. Gdy tylko
istnieje możliwość, należy unikać wyraźnego pseudopolimorfizmu, ponieważ
jego koszt w większości aspektów przekracza spodziewane korzyści.
Domieszka kopiująca
Spójrz na przedstawiony wcześniej kod funkcji pomocniczej mixin(..):
// Niezwykle uproszczony przykład domieszki mixin(..):
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// Kopiowanie klucza tylko wtedy, gdy nie istnieje w obiekcie docelowym.
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
Przeanalizujmy teraz sposób działania funkcji mixin(..). Jej zadaniem jest
przeprowadzenie iteracji przez obiekt źródłowy (sourceObj, tutaj Vehicle).
Jeżeli w obiekcie docelowym (targetObj, tutaj Car) nie zostanie znaleziona
dopasowana właściwość, funkcja tworzy jej kopię. Ponieważ tworzenie kopii
następuje już po zainicjalizowaniu obiektu, trzeba zachować ostrożność, aby
nie nadpisać właściwości istniejącej w obiekcie docelowym.
Jeżeli kopia zostanie utworzona na początku, jeszcze przed podaniem zawartości przeznaczonej dla obiektu Car, można zrezygnować z kroku sprawdzenia obiektu docelowego targetObj. Jednak takie rozwiązanie jest tylko nieco
mniej niezręczne i mniej efektywne, a więc nie powinno się go stosować:
// Alternatywna wersja funkcji, mniej bezpieczna pod względem nadpisywania właściwości.
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
targetObj[key] = sourceObj[key];
}
return targetObj;
}
var Vehicle = {
// …
Domieszki
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 101
0
};
// Zaczynamy od utworzenia pustego obiektu
// wraz ze skopiowanymi elementami obiektu Vehicle.
var Car = mixin( Vehicle, { } );
// Teraz do obiektu Car kopiujemy niezbędną zawartość.
mixin( {
wheels: 4,
drive: function() {
// …
}
}, Car );
Niezależnie od przyjętego podejścia do obiektu Car zostaje skopiowana nienadkładająca się zawartość obiektu Vehicle. Nazwa „domieszka” bierze się
z alternatywnego sposobu wyjaśnienia zadania: zawartość obiektu Car zostaje
zmieszana z zawartością obiektu Vehicle, podobnie jak czekoladę możesz
zmieszać z ulubionym deserem.
Wynikiem operacji kopiowania jest to, że obiekt Car działa zupełnie oddzielnie
od obiektu Vehicle. Po dodaniu właściwości do obiektu Car wprowadzona
zmiana nie będzie miała żadnego wpływu na obiekt Vehicle i na odwrót.
Pominąłem tutaj kilka mniej ważnych szczegółów. Nadal istnieją
pewne subtelne sposoby, na jakie dwa obiekty mogą na siebie
wpływać po operacji kopiowania. Przykładem może być współdzielenie odwołania do innego obiektu, takiego jak tablica.
Ponieważ dwa obiekty współdzielą również odwołania do funkcji, nawet
ręczne skopiowanie funkcji (domieszek) między obiektami tak naprawdę nie
zapewni rzeczywistego powielenia, z jakim mamy do czynienia w językach
programowania obiektowego.
Funkcje JavaScript w rzeczywistości nie mogą być powielane (w standardowy,
niezawodny sposób), więc efektem będzie powielone odwołanie do tego samego, współdzielonego obiektu funkcji (obiekty są funkcjami, patrz rozdział 3.).
Jeżeli zmodyfikujesz jeden z obiektów funkcji współdzielonych (takich jak
ignition()) przez na przykład dodanie właściwości, wówczas przez współdzielone odwołanie wprowadzona zmiana będzie miała wpływ na oba obiekty
Vehicle i Car.
102 
Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Jawne domieszki to dobry mechanizm w języku JavaScript. Jednak jego możliwości są znacznie mniejsze, niż się wydaje. Z rzeczywistego kopiowania właściwości między poszczególnymi obiektami wynika niewiele korzyści, w przeciwieństwie do dwukrotnego zdefiniowania właściwości, po jednym w każdym
obiekcie. Brak wyraźnych korzyści w szczególności dotyczy odwołania do
obiektu-funkcji, jak wcześniej o tym wspomniałem.
Po zmieszaniu dwóch lub większej liczby obiektów w obiekcie docelowym
można przynajmniej częściowo emulować dziedziczenie wielokrotne. Nie ma
jednak bezpośredniego sposobu obsługi kolizji, gdy dwie właściwości lub metody o tej samej nazwie są kopiowane z więcej niż tylko jednego źródła. Część
programistów (co można zaobserwować w niektórych bibliotekach) stosuje
techniki późnego wiązania, a także inne niestandardowe rozwiązania. Warto
pamiętać, że tego rodzaju sztuczki wymagają zwykle włożenia większego wysiłku (i pogodzenia się z mniejszą wydajnością) niż spodziewane korzyści.
Skoncentruj się na użyciu jedynie jawnych domieszek — i to w sytuacji, gdy ich
zastosowanie może poprawić czytelność kodu. Staraj się unikać tego wzorca,
jeśli okaże się, że utworzony kod stał się trudniejszy do monitorowania lub
też powoduje powstanie wielu niepotrzebnych zależności między obiektami.
Jeżeli prawidłowe użycie domieszek okaże się trudniejsze, niż sądziłeś, prawdopodobnie powinieneś zrezygnować z ich stosowania. Tak naprawdę gdy
korzystasz ze skomplikowanej biblioteki lub narzędzia pomocniczego w celu
poradzenia sobie ze wszystkimi przedstawionymi powyżej szczegółami, to może
być znak, że prawdopodobnie niepotrzebnie wybrałeś trudniejszą drogę.
W rozdziale 6. spróbuję przedstawić prostszy sposób pozwalający na osiągnięcie
żądanych efektów przy znacznie mniejszym zamieszaniu.
Dziedziczenie pasożytnicze
Wariant powyższego wzorca jawnej domieszki, który jest pod pewnymi względami jawny, a pod innymi niejawny, nosi nazwę dziedziczenia pasożytniczego
i został spopularyzowany przez Douglasa Crockforda.
Oto sposób jego działania.
// Tradycyjna klasa JavaScript o nazwie Vehicle.
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.ignition = function() {
console.log( "Włączenie silnika." );
Domieszki
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 103
0
};
Vehicle.prototype.drive = function() {
this.ignition();
console.log( "Sterowanie i poruszanie się do przodu!" );
};
// Klasa pasożytnicza o nazwie Car.
function Car() {
// Przede wszystkim 'car' to egzemplarz 'Vehicle'.
var car = new Vehicle();
// Teraz modyfikujemy obiekt 'car', aby nadać mu żądane cechy charakterystyczne.
car.wheels = 4;
// Zachowanie uprzywilejowanego odwołania do Vehicle::drive().
var vehDrive = car.drive;
// Nadpisanie Vehicle::drive().
car.drive = function() {
vehDrive.call( this );
console.log(
"Toczenie się na wszystkich " + this.wheels + " kołach!"
);
return car;
}
var myCar = new Car();
myCar.drive();
// Włączenie silnika.
// Sterowanie i poruszanie się do przodu!
// Toczenie się na wszystkich 4 kołach!
Jak możesz zobaczyć, na początku tworzymy kopię definicji z klasy nadrzędnej
(obiektu) Vehicle. Następnie mieszamy ją z definicją klasy potomnej (obiektu),
przy czym zachowujemy uprzywilejowane odwołanie do klasy nadrzędnej.
Tak przygotowany obiekt car jest przekazywany jako egzemplarz potomny.
Podczas wywołania new Car() następuje utworzenie nowego
obiektu, do którego można się odwoływać za pomocą słowa kluczowego this (patrz rozdział 2.). Ponieważ nie będziemy używać
tego obiektu, a zamiast tego zwracamy własny obiekt car, początkowo utworzony obiekt jest po prostu odrzucany. Dlatego też
wywołanie Car() powinno się odbywać bez słowa kluczowego new.
Funkcjonalność obu rozwiązań pozostaje identyczna, ale w drugim
przypadku nie tworzymy niepotrzebnie obiektu, który następnie
jest odrzucany i usuwany przez mechanizm usuwania nieużytków.
104 
Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Niejawne domieszki
Niejawne domieszki są ściśle powiązane z niejawnym polimorfizmem, co już
wcześniej wyjaśniłem. Dlatego też w ich przypadku zastosowanie mają te same
uwagi i ostrzeżenia.
Spójrz na poniższy fragment kodu:
var Something = {
cool: function() {
this.greeting = "Witaj, świecie!";
this.count = this.count ? this.count + 1 : 1;
}
};
Something.cool();
Something.greeting; // "Witaj, świecie!"
Something.count; // 1
var Another = {
cool: function() {
// Niejawna domieszka powodująca zmieszanie 'Something' z 'Another'.
Something.cool.call( this );
}
};
Another.cool();
Another.greeting; // "Witaj, świecie!"
Another.count; // 1 (stan nie jest współdzielony z 'Something').
W przypadku wywołania Something.cool.call( this ), które będzie zachodziło
w wywołaniu konstruktora (w większości przypadków) lub w wywołaniu metody (jak w przedstawionym powyżej kodzie), praktycznie „pożyczamy” funkcję
Something.cool() i wywołujemy ją w kontekście Another (za pomocą jej wiązania
this, patrz rozdział 2.) zamiast Something. W efekcie przypisanie tworzone przez
Something.cool() będzie zastosowane do obiektu Another, a nie Something.
Można więc powiedzieć o zmieszaniu funkcji obiektu Something z pochodzącymi z obiektu Another.
Wprawdzie przedstawiona technika wydaje się użyteczną funkcjonalnością
ponownego wiązania this, jednak wywołanie Something.cool.call( this )
jest zawodne, gdyż nie może być wykonane w odwołaniu względnym (a tym
samym bardziej elastycznym), należy więc zachować ostrożność. Ogólnie rzecz
biorąc, jeśli jest to możliwe, najlepiej unikać tego rodzaju konstrukcji i zamiast
nich zachować przejrzysty oraz łatwy do późniejszej konserwacji kod.
Domieszki
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 105
0
Podsumowanie
Klasa jest wzorcem projektowym. Wiele języków programowania zawiera
składnię pozwalającą w naturalny sposób tworzyć oprogramowanie z użyciem
podejścia zorientowanego obiektowo. JavaScript ma podobną składnię, ale
jego zachowanie znacznie odbiega od tego, do którego przywykłeś, pracując
z klasami w innych językach programowania.
Klasa oznacza kopiowanie.
Podczas tworzenia tradycyjnej klasy mamy do czynienia ze skopiowaniem zachowania z klasy do nowego egzemplarza. Z kolei w trakcie dziedziczenia również następuje skopiowanie zachowania z obiektu nadrzędnego do potomnego.
Polimorfizm (istnienie na wielu poziomach hierarchii dziedziczenia różnych
funkcji o takiej samej nazwie) może się wydawać niejawnym odwołaniem
względnym z obiektu potomnego do nadrzędnego, ale tak naprawdę jest wynikiem operacji kopiowania.
JavaScript nie tworzy automatycznie kopii (jak może na to wskazywać klasa)
między obiektami.
Wzorzec domieszki (zarówno jawnej, jak i niejawnej) jest często stosowany
w charakterze pewnego rodzaju emulacji zachowania kopiowania klasy, choć
zwykle prowadzi do powstania brzydkiej i zawodnej składni, takiej jak pseudopolimorfizm (InnyObiekt.nazwaMetody.wywołanie(this, ...)). Skutkiem
jest bardziej zagmatwany kod, który na dodatek będzie trudniejszy do późniejszej konserwacji.
Jawna domieszka nie jest dokładnie taka sama jak operacja kopiowania klasy,
ponieważ obiekty (i funkcje!) mają powielone jedynie współdzielone odwołania, a nie same obiekty bądź funkcje. Jeżeli nie zwrócisz uwagi na tego rodzaju
szczegóły, skutkiem może być wiele pułapek.
Ogólnie rzecz biorąc, nieprawdziwe klasy w JavaScript często powodują wiele
błędów, które ujawniają się podczas tworzenia kodu w przyszłości, a przy tym
niezbyt skutecznie rozwiązują obecne, rzeczywiste problemy.
106 
Rozdział 4. Mieszanie obiektów „klas”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
ROZDZIAŁ 5.
Prototypy
W rozdziałach 3. i 4. kilkakrotnie wspomniałem o łańcuchu [[Prototype]],
choć nie wyjaśniłem, o co dokładnie chodzi. W tym rozdziale przeanalizujemy
właśnie prototypy.
Wszystkie próby emulacji opisanego w rozdziale 4. zachowania
klasy (czyli kopiowania) oznaczone jako warianty domieszek całkowicie pomijają zaprezentowany w tym rozdziale mechanizm
łańcucha [[Prototype]].
[[Prototype]]
Obiekty w JavaScript mają wewnętrzną właściwość oznaczaną w specyfikacji jako
[[Prototype]]. Wymieniona właściwość jest po prostu odwołaniem do innego
obiektu. Niemal we wszystkich obiektach wartość właściwości [[Prototype]]
jest w chwili tworzenia obiektu inna niż null.
Uwaga, wkrótce się przekonasz, że jest możliwe, aby obiekt nie miał przypisanej
wartości dla właściwości [[Prototype]], jednak tego rodzaju sytuacja rzadko
ma miejsce.
Spójrz na poniższy fragment kodu:
var myObject = {
a: 2
};
myObject.a; // 2
107
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Do czego prowadzi odwołanie [[Prototype]]? W rozdziale 3. dowiedziałeś się,
że operacja [[Get]] jest wywoływana podczas odwoływania się do właściwości
obiektu, na przykład myObject.a. W przypadku domyślnej operacji [[Get]]
pierwszym krokiem jest sprawdzenie, czy sam obiekt zawiera wskazaną właściwość a. Jeżeli tak, zostanie ona użyta.
Proxy ES6 to zagadnienie wykraczające poza zakres tematyczny
książki (powrócę do niego w jednej z kolejnych książek serii).
Wszystko to, co powiemy sobie tutaj o operacjach [[Get]]
i [[Put]], nie będzie miało zastosowania w przypadku korzystania z proxy.
Jednak to, co się dzieje, gdy właściwość a nie znajduje się w myObject, kieruje
naszą uwagę w stronę połączenia [[Prototype]] obiektu.
Domyślna operacja [[Get]] wykorzysta łącze [[Prototype]] obiektu, jeśli nie
będzie mogła znaleźć żądanej właściwości bezpośrednio w obiekcie:
var anotherObject = {
a: 2
};
// Utworzenie obiektu połączonego z anotherObject.
var myObject = Object.create( anotherObject );
myObject.a; // 2
Wkrótce wyjaśnię sposób działania wywołania Object.create(..).
Teraz możesz przyjąć założenie, że wymienione wywołanie tworzy obiekt wraz z połączeniem [[Prototype]] do wskazanego
obiektu.
W tym momencie mamy obiekt myObject, który za pomocą [[Prototype]]
został połączony z innym obiektem. Wprawdzie myObject.a tak naprawdę
nie istnieje, ale nie ma to znaczenia. Operacja dostępu do właściwości kończy
się pomyślnie (właściwość znajduje się w anotherObject), a znalezioną wartością jest 2.
Co się stanie w sytuacji, gdy właściwość a nie zostanie znaleziona również
w obiekcie anotherObject? W obiekcie anotherObject następuje sprawdzenie
łańcucha [[Prototype]] i jeśli nie jest pusty, program podąża za nim.
108 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Proces jest kontynuowany aż do znalezienia szukanej właściwości lub zakończenia łańcucha [[Prototype]]. Jeżeli po przetworzeniu całego łańcucha właściwość w ogóle nie zostanie znaleziona, wartością zwrotną operacji [[Get]]
będzie undefined.
Podobnie jak w przypadku przedstawionego powyżej procesu wyszukiwania
w łańcuchu [[Prototype]], jeżeli użyjesz pętli for in do przeprowadzenia iteracji obiektu, wszystkie właściwości dostępne za pomocą łańcucha (i zdefiniowane jako enumerable — patrz rozdział 3.) zostaną wymienione. W przypadku użycia operatora in do sprawdzenia, czy w obiekcie istnieje wskazana
właściwość, operator ten sprawdzi cały łańcuch obiektu (niezależnie od widoczności w typach wyliczeniowych):
var anotherObject = {
a: 2
};
// Utworzenie obiektu połączonego z anotherObject.
var myObject = Object.create( anotherObject );
for (var k in myObject) {
console.log("Znaleziono: " + k);
}
// Znaleziono: a
("a" in myObject); // Prawda.
Podczas przeprowadzania operacji wyszukiwania na różne sposoby dochodzi
do sprawdzenia łańcucha [[Prototype]], po jednym łączu za każdym razem.
Wyszukiwanie zostaje zatrzymane po znalezieniu właściwości lub zakończeniu łańcucha.
Object.prototype
Mógłbyś w tym miejscu zapytać: gdzie tak dokładnie znajduje się „koniec”
łańcucha [[Prototype]]?
Górnym końcem każdego normalnego łańcucha [[Prototype]] jest wbudowany obiekt Object.prototype. Wymieniony obiekt zawiera wiele różnych
metod pomocniczych używanych w JavaScript, ponieważ wszystkie normalne
(wbudowane, a nie charakterystyczne dla hosta) obiekty w tym języku
„pochodzą” (czyli mają początek łańcucha [[Prototype]]) właśnie z obiektu
Object.prototype.
[[Prototype]]
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 109
0
Niektóre przedstawione tutaj metody pomocnicze mogą być znane, na przykład
.toString() i .valueOf(). W rozdziale 3. poznałeś też inną: .hasOwnProperty(..).
Mamy jeszcze jedną funkcję Object.prototype, która powinna być Ci znana:
.isPrototypeOf(..). Do ostatniej z wymienionych funkcji powrócimy w dalszej
części rozdziału.
Ustawianie i przesłanianie właściwości
W rozdziale 3. dowiedziałeś się, że ustawienie właściwości obiektu jest nieco
bardziej skomplikowane niż dodanie nowej właściwości do istniejącego obiektu
lub też zmiana wartości istniejącej właściwości. Powrócimy teraz do wspomnianej sytuacji i dokładnie ją omówimy:
myObject.foo = "bar";
Jeżeli obiekt myObject bezpośrednio zawiera zwykłą właściwość w postaci akcesora danych o nazwie foo, operacja przypisania jest prosta i sprowadza się
do zmiany wartości istniejącej właściwości.
Natomiast jeżeli foo nie znajduje się bezpośrednio w obiekcie myObject, dochodzi do sprawdzenia łańcucha [[Prototype]], podobnie jak podczas operacji
[[Get]]. W przypadku nieznalezienia foo w żadnym miejscu łańcucha właściwość foo będzie dodana bezpośrednio do myObject wraz z podaną wartością,
co jest oczekiwanym zachowaniem.
Jeśli jednak foo znajduje się gdzieś wyżej w łańcuchu, może dojść do dziwnego
(i prawdopodobnie zaskakującego) zachowania w trakcie operacji przypisania
myObject.foo = "bar". Wszystko dokładnie wyjaśni się już za moment.
Jeżeli właściwość o nazwie foo znajduje się zarówno w samym obiekcie myObject,
jak i gdzieś wyżej w łańcuchu [[Prototype]] rozpoczynającym myObject, mamy
do czynienia z tak zwanym przesłonięciem (ang. shadowing). Właściwość foo
znajdująca się bezpośrednio w obiekcie myObject przesłania każdą właściwość foo
pojawiającą się wyżej w łańcuchu, ponieważ operacja wyszukiwania myObject.foo
zawsze będzie znajdować tę właściwość foo, która znajduje się najniżej w łańcuchu.
Jak już wcześniej wspomniałem, przesłonięcie foo w obiekcie myObject nie
jest tak proste, jak może się wydawać. Przeanalizujemy teraz trzy scenariusze
dla operacji przypisania myObject.foo = "bar", gdy foo nie znajduje się bezpośrednio w obiekcie myObject , ale jest na wyższym poziomie łańcucha
[[Prototype]] dla myObject.
110 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
1. Jeżeli standardowy akcesor danych (patrz rozdział 3.) w postaci właściwości
o nazwie foo znajduje się gdziekolwiek wyżej w łańcuchu [[Prototype]]
i nie jest oznaczony jako tylko do odczytu (writable:false), dodanie
nowej właściwości o nazwie foo bezpośrednio do myObject skutkuje przesłonięciem właściwości.
2. Jeżeli właściwość foo zostanie znaleziona wyżej w łańcuchu [[Prototype]],
ale jest oznaczona jako tylko do odczytu (writable:false), ustawienie
wartości tej istniejącej właściwości czy utworzenie przesłoniętej właściwości w myObject jest niedozwolone. Gdy kod zostanie uruchomiony
w trybie ścisłym, następuje zgłoszenie błędu. W przeciwnym razie ustawienie wartości właściwości zostanie po cichu zignorowane. Niezależnie
od wszystkiego nie mamy do czynienia z przesłonięciem właściwości.
3. Jeżeli właściwość foo zostanie znaleziona wyżej w łańcuchu [[Prototype]]
i jest setterem (patrz rozdział 3.), zawsze będzie wywoływany setter. Do
obiektu myObject nie zostanie dodana właściwość foo (nie dojdzie do przesłonięcia) ani nie będziemy mieli do czynienia z ponownym zdefiniowaniem settera.
Większość programistów przyjmuje założenie, że przypisanie właściwości
(operacja [[Put]]) zawsze będzie skutkować przesłonięciem, jeśli właściwość
już istnieje wyżej w łańcuchu [[Prototype]]. Jak mogłeś zobaczyć, jest tak tylko
w jednej z trzech omówionych powyżej sytuacji (w pierwszej z nich).
Jeżeli chcesz przesłonić foo w pozostałych dwóch sytuacjach, nie możesz użyć
przypisania (=), ale musisz skorzystać z wywołania Object.defineProperty(..)
— patrz rozdział 3. — w celu dodania foo do obiektu myObject.
Drugi przypadek może być najbardziej zaskakujący ze wszystkich
omówionych powyżej. Istnienie właściwości tylko od odczytu
uniemożliwia niejawne utworzenie nowej właściwości o tej samej
nazwie na niższym poziomie łańcucha [[Prototype]], gdyż
doprowadziłoby to do przesłonięcia. Powodem wymienionego
ograniczenia jest przede wszystkim podtrzymanie iluzji istnienia
właściwości dziedziczonych przez klasę. Jeżeli właściwość foo
znajdującą się na wyższym poziomie łańcucha traktujesz jako
możliwą do dziedziczenia (skopiowania) przez myObject, wówczas sensowne jest wymuszenie, aby właściwość foo w obiekcie myObject była przeznaczona tylko do odczytu. Jeżeli jednak
przekonasz się, że podczas dziedziczenia tak naprawdę nie mamy
[[Prototype]]
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 111
0
do czynienia ze wspomnianym kopiowaniem (patrz rozdziały
4. i 5.), uniemożliwienie obiektowi myObject posiadania właściwości foo (ponieważ inny obiekt ma taką właściwość tylko
do odczytu) wydaje się nienaturalne. Jeszcze dziwniejsze jest
to, że wspomniane ograniczenie ma zastosowanie jedynie do
operacji przypisania (=), a nie jest wymuszane w trakcie użycia
wywołania Object.defineProperty(..).
Przesłanianie metod prowadzi do wyraźnego pseudomorfizmu (patrz rozdział 4.), o ile zachodzi potrzeba zastosowania wzorca delegowania między
tymi metodami. Zwykle przesłanianie jest bardzo skomplikowane i wywołuje
dużo więcej zamieszania, niż przynosi korzyści, dlatego warto go unikać, gdy
tylko istnieje taka możliwość. W rozdziale 6. poznasz alternatywne wzorce
projektowe, które zniechęcają do przesłaniania — zamiast tego skłaniają nas
do stosowania innych, czytelniejszych rozwiązań.
Przesłanianie może występować niejawnie na wiele subtelnych sposobów,
dlatego należy zachować ostrożność podczas próby jego uniknięcia. Spójrz na
poniższy fragment kodu:
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // Prawda.
myObject.hasOwnProperty( "a" ); // Fałsz.
myObject.a++; // Ups! Niejawne przesłonięcie!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // Prawda.
Wprawdzie może się wydawać, że polecenie myObject.a++ powinno (za pomocą delegowania) wyszukać i po prostu zainkrementować wartość właściwości anotherObject.a znajdującej się w danym miejscu, to tak naprawdę
operacja ++ będzie odpowiadała poleceniu myObject.a = myObject.a + 1.
W tym momencie zostanie wykonana seria operacji: dochodzi do wyszukania
przez operację [[Get]] właściwości a za pomocą [[Prototype]], a następnie
112 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
pobrania wartości bieżącej 2 z anotherObject.a. W dalszej kolejności ma
miejsce inkrementacja tej wartości o jeden i na koniec wykonywana jest operacja [[Put]], która przypisuje wartość 3 nowej, przesłoniętej właściwości a
w myObject. Ups!
Zachowaj ostrożność podczas pracy z delegowanymi właściwościami, które możesz
modyfikować. Jeżeli chcesz przeprowadzić inkrementację anotherObject.a, to
jedynym poprawnym podejściem jest zastosowanie polecenia anotherObject.a++.
„Klasa”
Na tym etapie mógłbyś zapytać, dlaczego jeden obiekt miałby być połączony
z innym? Czy istnieje jakakolwiek rzeczywista korzyść płynąca z takiego połączenia? To naprawdę dobre pytanie, ale zanim na nie odpowiemy, konieczne
jest zrozumienie, czym [[Prototype]] nie jest. Dzięki temu będziesz mógł
w pełni pojąć, czym [[Prototype]] jest i na czym polega jego użyteczność.
Jak już wyjaśniłem w rozdziale 4., w języku JavaScript nie ma abstrakcyjnych
wzorców dla obiektów, które w innych językach programowania obiektowego
są nazywane klasami. JavaScript ma jedynie obiekty.
W rzeczywistości JavaScript jest niemalże unikatowym językiem i prawdopodobnie jedynym, który zasłużenie używa etykiety „zorientowany obiektowo”.
To jeden z krótkiej listy języków, w których obiekt może być utworzony bezpośrednio, w ogóle bez konieczności użycia klasy.
W języku JavaScript klasy nie mogą (przecież nie istnieją!) opisywać sposobu
zachowania obiektu. Dlatego też obiekt bezpośrednio definiuje własne zachowanie. To po prostu jest obiekt.
Funkcje „klasy”
Od wielu lat w języku JavaScript niezwykle powszechnie stosowana jest sztuczka
polegająca na tworzeniu elementu przypominającego klasę. Teraz szczegółowo
omówimy takie podejście.
Wspomniana sztuczka polegająca na utworzeniu pewnego rodzaju klasy ma
swoje odzwierciedlenie w postaci dziwnej cechy charakterystycznej funkcji:
wszystkie funkcje domyślnie otrzymują publiczną, nieuwzględnianą przez
Funkcje „klasy”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 113
0
typy wyliczeniowe (patrz rozdział 3.) właściwość o nazwie prototype, która
prowadzi do innego dowolnego obiektu:
function Foo() {
// ...
}
Foo.prototype; // { }
Wspomniany obiekt jest często nazywany prototypem Foo, ponieważ uzyskujemy do niego dostęp za pomocą odwołania właściwości o niefortunnej nazwie
Foo.prototype. Terminologia niestety wywołuje pewne zamieszanie, o czym się
wkrótce przekonasz. Dlatego też wymienioną właściwość będę określał mianem
„obiektu wcześniej znanego jako prototyp Foo”. Żartowałem! Co powiesz na
„obiekt określony mianem Foo dot prototype”?
Niezależnie od tego, jak go nazwiemy, nadal aktualne pozostaje pytanie, czym
dokładnie jest wspomniany obiekt.
Oto najlepsze z możliwych wyjaśnień: każdy obiekt tworzony za pomocą wywołania new Foo() — patrz rozdział 2. — będzie zawierał (pod pewnymi względami dowolne) połączenie [[Prototype]] ze wspomnianym wcześniej obiektem
Foo dot prototype.
Zilustrujmy tę sytuację:
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // Prawda.
Podczas tworzenia a za pomocą wywołania new Foo() jednym z przeprowadzanych kroków (wszystkie cztery dokładnie omówiłem w rozdziale 2.) jest
dostarczenie a wewnętrznego łącza [[Prototype]] do obiektu wskazywanego
przez Foo.prototype.
Zatrzymaj się na chwilę i zastanów nad znaczeniem powyższego zdania.
W językach obiektowych można tworzyć wiele kopii (egzemplarzy) klasy,
podobnie jak w przypadku tłoczenia czegoś przy użyciu formy. Jak zobaczyłeś
w rozdziale 4., wymienione zachowanie jest możliwe, ponieważ proces tworzenia egzemplarza klasy (lub dziedziczenia po klasie) oznacza „skopiuj zawartość
114 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
tej klasy i umieść w fizycznym obiekcie”. Ta operacja jest powtarzana dla każdego nowego egzemplarza.
Jednak w języku JavaScript wspomniane powyżej kopiowanie nie jest przeprowadzane. Nie tworzysz wielu egzemplarzy klas. Można utworzyć wiele
obiektów połączonych z pewnym obiektem za pomocą [[Prototype]]. Domyślnie nie mamy do czynienia z kopiowaniem, dlatego też obiekty nie są zupełnie
oddzielne, a można wręcz stwierdzić, że pozostają połączone.
Wynikiem wywołania new Foo() jest nowy obiekt (nazywamy go a), który jest
wewnętrznie połączony z obiektem Foo.prototype za pomocą [[Prototype]].
W efekcie mamy dwa połączone ze sobą obiekty. I to tyle. Nie tworzymy egzemplarza klasy. Na pewno nie dochodzi do skopiowania zachowania „klasy”
do konkretnego obiektu. Po prostu spowodowaliśmy, że dwa obiekty są ze
sobą połączone.
W rzeczywistości sekretem umykającym większości programistów JavaScript
jest to, że wywołanie new Foo() ma naprawdę niewiele bezpośrednio wspólnego
z procesem tworzenia łącza. To raczej przypadkowy efekt uboczny. Wywołanie
new Foo() jest pośrednim, okrężnym sposobem otrzymania interesującego nas
efektu: nowego obiektu połączonego z innym obiektem.
Czy ten efekt można osiągnąć w bardziej bezpośredni sposób? Tak! Tutaj przydatne będzie wywołanie Object.create(..). Jednak do rozwiązania opartego
na wymienionym wywołaniu przejdziemy dopiero za chwilę.
Co z nazwą?
W JavaScript nie tworzymy kopii jednego obiektu („klasa”) w drugim („egzemplarz”). Zamiast tego tworzymy łącza między obiektami. W przypadku mechanizmu [[Prototype]] na rysunku 5.1 kierunek działania pokazują strzałki,
od prawej do lewej i od dołu do góry:
Ten mechanizm jest często nazywany dziedziczeniem prototypowym (szczegółowe omówienie kodu przedstawię za chwilę), o którym mówi się, że to
wersja klasycznego dziedziczenia w językach dynamicznych. To próba wykorzystania powszechnie stosowanego znaczenia „dziedziczenia” w świecie programowania zorientowanego obiektowo, ale w wersji zmodyfikowanej, aby
semantyka została dopasowana do dynamicznego języka skryptowego.
Funkcje „klasy”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 115
0
Rysunek 5.1. Graficzne przedstawienie mechanizmu [[Prototype]]
Słowo „dziedziczenie” ma bardzo jasno zdefiniowane znaczenie (patrz rozdział 4.),
z którym na dodatek wiąże się spora tradycja. Dodanie słowa „prototypowe”
w celu nadania nazwy zupełnie przeciwnemu do dziedziczenia zachowaniu
w JavaScript nie pomogło w zlikwidowaniu trwającego już od niemal dwóch
dekad zamieszania.
Mogę stwierdzić, że dodanie słowa „prototypowe” do dziedziczenia w celu
całkowitego odwrócenia jego znaczenia przypomina trzymanie pomarańczy
w jednej ręce i jabłka w drugiej, a następnie nazwanie jabłka czerwoną pomarańczą. Niezależnie od tego, jak mylącej nazwy tutaj użyjemy, nie zmienia to
faktu, że jednym owocem jest pomarańcza, a drugim jabłko.
Lepszym podejściem będzie po prostu nazwanie jabłka jabłkiem — wówczas
używamy najodpowiedniejszej i bezpośredniej terminologii. W ten sposób
łatwiej zrozumieć podobieństwa i różnice, ponieważ wszyscy doskonale wiedzą
i rozumieją znaczenie słowa „jabłko”.
Z powodu tego zamieszania i łączenia pojęć określenie „dziedziczenie prototypowe” (i nieudolna próba stosowania związanej z tym terminologii programowania zorientowanego obiektowo, czyli na przykład słów takich jak
„klasa”, „konstruktor”, „egzemplarz”, „polimorfizm” itd.) wyrządziło więcej
szkody, niż przyniosło pożytku w wyjaśnieniu rzeczywistego sposobu działania
mechanizmu JavaScript.
116 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Dziedziczenie sugeruje operację kopiowania, ale domyślnie JavaScript nie kopiuje
właściwości obiektu. Zamiast tego tworzy połączenie między dwoma obiektami, z których jeden w zasadzie deleguje do innego obiektu dostęp do funkcji
i właściwości. Wspomniane delegowanie (patrz rozdział 6.) jest znacznie odpowiedniejszym określeniem dla stosowanego w JavaScript mechanizmu łączenia obiektów ze sobą.
Innym pojęciem, czasami pojawiającym się w języku JavaScript, jest dziedziczenie różnicowe. W tym przypadku idea polega na tym, że zachowanie
obiektu opisujemy w kategoriach różnic względem ogólnego deskryptora.
Na przykład można wyjaśnić, że samochód jest rodzajem pojazdu o czterech
kołach, zamiast po prostu podawać wszystkie cechy charakterystyczne
ogólnego pojazdu (silnik itd.).
Jeżeli spróbujesz potraktować dany obiekt w JavaScript jako połączenie wszystkich funkcji dostępnych dla niego poprzez wzorzec delegowania, a następnie
w myślach sprowadzisz te wszystkie funkcje do pojedynczej namacalnej rzeczy, wtedy (z grubsza) możesz zobaczyć, jak przedstawia się dziedziczenie
różnicowe.
Jednak podobnie jak w przypadku dziedziczenia prototypowego, dziedziczenie
różnicowe udaje, że model powstający w Twoich myślach jest znacznie ważniejszy od rzeczywistego sposobu działania języka. Pomijamy fakt, że obiekt B
nie został skonstruowany w odmienny sposób, a zamiast tego jest zbudowany
z użyciem określonych cech charakterystycznych, obok których istnieją „dziury”,
w których nic nie zostało zdefiniowane. W przypadku wspomnianych dziur
(lub inaczej braku definicji) może być zastosowany wzorzec delegowania, który
po prostu w locie „wypełni je” delegowanymi funkcjami.
Domyślnie w trakcie operacji kopiowania nie powstaje pojedynczy obiekt
różnicowy, na co może wskazywać model dziedziczenia różnicowego. Dlatego
też dziedziczenie różnicowe nie jest najbardziej naturalnym sposobem opisywania rzeczywistego działania mechanizmu [[Prototype]] w JavaScript.
Oczywiście możesz się zdecydować na stosowanie terminologii dziedziczenia
różnicowego i modelu ułożonego w myślach, ale to nie zmienia faktu, że mamy wówczas do czynienia jedynie z umysłową akrobatyką, a nie z faktycznym
zachowaniem języka.
Funkcje „klasy”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 117
0
Konstruktory
Powracamy do przedstawionego wcześniej fragmentu kodu:
function Foo() {
// ...
}
var a = new Foo();
Co dokładnie skłania nas do uznania Foo za „klasę”?
Przede wszystkim użycie słowa kluczowego new, podobnie jak ma to miejsce
w językach obiektowych podczas tworzenia egzemplarzy klasy. Inna przesłanka
to fakt wykonania metody konstruktora klasy, ponieważ Foo() jest faktycznie
wywoływaną metodą, podobnie jak konstruktor rzeczywistej klasy jest wywoływany podczas tworzenia egzemplarzy tej klasy.
Aby jeszcze bardziej zagmatwać semantykę „konstruktora”, obiekt nazwany
Foo.prototype charakteryzuje się jeszcze jedną interesującą cechą. Spójrz na
poniższy fragment kodu:
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // Prawda.
var a = new Foo();
a.constructor === Foo; // Prawda.
Obiekt Foo.prototype domyślnie (w trakcie deklaracji, czyli w pierwszym wierszu kodu!) otrzymuje publiczną, nieuwzględnianą w typach wyliczeniowych
(patrz rozdział 3.) właściwość o nazwie .constructor. Wymieniona właściwość jest odwołaniem zwrotnym do funkcji (w tym przypadku Foo) powiązanej
z obiektem. Co więcej, można zobaczyć, że obiekt a utworzony przez wywołanie „konstruktora” new Foo() również wydaje się mieć właściwość o nazwie
.constructor, wskazującą „funkcję, która utworzyła dany obiekt”.
To niekoniecznie prawda. Obiekt a nie ma właściwości .const
ructor i choć a.constructor faktycznie wskazuje funkcję Foo,
to jednak w rzeczywistości użyte tutaj określenie „konstruktor” nie
oznacza „utworzone przez (tutaj podaj nazwę klasy)”, jak mogłoby
się wydawać. Do tego dziwnego zachowania jeszcze powrócimy.
118 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
O tak, ponadto… zgodnie z konwencją obowiązującą w świecie JavaScript nazwa
„klasy” rozpoczyna się dużą literą, a więc fakt użycia nazwy Foo zamiast foo wyraźnie wskazuje na „klasę”. To całkowicie zrozumiałe, prawda!?
Ta konwencja jest na tyle silnie zakorzeniona w świecie JavaScript, że wielu programistów nawet narzeka, gdy spotykają
wywołanie new wraz z metodą o nazwie rozpoczynającej się
małą literą lub gdy brakuje słowa kluczowego new przed wywołaniem funkcji o nazwie rozpoczynającej się wielką literą.
To niewiarygodne, że tak bardzo się trudzimy, by otrzymać
(nieprawdziwą) „klasę” bezpośrednio w JavaScript, i tworzymy
reguły nakazujące użycie wielkich liter, które tak naprawdę
w ogóle nie mają żadnego znaczenia dla silnika JavaScript.
Konstruktor czy wywołanie?
W poprzednim fragmencie kodu kuszące może być uznanie Foo za konstruktor,
ponieważ wywołujemy tę funkcję wraz ze słowem kluczowym new i obserwujemy, jak „konstruuje” obiekt.
W rzeczywistości Foo nie ma żadnych specjalnych cech, dzięki którym bardziej od innych funkcji w programie nadaje się do pełnienia roli „konstruktora”. Funkcje same w sobie nie są konstruktorami. Jednak po umieszczeniu
słowa kluczowego new przed zwykłym wywołaniem funkcji otrzymujemy tak
zwane „wywołanie konstruktora”. W rzeczywistości użycie słowa kluczowego
new stanowi pewnego rodzaju przechwycenie zwykłej funkcji i wywołanie jej
w sposób pozwalający na konstrukcję obiektu, oczywiście poza wykonaniem
innych zadań, do których została przeznaczona dana funkcja.
Na przykład:
function NothingSpecial() {
console.log( "Wszystko mi jedno!" );
}
var a = new NothingSpecial();
// "Wszystko mi jedno!"
a; // {}
NothingSpecial() to zwykła funkcja, ale po wywołaniu wraz ze słowem
kluczowym new powoduje skonstruowanie obiektu, co następuje właściwie
jako efekt uboczny, a później przypisanie go do zmiennej a. To wywołanie
Funkcje „klasy”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 119
0
jest wywołaniem konstruktora, ale funkcja NothingSpecial() w żadnym razie
konstruktorem nie jest.
Innymi słowy, w świecie JavaScript najbardziej odpowiednie stwierdzenie
brzmi następująco: konstruktor to dowolna funkcja wywołana wraz ze słowem kluczowym new.
Funkcja nie jest konstruktorem, natomiast wywołanie funkcji może się stać
wywołaniem konstruktora tylko i wyłącznie wtedy, gdy odbywa się za pomocą
słowa kluczowego new.
Mechanika
Czy to jedyne powszechnie stosowane wyzwalacze dla feralnych „klas” w języku JavaScript?
Nie całkiem. Programiści JavaScript starają się symulować tyle zachowań znanych z programowania obiektowego, ile się tylko da:
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
var a = new Foo( "a" );
var b = new Foo( "b" );
a.myName(); // "a"
b.myName(); // "b"
W powyższym fragmencie kodu zastosowano jeszcze dwie dodatkowe sztuczki
związane z podejściem programowania obiektowego:
1. Polecenie this.name = name powoduje dodanie właściwości .name do
każdego obiektu (odpowiednio a i b, więcej informacji o wiązaniu this
znajdziesz w rozdziale 2.), podobnie jak egzemplarze klasy hermetyzują
wartości danych.
2. Użycie polecenia Foo.prototype.myName = ... to prawdopodobnie najbardziej interesujące podejście, które powoduje dodanie właściwości
(funkcji) do obiektu Foo.prototype. Tym samym wywołanie a.myName()
działa, co prawdopodobnie będzie zaskoczeniem. Jak to możliwe?
120 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
W powyższym fragmencie kodu niezwykle kuszące może być przyjęcie założenia,
że podczas tworzenia a i b właściwości oraz funkcje w obiekcie Foo.prototype
zostaną skopiowane do obu obiektów. Nic takiego nie ma jednak miejsca.
Na początku rozdziału omówiłem połączenie za pomocą [[Prototype]] i pokazałem, że takie podejście zapewnia awaryjne rozwiązanie podczas wyszukiwania — gdy odwołanie do właściwości nie znajduje się bezpośrednio w obiekcie
— ponieważ stanowi część domyślnego algorytmu [[Get]].
Dlatego też ze względu na sposób tworzenia obiekty a i b zawierają wewnętrzne
połączenie [[Prototype]] z Foo.prototype. W przypadku nieznalezienia właściwości myName w a lub b, znajdzie się ona (za pomocą wzorca delegowania,
patrz rozdział 6.) w obiekcie Foo.prototype.
Powrót „konstruktora”
Czy pamiętasz wcześniejszą analizę dotyczącą właściwości .constructor i to,
jak wydawało się, że a.constructor === true, czyli a miało mieć rzeczywistą
właściwość .constructor wskazującą Foo? Tak jednak nie jest.
Mamy tutaj do czynienia z niefortunnym nieporozumieniem. W rzeczywistości
odwołanie .constructor również zostało delegowane do obiektu Foo.prototype,
który domyślnie ma właściwość .constructor wskazującą Foo.
Wydaje się, że jest to niezwykle użyteczne, iż obiekt a „skonstruowany przez”
Foo ma dostęp do właściwości .constructor wskazującej Foo. Niestety, daje
nam to fałszywe poczucie bezpieczeństwa. To tylko szczęśliwy zbieg okoliczności, że a.constructor wskazuje Foo za pomocą domyślnego delegowania
[[Prototype]]. Z gruntu fałszywe przekonanie, że .constructor oznacza „został skonstruowany przez (tutaj podaj nazwę klasy)”, może naprawdę na wiele
różnych sposobów wypaczyć działanie kodu.
Z jednej strony właściwość .constructor w obiekcie Foo.prototype jest umieszczana domyślnie w obiekcie tworzonym podczas deklarowania funkcji Foo. Jeżeli
utworzysz nowy obiekt i zastąpisz w funkcji domyślne odwołanie .prototype,
nowy obiekt nie otrzyma w magiczny sposób właściwości .constructor.
Spójrz na poniższy fragment kodu:
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // Utworzenie nowego obiektu prototype.
Funkcje „klasy”
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 121
0
var a1 = new Foo();
a1.constructor === Foo; // Fałsz!
a1.constructor === Object; // Prawda!
Wywołanie Object(..) nie spowodowało „skonstruowania” (utworzenia)
obiektu a1, prawda? Wydaje się jednak, że wywołanie takie jak Foo() „konstruuje” obiekt a1. Większość programistów uważa, że wywołanie Foo() tworzy
obiekt. Jednak to przekonanie okazuje się błędne, gdy znaczenie słowa „konstruktor” odczytujesz jako „został skonstruowany przez (tutaj podaj nazwę
klasy)”, ponieważ w takim przypadku właściwość a1.constructor powinna
wskazywać Foo, a nie wskazuje!
Co się dzieje? Obiekt a1 nie ma właściwości .constructor, a więc stosuje delegowanie w górę łańcucha [[Prototype]] do obiektu Foo.prototype. Jednak
ten obiekt również nie ma właściwości .constructor (takiej jak w domyślnym
obiekcie Foo.prototype!), a zatem delegowanie trwa dalej. Tym razem docieramy do Object.prototype, czyli na początek łańcucha delegowania. Ten
obiekt faktycznie ma właściwość .constructor, która prowadzi do wbudowanej
funkcji Object(..).
Nieporozumienie: klapa.
Oczywiście właściwość .constructor można dodać z powrotem do obiektu
Foo.prototype, ale to wymaga ręcznej operacji, zwłaszcza jeśli chcesz otrzymać
takie samo zachowanie jak w przypadku natywnej właściwości i zagwarantować,
że wymieniona właściwość nie będzie uwzględniana przez typy wyliczeniowe
(patrz rozdział 3.).
Spójrz na poniższy fragment kodu:
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // Utworzenie nowego obiektu prototype.
// Konieczne jest poprawne usunięcie usterki, jaką jest brak właściwości
// '.constructor' w nowym obiekcie dostępnym jako 'Foo.prototype'.
// Zajrzyj do rozdziału 3. i poszukaj tam opisu funkcji defineProperty(..).
Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo
// Wskazuje właściwość '.constructor' w obiekcie 'Foo'.
} );
122 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
To całkiem dużo pracy niezbędnej do wykonania, aby naprawić właściwość
.constructor. Co więcej, nasze działanie to utrwalanie błędnej koncepcji, że
słowo „konstruktor” oznacza „został skonstruowany przez (tutaj podaj nazwę
klasy)”. Mamy więc do czynienia z dość kosztowną iluzją.
Faktem jest, że właściwość .constructor w obiekcie domyślnie wskazuje
funkcję, która ma odwołanie zwrotne do tego obiektu — wspomniane odwołanie nosi nazwę .prototype. Słowa „konstruktor” i „prototyp” mają tutaj luźne
znaczenia, które później mogą — choć nie muszą — być prawdziwe. Najlepszym rozwiązaniem jest nieustanne przypominanie sobie, że „konstruktor”
nie oznacza „skonstruowany przez (tutaj podaj nazwę klasy)”.
Właściwość .constructor nie jest magicznie niemodyfikowalną właściwością.
Jak wcześniej wspomniałem (patrz poprzedni fragment kodu), nie jest uwzględniana przez typy wyliczeniowe, ale jej wartość może być zmieniona. Co więcej,
istnieje możliwość dodania lub nadpisania (celowo bądź przypadkowo) właściwości
o nazwie constructor w dowolnym obiekcie dowolnego łańcucha [[Prototype]].
Ze względu na sposób poruszania się algorytmu [[Get]] po łańcuchu [[Pro
totype]] odwołanie do właściwości .constructor znalezionej w dowolnym
miejscu wspomnianego łańcucha może zostać rozwiązane zupełnie inaczej, niż
tego oczekujesz.
Czy już widzisz, jak dowolne może być rzeczywiste znaczenie omawianej właściwości?
Wynik? Pewne właściwości, takie jak a1.constructor, nie mogą być bezpiecznie
uznane za odwołania do funkcji domyślnych. Co więcej, jak się wkrótce przekonasz, z powodu prostego pominięcia właściwość a1.constructor może
prowadzić do zupełnie innego, zaskakującego miejsca.
Właściwość a1.constructor okazuje się wyjątkowo zawodna, więc opieranie
się w kodzie na tym odwołaniu jest dość niebezpieczne. Ogólnie rzecz biorąc,
należy unikać tego rodzaju odwołań, o ile to możliwe.
Dziedziczenie (prototypowe)
Wcześniej przedstawiłem pewne często stosowane w programach JavaScript
sztuczki, których celem jest zapewnienie funkcjonalności klas. Jednak klasy JavaScript pozostałyby puste, gdyby nie istniał mechanizm podobny do dziedziczenia.
Dziedziczenie (prototypowe)
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 123
0
Mechanizm powszechnie określany mianem dziedziczenia prototypowego
przedstawiłem w działaniu na przykładzie obiektu a, który miał możliwość
dziedziczenia po obiekcie Foo.prototype, a tym samym uzyskał dostęp do
funkcji myName(). Jednak dziedziczenie jest tradycyjnie traktowane jako związek między dwoma klasami, a nie między klasą i egzemplarzem, jak pokazałem na rysunku 5.2.
Rysunek 5.2. Graficzne przedstawienie dziedziczenia prototypowego
Powyższy rysunek powinieneś pamiętać z wcześniejszej części rozdziału.
Pokazuje nie tylko wzorzec delegowania z obiektu (tutaj „egzemplarza”) a1
do Foo.prototype, ale również z Bar.prototype do Foo.prototype, co czasami
przypomina koncepcję dziedziczenia klasy potomnej po nadrzędnej. Przypomina, ale oczywiście za wyjątkiem kierunku operacji, ponieważ tutaj mamy
do czynienia z łączami delegowania, a nie z operacją kopiowania.
Poniżej przedstawiłem typowy kod wykorzystujący „styl prototypu” odpowiedzialny za utworzenie wspomnianych łączy:
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
124 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
// W tym miejscu tworzymy nowy obiekt Bar.prototype
// połączony z Foo.prototype.
Bar.prototype = Object.create( Foo.prototype );
// Uważaj! Teraz nie ma już Bar.prototype.constructor
// i może wystąpić konieczność ręcznego poprawienia,
// jeżeli masz w zwyczaju opieranie się na tego rodzaju właściwościach!
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
Aby zrozumieć, dlaczego this w powyższym fragmencie kodu
prowadzi do a, zajrzyj do rozdziału 2.
Najważniejszym poleceniem jest Bar.prototype = Object.create( Foo.prototype ).
Wywołanie Object.create(..) powoduje utworzenie „nowego” obiektu oraz połączenie nowego obiektu ze wskazanym za pomocą [[Prototype]] (w omawianym przykładzie to Foo.prototype).
Innymi słowy, omawiany powyżej wiersz kodu można odczytać następująco:
„utwórz nowy obiekt określany mianem Bar dot prototype, który jest połączony
z obiektem nazywanym Foo dot prototype”.
Podczas deklarowania funkcji Bar (function Bar() { .. }), podobnie jak
każda inna funkcja, otrzymuje ona łącze .prototype prowadzące do obiektu
domyślnego. Jednak ten obiekt nie jest połączony z Foo.prototype, jak byśmy
tego chcieli. Dlatego też tworzymy nowy obiekt, który będzie połączony w oczekiwany sposób, a tym samym w efekcie pozbywamy się początkowo nieprawidłowo połączonego obiektu.
Dziedziczenie (prototypowe)
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 125
0
Bardzo często pojawiającym się tutaj nieporozumieniem jest to, że poniższe
podejście również działa, ale nie tak, jak można by oczekiwać:
// Poniższe polecenie nie działa tak, jak tego oczekujesz!
Bar.prototype = Foo.prototype;
// Poniższe polecenie mniej więcej działa w oczekiwany sposób,
// ale jednocześnie powoduje efekty uboczne, których nie chcesz :(
Bar.prototype = new Foo();
Polecenie Bar.prototype = Foo.prototype nie powoduje utworzenia nowego
obiektu, z którym zostanie połączony Bar.prototype. Wymienione polecenie
po prostu oznacza, że Bar.prototype stanie się kolejnym odwołaniem do
Foo.prototype, co efektywnie łączy Bar bezpośrednio z tym samym obiektem,
do którego prowadzi Foo: Foo.prototype. Oznacza to, że po rozpoczęciu stosowania przypisań, takich jak Bar.prototype.myLabel = ..., przeprowadzasz
modyfikację nie oddzielnego obiektu, ale współdzielonego Foo.prototype, co
może mieć wpływ na obiekty połączone z Foo.prototype. Prawie nigdy nie
oczekujesz właśnie takiego efektu. Jeżeli zaś chcesz otrzymać taki wynik, to
praktycznie w ogóle nie potrzebujesz Bar — powinieneś używać tylko Foo
i dzięki temu uprościć kod.
Polecenie Bar.prototype = new Foo() w rzeczywistości tworzy nowy obiekt,
który zgodnie z oczekiwaniami jest połączony z Foo.prototype. Do utworzenia
obiektu wykorzystaliśmy wywołanie konstruktora. Jeżeli wymieniona funkcja
powoduje jakiekolwiek efekty uboczne (na przykład rejestrację danych, zmianę
stanu, rejestrację innych obiektów, dodanie właściwości danych do this itd.),
wystąpią one w trakcie łączenia (prawdopodobnie z nieprawidłowym obiektem!), a nie tylko podczas tworzenia elementów potomnych Bar(), jak można
by tego oczekiwać.
Dlatego też pozostało nam jedynie użycie wywołania Object.create(..)
w celu utworzenia nowego obiektu, który będzie prawidłowo połączony z innym.
W ten sposób unikniemy efektów ubocznych występujących podczas wywoływania Foo(..). Małym minusem tego rozwiązania jest konieczność utworzenia nowego obiektu poprzez pozbycie się starego, a nie po prostu modyfikację istniejącego.
Byłoby dobrze, gdyby istniał standardowy i niezawodny sposób modyfikacji
połączenia z istniejącym obiektem. W specyfikacji wcześniejszej niż ES6 mamy
niestandardowy i nieobsługiwany we wszystkich przeglądarkach internetowych
126 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
sposób bazujący na właściwości .__proto__, której wartość można ustawić. Z kolei
w specyfikacji ES6 dodano funkcję pomocniczą Object.setPrototypeOf(..),
która wykonuje oczekiwane zadanie w standardowy i przewidywalny sposób.
Obie techniki (stosowaną przed wprowadzeniem specyfikacji ES6 i oferowaną
przez ES6) porównaj podczas łączenia Bar.prototype z Foo.prototype:
// Przed wprowadzeniem specyfikacji ES6.
// Pozbycie się domyślnego, istniejącego obiektu Bar.prototype.
Bar.prototype = Object.create( Foo.prototype );
// Specyfikacja ES6+.
// Modyfikacja istniejącego obiektu Bar.prototype.
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
Jeśli zignorujemy niewielki spadek wydajności (pozbycie się obiektu, który
później będzie usunięty przez mechanizm usuwania nieużytków) związany
z użyciem wywołania Object.create(..), to to podejście jest lepsze, gdyż jest
nieco krótsze i prawdopodobnie łatwiejsze w odczycie niż podejście wprowadzone w specyfikacji ES6+. Jednak skutek zastosowania obu rozwiązań jest taki sam.
Analiza związków „klasy”
Co można zrobić w sytuacji, gdy masz obiekt taki jak a i chcesz ustalić, do
jakiego (o ile w ogóle) obiektu się odwołuje (deleguje)? Analizę egzemplarza
(czyli po prostu obiektu w JavaScript) i sprawdzenie jego hierarchii dziedziczenia (połączeń mechanizmu delegowania w JavaScript) w tradycyjnych
środowiskach programowania obiektowego często określamy mianem introspekcji (lub refleksji).
Spójrz na poniższy fragment kodu:
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
W jaki sposób można przeprowadzić introspekcję obiektu a, aby poznać jego
przodków (łącza wzorca delegowania). Pierwsze podejście jest związane z nieporozumieniem dotyczącym „klasy”:
a instanceof Foo; // Prawda.
Dziedziczenie (prototypowe)
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 127
0
Operator instanceof pobiera zwykły obiekt jako lewy operand oraz funkcję jako
jego prawy operand. Pytanie, na które udziela odpowiedzi operator instanceof,
brzmi: czy w całym łańcuchu [[Prototype]] dla a istnieje obiekt wskazujący
na Foo.prototype?
Niestety oznacza to możliwość sprawdzenia przodków jedynie pewnego obiektu
(a) tylko wtedy, jeśli mamy funkcję (tutaj Foo wraz z dołączonym odwołaniem
.prototype) do przetestowania. Jeżeli masz dwa dowolne obiekty — powiedzmy
a i b — i chcesz dowiedzieć się, czy są one powiązane ze sobą za pomocą łańcucha [[Prototype]], operator instanceof na nic Ci się nie przyda.
Jeżeli za pomocą wbudowanej funkcji pomocniczej .bind() tworzysz funkcję, która ma na stałe zdefiniowane wywołanie funkcji
początkowej wraz z kontekstem this (patrz rozdział 2.), wówczas
ta nowa funkcja nie będzie miała właściwości .prototype. W takim przypadku operator instanceof wraz z tego rodzaju funkcją
po prostu zastąpi .prototype funkcją docelową, która została
utworzona na podstawie na stałe dołączonej funkcji.
Wcale nie tak rzadko zdarza się sytuacja, kiedy to na stałe zdefiniowane wywołanie funkcji początkowej wraz z kontekstem this
jest używane w charakterze „wywołania konstruktora”. Jednak
w takim przypadku wspomniana funkcja będzie się zachowywała,
jakby nastąpiło wywołanie początkowej funkcji docelowej. Oznacza to, że użycie operatora instanceof wraz z na stałe dołączoną funkcją również daje efekt jak w przypadku wywołania
funkcji pierwotnej.
Poniższy fragment kodu pokazuje śmieszność próby uzasadnienia związku między dwoma obiektami za pomocą semantyki „klasy” i operatora instanceof:
// Funkcja pomocnicza pozwalająca sprawdzić, czy obiekt 'o1'
// jest w jakikolwiek sposób powiązany (delegowany) z 'o2'.
function isRelatedTo(o1, o2) {
function F(){}
F.prototype = o2;
return o1 instanceof F;
}
var a = {};
var b = Object.create( a );
isRelatedTo( b, a ); // Prawda.
128 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Wewnątrz wywołania isRelatedTo(..) pożyczamy funkcję F, ponownie przypisujemy wartość jej właściwości .prototype, wskazując pewien obiekt o2, a następnie sprawdzamy, czy o1 jest egzemplarzem F. Oczywiście obiekt o1 w rzeczywistości nie dziedziczy po F, nie jest obiektem pochodnym F ani nawet nie
został skonstruowany na podstawie F. Dlatego też powinno być jasne i oczywiste,
że tego rodzaju ćwiczenie jest bezsensowne i mylące. Problem sprowadza się
do niewygody związanej z wymuszeniem stosowania semantyki klasy w JavaScript, w tym przypadku jako pośredniej semantyki operatora instanceof.
Drugie i znacznie prostsze podejście do refleksji [[Prototype]] przedstawia
się następująco:
Foo.prototype.isPrototypeOf( a ); // Prawda.
Zwróć uwagę, że w tym przypadku nie przejmujemy się (a nawet nie potrzebujemy) Foo, potrzebny jest jedynie obiekt (w omawianym przykładzie nazwany
Foo.prototype) do przetestowania z innym obiektem. Pytanie, na które
udziela odpowiedzi wywołanie isPrototypeOf(..), brzmi: czy w całym łańcuchu [[Prototype]] dla a istnieje obiekt wskazujący na Foo.prototype?
Mamy więc dokładnie takie samo pytanie jak wcześniej i dokładnie tę samą
odpowiedź. Jednak w omawianym tutaj podejściu tak naprawdę nie potrzebujemy pośredniego odwołania się do funkcji (Foo), której właściwość .prototype
będzie automatycznie sprawdzana.
Potrzebne są po prostu dwa obiekty, abyśmy mogli sprawdzić, czy zachodzi
między nimi jakikolwiek związek. Na przykład:
// Sprawdzamy, czy 'b' pojawia się w jakimkolwiek
// miejscu łańcucha [[Prototype]] obiektu 'c'.
b.isPrototypeOf( c );
Zwróć uwagę, że to podejście w ogóle nie wymaga funkcji („klasy”). Używa
jedynie bezpośrednio odwołań do obiektów b i c, a następnie sprawdza,
czy zachodzi między nimi związek. Innymi słowy, przedstawiona wcześniej
funkcja pomocnicza isRelatedTo(..) jest wbudowana w język i nosi nazwę
isPrototypeOf(..).
Istnieje również możliwość bezpośredniego pobrania łańcucha [[Prototype]]
obiektu. W przypadku specyfikacji ES5 standardowym sposobem wykonania
takiego zadania jest wywołanie:
Object.getPrototypeOf( a );
Dziedziczenie (prototypowe)
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 129
0
Jak możesz zobaczyć, odwołanie do obiektu jest zgodne z oczekiwaniami:
Object.getPrototypeOf( a ) === Foo.prototype; // Prawda.
Większość przeglądarek internetowych (choć nie wszystkie!) od dłuższego
czasu obsługuje niestandardowy, alternatywny sposób uzyskania dostępu do
[[Prototype]]:
a.__proto__ === Foo.prototype; // Prawda.
Dziwna właściwość .__proto__ (nieustandaryzowana aż do chwili wydania ES6!)
w „magiczny” sposób pobiera wewnętrzny łańcuch [[Prototype]] obiektu jako
odwołanie. To niezwykle użyteczne, gdy zachodzi potrzeba bezpośredniego
przeanalizowania łańcucha .__proto__.__proto__... lub nawet poruszania
się po nim.
Podobnie jak widziałeś wcześniej w przypadku właściwości .constructor,
.__proto__ tak naprawdę nie istnieje w analizowanym obiekcie (tutaj to a).
Natomiast w rzeczywistości istnieje (choć właściwość ta nie jest uwzględniana przez typy wyliczeniowe, patrz rozdział 2.) we wbudowanym obiekcie
Object.prototype wraz z innymi często używanymi funkcjami pomocniczymi,
takimi jak .toString(), .isPrototypeOf(..) itd.
Co więcej, .__proto__ wygląda jak właściwość, choć tak naprawdę lepszym
określeniem będzie getter lub setter (patrz rozdział 3.).
Implementacja .__proto__ może się z grubsza przedstawiać następująco
(w rozdziale 3. znajdziesz definicję właściwości obiektu):
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
// Wywołanie setPrototypeOf(..) w specyfikacji ES6.
Object.setPrototypeOf( this, o );
return o;
}
} );
Kiedy próbujemy uzyskać dostęp do właściwości a.__proto__ (inaczej: pobrać jej wartość), przypomina to wywołanie a.__proto__(), czyli wywołanie
funkcji typu getter. To wywołanie funkcji ma a jako wiązanie this, mimo że
funkcja typu getter istnieje w obiekcie Object.prototype (reguły wiązania
this omówiłem w rozdziale 2.). Dlatego też wymienione wywołanie odpowiada Object.getPrototypeOf( a ).
130 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Istnieje również możliwość przypisania wartości właściwości .__proto__, podobnie jak w pokazanym wcześniej przypadku użycia wprowadzonego w specyfikacji ES6 wywołania Object.setPrototypeOf(..). Ogólnie rzecz biorąc,
nie powinieneś zmieniać [[Prototype]] istniejącego obiektu.
Dostępne są pewne bardzo skomplikowane i zaawansowane techniki, używane
głęboko we frameworkach, pozwalające na zastosowanie sztuczek, takich jak tworzenie podklas tablicy (Array), ale korzystanie z nich nie jest zalecane, ponieważ
zwykle kończy się to powstaniem kodu znacznie trudniejszego do odczytu i obsługi.
W specyfikacji ES6 słowo kluczowe class pozwala na osiągnięcie efektu podobnego do tworzenia podklas dla wbudowanych
typów danych, takich jak Array. Analiza składni słowa kluczowego class wprowadzonego w specyfikacji ES6 znajduje się
w dodatku A.
Jedynym wyjątkiem (jak wcześniej wspomniałem) będzie takie ustawienie
[[Prototype]] dla domyślnego obiektu .prototype funkcji, aby wskazywał on
inny obiekt (poza Object.prototype). Tym samym unikniemy całkowitego
zastąpienia obiektu domyślnego nowym, połączonym obiektem. Poza tym
połączenie [[Prototype]] najlepiej traktować jako cechę charakterystyczną
tylko do odczytu, ułatwiającą później odczyt kodu.
Społeczność JavaScript wypracowała nieoficjalne wyrażenie dunder w odniesieniu do dwóch znaków podkreślenia stosowanych
we właściwościach takich jak __proto__. Dlatego też programiści JavaScript będą wymawiali __proto__ jako „dunder proto”.
Łącza obiektu
Jak teraz zobaczysz, mechanizm [[Prototype]] jest wewnętrznym łączem,
które istnieje w obiekcie odwołującym się do pewnego innego obiektu.
To połączenie jest wykorzystywane przede wszystkim podczas wykonywania
odwołania właściwości lub metody do pierwszego obiektu, a wskazana właściwość lub metoda nie istnieją. W takim przypadku połączenie [[Prototype]]
nakazuje silnikowi wyszukanie tej właściwości lub metody w obiekcie połączonym z bieżącym. Jeżeli także połączony obiekt nie może spełnić wyszukiwania, następuje wykorzystanie jego połączenia [[Prototype]] itd. Taka seria
połączeń między obiektami tworzy tak zwany łańcuch prototypu.
Łącza obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 131
0
Tworzenie łączy za pomocą create()
Wcześniej z grubsza wyjaśniłem, dlaczego oferowany przez JavaScript mechanizm [[Prototype]] nie przypomina klas. Dowiedziałeś się również, że tworzy
połączenia między prawidłowymi obiektami.
Mógłbyś w tym miejscu zapytać, jakie jest przeznaczenie mechanizmu [[Pro
totype]]. Dlaczego programiści JavaScript tak często ponoszą tak duży wysiłek, emulując klasy w kodzie, aby powiązać ze sobą wspomniane połączenia?
Czy pamiętasz, jak wcześniej w tym rozdziale stwierdziłem, że przydatne będzie
wywołanie Object.create(..)? Teraz możesz wreszcie zobaczyć dlaczego:
var foo = {
something: function() {
console.log( "Powiedz mi coś dobrego..." );
}
};
var bar = Object.create( foo );
bar.something(); // Powiedz mi coś dobrego...
Wywołanie Object.create(..) tworzy nowy obiekt (bar) połączony z podanym obiektem (foo), co daje nam dostęp do potężnych możliwości mechanizmu [[Prototype]]. Odbywa się to bez żadnych niepotrzebnych komplikacji
funkcji new działających jako klasy i bez wywołania konstruktora, mylących
odwołań .prototype i .constructor oraz wszelkich innych problemów.
Wywołanie Object.create(null) tworzy obiekt wraz z pustym
(null) połączeniem [[Prototype]], co oznacza, że obiekt nie może
być delegowany. Ponieważ tego rodzaju obiekt nie ma łańcucha
prototypu, operator instanceof (omówiony wcześniej w rozdziale) nie ma nic do sprawdzenia, a więc zawsze zwraca wartość false. Takie obiekty wraz z pustymi łączami [[Prototype]]
są często nazywane słownikami, ponieważ najczęściej są używane wyłącznie do przechowywania danych we właściwościach. Wynika to z faktu, że nie sprawiają żadnych problemów podczas obsługi delegowanych właściwości lub funkcji
w łańcuchu [[Prototype]], a tym samym są jednorodnym
magazynem danych.
132 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Nie potrzebujemy klas do tworzenia mających znaczenie związków między
dwoma obiektami. Tak naprawdę powinniśmy się przejmować jedynie łączeniem obiektów w celu zastosowania wzorca delegowania. Wywołanie
Object.create(..) umożliwia utworzenie połączenia bez całego bałaganu
związanego z klasami.
Skrypt typu polyfill dla Object.create()
Wywołanie Object.create(..) zostało wprowadzone w specyfikacji ES5. Być
może będziesz musiał zapewnić obsługę środowiska zgodnego z ES5 (na przykład starsze wersje przeglądarek IE). Dlatego też warto spojrzeć na prosty częściowy skrypt typu polyfill dla wywołania Object.create(..). Dzięki niemu
funkcjonalność wprowadzoną w nowych specyfikacjach ES będzie można stosować nawet w starszych wersjach środowisk JavaScript:
if (!Object.create) {
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
};
}
Powyższy skrypt typu polyfill działa przez wykorzystanie funkcji F: nadpisujemy
jej właściwość .prototype w taki sposób, aby prowadziła do obiektu, z którym
chcemy powiązać bieżący obiekt. Kolejnym krokiem jest użycie wywołania
new F() do utworzenia nowego obiektu zawierającego wskazane połączenie.
Z takim sposobem użycia wywołania Object.create(..) można się spotkać
najczęściej, ponieważ to jedno z wielu podejść, które może być obsłużone za pomocą skryptów typu polyfill. Wbudowane w ES5 wywołanie Object.create(..)
oferuje jeszcze inną funkcjonalność, która jednak nie może być wykorzystana
w środowiskach wcześniejszych niż ES5. Dlatego też te możliwości są już rzadziej stosowane. Aby przedstawione wyjaśnienie było pełne, warto spojrzeć
także na wspomnianą dodatkową funkcjonalność:
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject, {
b: {
enumerable: false,
Łącza obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 133
0
writable: true,
configurable: false,
value: 3
},
c: {
enumerable: true,
writable: false,
configurable: false,
value: 4
}
} );
myObject.hasOwnProperty( "a" ); // Fałsz.
myObject.hasOwnProperty( "b" ); // Prawda.
myObject.hasOwnProperty( "c" ); // Prawda.
myObject.a; // 2
myObject.b; // 3
myObject.c; // 4
Drugi argument wywołania Object.create(..) wskazuje nazwy właściwości,
które mają być dodane do nowo utworzonego obiektu za pomocą zadeklarowania deskryptora właściwości (patrz rozdział 3.) dla każdej nowej właściwości.
Ponieważ nie ma możliwości użycia skryptu typu polyfill do obsługi deskryptorów właściwości w środowisku wcześniejszym niż ES5, dodatkowa funkcjonalność wywołania Object.create(..) nie będzie obsługiwana za pomocą
takiego skryptu.
W większości przypadków użycia wywołania Object.create(..) stosowany
jest bezpieczny dla skryptów polyfill podzbiór funkcjonalności, dzięki czemu
programiści nie mają problemu ze stosowaniem częściowych skryptów typu
polyfill w środowiskach wcześniejszych niż ES5.
Część programistów stosuje znacznie bardziej restrykcyjne rozwiązanie, bazujące na podejściu, zgodnie z którym żadna funkcja nie powinna być obsługiwana przez skrypty typu polyfill, o ile nie będzie w pełni obsłużona. A że wywołanie Object.create(..) jest jednym z tych obsługiwanych częściowo, to jeśli
w środowiskach wcześniejszych niż ES5 potrzebujesz jakiejkolwiek funkcjonalności Object.create(..), zamiast skryptów typu polyfill zastosuj samodzielnie opracowane funkcje pomocnicze i unikaj rozwiązań opierających się
na Object.create(..). Możesz przygotować własną funkcję pomocniczą, na
przykład podobną do poniższej:
134 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
function createAndLinkObject(o) {
function F(){}
F.prototype = o;
return new F();
}
var anotherObject = {
a: 2
};
var myObject = createAndLinkObject( anotherObject );
myObject.a; // 2
Nie jestem zwolennikiem tego rodzaju restrykcyjnego podejścia. W pełni popieram zastosowanie przedstawionego wcześniej częściowego skryptu typu
polyfill dla wywołania Object.create(..) i użycie go w kodzie uruchamianym
nawet w środowiskach wcześniejszych niż ES5. Wybór należy do Ciebie.
Łącza jako rozwiązanie awaryjne?
Być może kusi Cię, by wspomniane tutaj łącza między obiektami potraktować
przede wszystkim jako pewnego rodzaju rozwiązanie awaryjne ze względu na
brak właściwości lub metod. Wprawdzie tego rodzaju podejście może przynieść pewne korzyści, ale nie jest to poprawny sposób wykorzystywania mechanizmu [[Prototype]].
Spójrz na poniższy fragment kodu:
var anotherObject = {
cool: function() {
console.log( "Świetnie!" );
}
};
var myObject = Object.create( anotherObject );
myObject.cool(); // "Świetnie!"
Powyższy fragment kodu działa ze względu na sposób funkcjonowania [[Pro
totype]]. Jeśli jednak utworzysz go w taki sposób, że anotherObject będzie
rozwiązaniem awaryjnym, gdy obiekt myObject nie może obsłużyć pewnej
właściwości lub metody, która może być wywoływana przez innego programistę, wówczas istnieje duże prawdopodobieństwo, że tworzone przez Ciebie
oprogramowanie będzie trudniejsze do zrozumienia i obsługi.
Łącza obiektu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 135
0
To oczywiście nie oznacza, że nie zdarzają się sytuacje, gdy tego rodzaju rozwiązanie jest odpowiednim wzorcem projektowym. W języku JavaScript nie
stosuje się go jednak zbyt często. Dlatego też jeśli korzystasz z niego w kodzie,
zrób przerwę, wykonaj krok wstecz i zastanów się, czy naprawdę wybrałeś
odpowiedni i rozsądny projekt.
W specyfikacji ES6 wprowadzono zaawansowaną funkcjonalność
o nazwie proxy, która może dostarczyć pewnego rodzaju zachowanie typu „metoda nie została znaleziona”. Omówienie proxy
wykracza poza zakres tematyczny książki, ale tą funkcjonalnością zajmę się szczegółowo w jednej z kolejnych książek serii.
Nie przegap tutaj ważnego punktu.
Jeżeli projektujesz oprogramowanie przeznaczone dla innych programistów
i na przykład opracujesz wywołanie myObject.cool(), które działa nawet
w przypadku braku metody cool() w obiekcie myObject, wprowadzasz pewien element zaskoczenia w projekcie API. Przyjęte rozwiązanie może być zaskoczeniem dla programistów, którzy będą się w przyszłości zajmować obsługą
utworzonego przez Ciebie oprogramowania.
Możesz jednak zaprojektować API w taki sposób, aby wyeliminować wspomniane zaskoczenie, a przy tym nadal wykorzystać potężne możliwości drzemiące w połączeniu za pomocą [[Prototype]]:
var anotherObject = {
cool: function() {
console.log( "Świetnie!" );
}
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
this.cool(); // Wewnętrzne delegowanie!
};
myObject.doCool(); // "Świetnie!"
W powyższym fragmencie kodu mamy wywołanie myObject.doCool(), które
jest metodą faktycznie istniejącą w obiekcie myObject. Dlatego też nasz projekt
API będzie jasno i wyraźnie sprecyzowany. Wewnętrznie nasza implementacja stosuje wzorzec projektu delegowania (patrz rozdział 6.) i wykorzystuje
zalety delegowania [[Prototype]] do anotherObject.cool().
136 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Innymi słowy, zastosowanie wzorca delegowania powoduje, że działanie oprogramowania okazuje się mniej zaskakujące i mylące, o ile wspomniany wzorzec będzie szczegółem wewnętrznej implementacji, a nie zostanie po prostu
udostępniony w projekcie interfejsu API. Szczegółowe omówienie wzorca delegowania znajdziesz w kolejnym rozdziale.
Podsumowanie
Próba uzyskania dostępu do nieistniejącej w obiekcie właściwości powoduje,
że wewnętrzne połączenie [[Prototype]] w obiekcie definiuje miejsce, w którym następnie powinna działać operacja [[Get]] (patrz rozdział 3.). Tego rodzaju kaskadowe połączenie obiektów w praktyce definiuje łańcuch prototypu (pod pewnymi względami podobny do łańcucha zagnieżdżonego zakresu)
obiektów analizowanych w celu znalezienia wskazanej właściwości.
Wszystkie zwykłe obiekty na początku swojego łańcucha prototypu (może
to na przykład być zakres globalny w przypadku operacji wyszukiwania elementu w danym zakresie) mają wbudowaną właściwość Object.prototype, na
której zatrzyma się operacja wyszukiwania, jeśli nigdzie wcześniej w łańcuchu
nie zostanie znaleziony szukany element. Obiekt Object.prototype zawiera
toString(), valueOf() oraz wiele innych funkcji pomocniczych, co wyjaśnia,
dlaczego wszystkie obiekty w języku mogą uzyskać do nich dostęp.
Najczęściej w celu połączenia dwóch obiektów ze sobą wykorzystuje się słowo
kluczowe new wraz z wywołaniem funkcji. Wspomniane wywołanie jest jednym
z czterech kroków (patrz rozdział 2.) procedury odpowiedzialnej za utworzenie
nowego obiektu połączonego z innym obiektem.
Taki „inny obiekt” połączony z nowym obiektem jest wskazywany przez właściwość o nazwie .prototype funkcji wywoływanej wraz ze słowem kluczowym new. Funkcje wywoływane wraz z new są często nazywane konstruktorami, mimo że w rzeczywistości nie tworzą egzemplarza klasy, jak ma to miejsce
w przypadku konstruktorów w tradycyjnych językach obiektowych.
Mimo że te mechanizmy JavaScript przypominają tworzenie egzemplarza klasy
i dziedziczenie klasy, znane z tradycyjnych języków programowania obiektowego,
to nie można zapominać tutaj o podstawowej różnicy między nimi: w JavaScript
nie mamy do czynienia z tworzeniem kopii obiektu. Zamiast tego obiekty są
ze sobą łączone za pomocą wewnętrznego łańcucha [[Prototype]].
Podsumowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 137
0
Z wielu różnych powodów, nie tylko związanych z terminologią, wyrażenia
takie jak „dziedziczenie” i „dziedziczenie prototypowe” oraz pozostałe pojęcia
programowania obiektowego nie mają większego sensu, jeśli wziąć pod uwagę
rzeczywisty sposób działania JavaScript (a nie jedynie modele układane w myślach przez programistę).
Zamiast tego „wzorzec delegowania” to znacznie odpowiedniejsze pojęcie, ponieważ wspomniane związki nie są kopiami, ale łączami wzorca delegowania.
138 
Rozdział 5. Prototypy
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
ROZDZIAŁ 6.
Delegowanie
W rozdziale 5. dokładnie omówiłem mechanizm [[Prototype]] — dowiedziałeś się, dlaczego pomimo podejmowanych przez niemal dwie dekady niezliczonych prób opisywanie go w kontekście klasy lub dziedziczenia jest mylące.
Mozolnie przebrnęliśmy nie tylko przez całkiem rozwlekłą składnię (porozrzucane po całym kodzie .prototype), ale także przez różne pułapki (na przykład
zaskakujące rozwiązanie właściwości .constructor lub brzydka, pseudopolimorficzna składnia). Przeanalizowaliśmy również warianty podejścia opartego
na domieszkach, które jest przez wielu programistów stosowane w celu ułatwienia pracy.
W tym miejscu możesz się zastanawiać, jaki jest sens stosowania tak skomplikowanego rozwiązania do wykonania — jak się wydaje — prostego zadania.
Skoro poznałeś wszystkie nieciekawe szczegóły wspomnianego rozwiązania,
nie powinno być zaskoczeniem, że większość programistów JavaScript nie decyduje się na jego stosowanie. Zamiast tego obsługę wszelkich szczegółów wybranego podejścia przerzuca na bibliotekę „klas”.
Mam nadzieję, że nie wystarczy Ci powierzchowna wiedza i nie zamierzasz
ukryć wszystkich szczegółów w „czarnym pudełku” biblioteki. Przejdźmy dalej,
aby zobaczyć, jak możemy i jak powinniśmy traktować mechanizm [[Prototype]]
w JavaScript w znacznie prostszy i bardziej bezpośredni sposób niż w oparciu
o klasy.
W podsumowaniu rozdziału 5. stwierdziłem, że mechanizm [[Prototype]]
jest wewnętrznym połączeniem istniejącym w obiekcie, który odwołuje się do
innego obiektu.
139
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Wspomniane połączenie jest wykorzystywane podczas odwoływania się właściwości lub metody do pierwszego obiektu, a wskazana właściwość lub metoda
nie istnieją. W takim przypadku połączenie [[Prototype]] nakazuje silnikowi
wyszukanie tej właściwości lub metody w obiekcie połączonym z bieżącym.
A jeżeli również ten połączony obiekt nie może spełnić wyszukiwania, wykorzystywane jest jego połączenie [[Prototype]] itd. Taka seria połączeń między
obiektami tworzy tak zwany łańcuch prototypu.
Innymi słowy, rzeczywisty mechanizm to obiekty połączone z innymi obiektami. Daje nam on dostęp do bardzo ważnej funkcjonalności oferowanej przez
JavaScript.
To stwierdzenie jest fundamentalne i ma krytyczne znaczenie dla zrozumienia
materiału przedstawionego w pozostałej części rozdziału!
Projekt oparty na delegowaniu
Aby skoncentrować się na użyciu [[Prototype]] w najbardziej bezpośredni
sposób, konieczne jest zrozumienie, że ten mechanizm przedstawia zupełnie
inny wzorzec projektowy niż klasa (patrz rozdział 4.).
Pewne reguły programowania obiektowego nadal zachowują
ważność, nie odrzucaj więc zdobytej dotąd wiedzy (a jedynie
jej większość!). Na przykład hermetyzacja to technika o całkiem
potężnych możliwościach, a na dodatek jest ona zgodna ze wzorcem delegowania (choć niezbyt powszechnie stosowana).
Musisz podjąć próbę zmiany sposobu myślenia; zamiast wzorca opartego na
klasach i dziedziczeniu lepiej skorzystać ze wzorca bazującego na delegowaniu. Jeżeli do tej pory w pracy programisty wykorzystywałeś przede wszystkim
klasy, wspomniana zmiana może się okazać niekomfortowa i nienaturalna.
Nieuniknione, że będzie musiało upłynąć nieco czasu, zanim przestawisz się
na nowy sposób myślenia.
Zacznę od przedstawienia pewnych ćwiczeń teoretycznych, a dopiero później
przejdziemy do konkretnych przykładów przedstawiających praktyczny kontekst tworzenia kodu.
140 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Teoria klasy
Przyjmujemy założenie, że mamy wiele podobnych zadań (XYZ, ABC itd.),
które mają być wykonane przez oprogramowanie.
W przypadku podejścia bazującego na klasach taki projekt może się przedstawiać następująco: definiujemy ogólną klasę nadrzędną (bazową), na przykład
Task, w której będzie zdefiniowane współdzielone zachowanie dla wszystkich
tego rodzaju zadań. Następnie przygotowujemy klasy potomne: XYZ i ABC,
dziedziczące po Task, z których każda zawiera specjalizowane zachowanie
przeznaczone do obsługi odpowiedniego zadania.
Co ważniejsze, projekt oparty na wzorcu klas zachęca do zastosowania możliwości przeciążania metod (i polimorfizmu), aby w maksymalnym stopniu
wykorzystać dziedziczenie. Dzięki temu w klasach potomnych (na przykład
XYZ) będzie można nadpisywać definicje pewnych ogólnych metod klasy Task,
prawdopodobnie wykorzystując wywołanie super w celu odwołania się do
wersji bazowej danej metody podczas dodawania kolejnego zachowania do
klasy. Ponadto istnieje duże prawdopodobieństwo, że znajdziesz kilka miejsc,
w których można zmienić ogólne zachowanie klasy nadrzędnej i dostosować
je do własnych potrzeb w klasach potomnych.
Poniżej przedstawiłem pewien luźny pseudokod dla powyższej sytuacji:
class Task {
id;
// Konstruktor Task().
Task(ID) { id = ID; }
outputTask() { output( id ); }
}
class XYZ inherits Task {
label;
// Konstruktor XYZ().
XYZ(ID,Label) { super( ID ); label = Label; }
outputTask() { super(); output( label ); }
}
class ABC inherits Task {
// …
}
Projekt oparty na delegowaniu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 141
0
Teraz można utworzyć jedną lub większą liczbę kopii klasy potomnej XYZ,
a następnie wykorzystać te egzemplarze do wykonania zadania XYZ. Wspomniane egzemplarze będą miały skopiowane zachowanie zarówno ogólnej
klasy Task, jak i konkretnej XYZ. Podobna sytuacja zachodzi w przypadku egzemplarzy klasy ABC, które będą zawierały kopie zachowania ogólnej klasy Task
i konkretnej ABC. Po utworzeniu egzemplarzy klas XYZ i ABC, ogólnie rzecz biorąc,
będziesz prowadził interakcje jedynie z tymi egzemplarzami (a nie klasami),
ponieważ każdy z nich zawiera skopiowane wszystkie funkcje, jakie są niezbędne do wykonania danego zadania.
Teoria delegowania
Spróbujemy teraz rozważyć ten sam problem, ale opierając się na delegowaniu,
a nie na klasach.
Przede wszystkim zaczynamy od zdefiniowania obiektu (a nie, jak sądzi większość programistów, klasy czy funkcji) o nazwie Task, zawierającego między
innymi różne metody pomocnicze, które później będą mogły być używane do
wykonywania różnych zadań (czytaj: będzie można stosować delegowanie do
nich!). Następnie dla każdego zadania (XYZ, ABC) definiujemy obiekt przechowujący dane i zachowanie typowe dla danego zadania. Obiekt lub obiekty
charakterystyczne dla danego zadania łączymy z obiektem narzędziowym Task,
umożliwiając tym samym ich delegowanie, gdy zachodzi potrzeba.
Zasadniczo możemy uznać za konieczne wykorzystanie zachowania z dwóch
bliźniaczych obiektów (XYZ i Task) do wykonania zadania XYZ. Jednak zamiast
łączyć je razem za pomocą kopii klas, zachowujemy oddzielne obiekty i pozwalamy obiektowi XYZ na delegowanie do Task, gdy zachodzi taka potrzeba.
Poniżej przedstawiłem prosty fragment kodu, sugerujący, jak można wykonać
omówione powyżej zadanie:
Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log( this.id ); }
};
// Obiekt XYZ deleguje do obiektu Task.
XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
this.setID( ID );
142 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
this.label = Label;
};
XYZ.outputTaskDetails = function() {
this.outputID();
console.log( this.label );
};
// ABC = Object.create( Task );
// ABC… = …
W powyższym fragmencie kodu Task i XYZ nie są klasami (ani funkcjami)
— to po prostu obiekty. Obiekt XYZ został skonfigurowany za pomocą
Object.create(..) jako delegat [[Prototype]] do obiektu Task (patrz
rozdział 5.).
W porównaniu do podejścia opartego na klasach (inaczej zorientowanego
obiektowo) ten styl nazywam kodem OLOO (ang. object linked to other objects — obiekty połączone z innymi obiektami). Wszystko, co tak naprawdę
nas interesuje, to fakt, że obiekt XYZ deleguje do obiektu Task (podobnie czyni
także obiekt ABC).
W języku JavaScript mechanizm [[Prototype]] łączy obiekty z innymi obiektami. Nie ma mechanizmu abstrakcji takiego jak „klasy”, niezależnie od tego,
jak bardzo próbujesz przekonać samego siebie, że jest dokładnie na odwrót.
Tę sytuację można porównać do płynięcia łodzią pod prąd: wprawdzie można
to zrobić, ale oznacza to pogodzenie się z tym, że będzie znacznie trudniej
dotrzeć do miejsca docelowego.
Poniżej przedstawiłem inne różnice dotyczące kodu w stylu OLOO, o których
warto wiedzieć:
1. Elementy składowe id i label z przedstawionego wcześniej przykładu
opartego na klasach są właściwościami danych bezpośrednio w XYZ (nie
istnieją w klasie Task). Ogólnie rzecz biorąc, w przypadku delegowania za
pomocą [[Prototype]] oczekujemy przechowywania informacji o stanie
za pomocą wzorca delegatora (XZY, ABC), a nie w delegacie (Task).
2. W przypadku projektu opartego na klasach celowo użyliśmy nazwy
outputTask w klasie nadrzędnej (Task) i potomnej (XYZ), aby można było
wykorzystać zalety płynące z przeciążania (polimorfizm). W przypadku
delegowania postępujemy odwrotnie: jeżeli to możliwe, unikamy nadawania tych samych nazw na różnych poziomach łańcucha [[Prototype]]
Projekt oparty na delegowaniu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 143
0
(tak zwane przesłanianie — patrz rozdział 5.), ponieważ tego rodzaju kolizje nazw prowadzą do niewygodnej i zawodnej składni, utrudniającej
rozróżnianie odwołań (patrz rozdział 4.). Dlatego też staramy się unikać
takiej sytuacji, o ile to możliwe.
Ten wzorzec projektowy zniechęca do używania ogólnych nazw metod,
które są podatne na przeciążenie. Zamiast tego zalecane jest stosowanie
znacznie bardziej opisowych nazw metod, konkretnych dla rodzaju zachowania oferowanego przez dany obiekt. Efektem będzie powstanie kodu łatwiejszego do zrozumienia i późniejszej obsługi, ponieważ nazwy
metod (nie tylko w miejscu ich zdefiniowania, ale także rozrzucone po
całym kodzie) są znacznie bardziej oczywiste i pomagają w samodokumentacji kodu źródłowego.
3. Polecenie this.setID(ID); wewnątrz metody w obiekcie XYZ najpierw
sprawdza XYZ dla setID(..), ale ponieważ nie znajduje w XYZ metody
o podanej nazwie, delegowanie [[Prototype]] oznacza możliwość przejścia
do Task i przeprowadzenia w wymienionym obiekcie operacji wyszukiwania dla setID(..), która zakończy się powodzeniem. Co więcej, ze
względu na reguły wiązania this (patrz rozdział 2.) podczas wykonywania funkcji setID(..) wiązaniem this dla funkcji będzie XYZ, tak jak tego
oczekujemy i chcemy. Dzieje się tak mimo znalezienia w obiekcie Task
szukanej metody. Taka sama sytuacja występuje także w dalszej części listingu, a dokładnie w this.outputID().
Innymi słowy, ogólne metody narzędziowe istniejące w obiekcie Task są
dostępne do użycia podczas interakcji z XYZ, ponieważ obiekt XYZ ma
możliwość delegowania do Task.
Zachowanie bazujące na delegowaniu oznacza pozwolenie pewnemu obiektowi (XYZ) na delegowanie (do Task) w celu uzyskania dostępu do właściwości
lub metody, jeśli nie zostaną one znalezione w obiekcie (XYZ).
Ten wzorzec projektowy oferuje niezwykle potężne możliwości i jest zupełnie
odmienny od wzorca klas, dziedziczenia, polimorfizmu itd. Zamiast układać
w myślach klasy pionowo, począwszy od nadrzędnej do potomnych, wyobraź
sobie, że obiekty są ułożone obok siebie i zawierają między sobą niezbędne
łącza delegowania w dowolnych kierunkach.
144 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Znacznie poprawniejsze będzie użycie delegowania w charakterze
wewnętrznych szczegółów implementacji, a nie jego udostępnienie bezpośrednio w projekcie interfejsu API. W poprzednim
przykładzie intencją niekoniecznie było umożliwienie programistom wywoływania XYZ.setID() w naszym projekcie API
(choć oczywiście można się na to zgodzić!). W pewien sposób
ukryliśmy delegowanie jako wewnętrzny szczegół implementacji API, w której wywołanie XYZ.prepareTask(..) jest delegowane do Task.setID(..). Więcej informacji na ten temat
znajdziesz w rozdziale 5., a dokładnie w sekcji „Łącza jako
rozwiązanie awaryjne?”.
Wzajemne delegowanie (zabronione)
Nie można utworzyć cyklu, w którym dwa obiekty lub większa ich liczba są do
siebie wzajemnie delegowane (dwukierunkowo). Jeżeli obiekt B połączysz z A,
a następnie spróbujesz połączyć A z B, otrzymasz błąd.
Nie powinno być zaskoczeniem, choć jednocześnie jest to nieco irytujące,
że wspomniane wzajemne delegowanie jest zabronione. Jeżeli wykonasz
odwołanie do właściwości lub metody nieistniejącej w żadnym ze sprawdzanych miejsc, wówczas powstanie trwająca w nieskończoność rekurencja pętli
[[Prototype]]. Jeżeli wszystkie odwołania będą istniały, wówczas B może delegować do A i na odwrót, a tego rodzaju rozwiązanie mogłoby funkcjonować.
W ten sposób dowolny obiekt można delegować do innego w celu wykonywania różnych zadań. Jest jednak tylko kilka sytuacji, w których tego rodzaju
podejście byłoby użyteczne.
Wzajemne delegowanie jest zabronione, ponieważ programiści pracujący nad
implementacjami silników JavaScript zauważyli, że większą wydajność ma
jednokrotne wykonywanie operacji sprawdzenia pod kątem trwającego w nieskończoność odwołania cyklicznego (i jego odrzucenie!) niż przeprowadzanie
sprawdzania w trakcie każdej operacji wyszukiwania właściwości w obiekcie.
Debugowanie
Pokrótce zajmiemy się drobnym szczegółem, który może wprowadzić nieco
zamieszania wśród programistów. Ogólnie rzecz biorąc, specyfikacja JavaScript nie kontroluje sposobu, w jaki narzędzia programistyczne wbudowane
w przeglądarkę internetową powinny przedstawiać programistom określone
Projekt oparty na delegowaniu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 145
0
wartości lub struktury. Dlatego też interpretacja w tym zakresie może się różnić
w poszczególnych przeglądarkach i silnikach, a stąd częste rozbieżności między
nimi. Przeanalizujemy teraz zachowanie zaobserwowane jedynie w narzędziach
programistycznych przeglądarki Chrome.
Spójrz na konstruktor klasy w tradycyjnym stylu kodu JavaScript, jaki zostanie
wyświetlony w konsoli narzędzi programistycznych przeglądarki internetowej
Chrome:
function Foo() {}
var a1 = new Foo();
a1; // Foo {}
Przyjrzyj się ostatniemu wierszowi w powyższym fragmencie kodu: dane wyjściowe po oszacowaniu wyrażenia a1 to Foo {}. Jeżeli ten sam kod wypróbujesz
w przeglądarce internetowej Firefox, prawdopodobnie zobaczysz Object {}.
Na czym polega różnica? Co oznaczają te dane wyjściowe?
Zachowanie przeglądarki Chrome można wyjaśnić następująco: „{} to pusty
obiekt, który został skonstruowany przez funkcję o nazwie Foo”. Z kolei wyjaśnienie zachowania przeglądarki Firefox jest następujące: „{} to pusty obiekt
ogólnej konstrukcji Object”. Subtelna różnica polega na tym, że przeglądarka
Chrome aktywnie monitoruje — za pomocą wewnętrznej właściwości — nazwę
rzeczywistej funkcji, która skonstruowała obiekt, podczas gdy inne przeglądarki nie śledzą tych dodatkowych informacji.
Kusząca może być próba wyjaśnienia tego za pomocą mechanizmów JavaScript:
function Foo() {}
var a1 = new Foo();
a1.constructor; // Foo(){}
a1.constructor.name; // "Foo"
Czy przeglądarka internetowa Chrome wyświetla Foo tylko z powodu sprawdzenia właściwości .constructor.name obiektu? Zaskakujące, ale odpowiedź
to jednocześnie i tak, i nie.
Spójrz na poniższy fragment kodu:
function Foo() {}
var a1 = new Foo();
146 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Foo.prototype.constructor = function Gotcha(){};
a1.constructor; // Gotcha(){}
a1.constructor.name; // "Gotcha"
a1; // Foo {}
Wprawdzie zmieniamy a1.constructor.name na wciąż prawidłową nazwę
(Gotcha), ale w konsoli przeglądarki Chrome nadal wyświetlana jest nazwa Foo.
A zatem wydaje się, że odpowiedź na zadane powyżej pytanie (czy używana
jest właściwość .constructor.name) brzmi nie — przeglądarka musi sprawdzić nazwę obiektu gdzieś indziej, wewnętrznie.
Nie tak szybko! Przekonajmy się jeszcze, jak tego rodzaju zachowanie sprawdza się w przypadku kodu w stylu OLOO:
var Foo = {};
var a1 = Object.create( Foo );
a1; // Object {}
Object.defineProperty( Foo, "constructor", {
enumerable: false,
value: function Gotcha(){}
});
a1; // Gotcha {}
W omawianym przykładzie konsola przeglądarki internetowej Chrome znalazła
właściwość .constructor.name i skorzystała z niej. Tak naprawdę w trakcie powstawania książki to konkretne zachowanie zostało uznane za błąd w Chrome,
więc kiedy czytasz te słowa, błąd może już być usunięty. Dlatego też wynikiem
może być a1; // Object {}.
Pomijając już kwestię błędu, wewnętrzne monitorowanie (w rzeczywistości
jedynie w celu debugowania danych wyjściowych) nazwy konstruktora przez
Chrome (jak pokazałem we wcześniejszym fragmencie kodu) jest przeznaczonym tylko dla tej przeglądarki celowym rozszerzeniem zachowania wykraczającego poza te zdefiniowane w specyfikacji JavaScript.
Jeżeli do utworzenia obiektów nie używasz konstruktora, do czego zniechęcam
w tym rozdziale w przypadku stosowania kodu w stylu OLOO, otrzymasz obiekty, których nazwa konstruktora nie jest wewnętrznie monitorowana przez
Chrome. Tego rodzaju obiekty zostaną wyświetlone prawidłowo jako Object
{}, co oznacza „obiekt wygenerowany na podstawie konstrukcji Object()”.
Projekt oparty na delegowaniu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 147
0
Nawet nie myśl, że można to uznać za wadę tworzenia kodu w stylu OLOO.
Podczas przygotowywania kodu w stylu OLOO opartego na wzorcu projektowym delegowania to, kto „skonstruował” (to znaczy, która funkcja została
wywołana wraz ze słowem kluczowym new) pewien obiekt, jest nieistotnym
szczegółem. Charakterystyczne dla przeglądarki internetowej Chrome wewnętrzne monitorowanie nazwy konstruktora okazuje się naprawdę użyteczne
tylko wtedy, gdy w pełni korzystasz ze stylu programowania z użyciem klas.
Jednak monitorowanie to staje się dyskusyjne, gdy zamiast klas stosujesz styl
delegowania i OLOO.
Porównanie modeli mentalnych
Teraz, skoro przynajmniej teoretycznie możesz dostrzec różnicę między wzorcami projektowymi „klasy” i „delegowania”, spójrzmy na implikacje zastosowania tych wzorców projektowych w przypadku modeli mentalnych używanych przez nas do uzasadnienia tworzonego kodu.
Przeanalizujemy pewien bardziej teoretyczny (Foo i Bar) kod i porównamy
oba podejścia (OOP kontra OLOO) w zakresie implementacji kodu. Pierwszy
fragment przedstawia przykład użycia klasycznego („prototypowego”) stylu
programowania obiektowego:
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "Jestem " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Witaj, " + this.identify() + "!" );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();
148 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Klasa nadrzędna Foo jest dziedziczona przez klasę potomną Bar, która następnie została użyta do utworzenia dwóch egzemplarzy b1 i b2. Mamy więc
do czynienia z delegowaniem b1 do obiektu Bar.prototype, który z kolei deleguje do Foo.prototype. Na tym etapie rozwiązanie powinno wyglądać znajomo. Nie znajdziesz w nim żadnych nowych elementów.
Przechodzimy teraz do implementacji dokładnie tej samej funkcjonalności
za pomocą kodu w stylu OLOO:
Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "Jestem " + this.me;
}
};
Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Witaj, " + this.identify() + "!" );
};
var b1 =
b1.init(
var b2 =
b2.init(
Object.create( Bar );
"b1" );
Object.create( Bar );
"b2" );
b1.speak();
b2.speak();
Wykorzystujemy tę samą zaletę delegowania [[Prototype]] z b1 do Bar i później do Foo jak w poprzednim fragmencie kodu między b1, Bar.prototype
i Foo.prototype. Wciąż mamy te same trzy obiekty powiązane ze sobą.
Jednak, co ważniejsze, znacznie uprościliśmy rozwiązanie, ponieważ teraz
jedynie konfigurujemy połączone ze sobą obiekty. Unikamy powstawania niepotrzebnych elementów i ogólnie całego zamieszania związanego z elementami
przypominającymi klasy, konstruktory, prototypy i wywołania new (ale nie
zachowującymi się jak one!).
Zadaj sobie pytanie: czy za pomocą kodu w stylu OLOO mogę uzyskać tę samą
funkcjonalność, jaką mam dzięki kodowi w stylu programowania obiektowego?
Skoro podejście oparte na OLOO jest prostsze i wymaga zajęcia się mniejszą
liczbą szczegółów, to czy nie będzie lepsze?
Projekt oparty na delegowaniu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 149
0
Przeanalizujmy teraz modele zastosowane w obu przedstawionych powyżej
fragmentach kodu.
Na początek fragment kodu w stylu programowania zorientowanego obiektowo wymusza zastosowanie modelu mentalnego encji i związków zachodzących między nimi, jak pokazałem na rysunku 6.1.
Rysunek 6.1. Graficzne przedstawienie kodu w stylu programowania zorientowanego obiektowo
W rzeczywistości to nieco nie w porządku i mylące, ponieważ kod zawiera
wiele dodatkowych szczegółów, których technicznie nie musisz znać (choć
powinieneś je zrozumieć!). Mamy tutaj do czynienia z całkiem skomplikowaną serią relacji. Jeżeli poświęcisz nieco czasu na śledzenie relacji wskazywanych przez strzałki, będziesz zaskoczony wewnętrzną spójnością w mechanizmach JavaScript.
150 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Na przykład funkcja JavaScript może uzyskać dostęp do call(..), apply(..)
i bind(..) (patrz rozdział 2.), ponieważ funkcje są obiektami i mają połączenie [[Prototype]] z obiektem Function.prototype definiującym wymienione
metody domyślne. Następnie do nich może być delegowana dowolna funkcjaobiekt. Skoro JavaScript może to zrobić, możesz i Ty!
Dobrze, spójrzmy teraz (patrz rysunek 6.2) na uproszczoną wersję diagramu,
która pozwala na nieco bardziej „uczciwe” porównanie — zawiera jedynie
niezbędne encje i relacje.
Rysunek 6.2. Uproszczona wersja wykresu przedstawiającego kod w stylu
programowania obiektowego
Projekt oparty na delegowaniu
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 151
0
Nadal mamy całkiem skomplikowany wykres, prawda? Przerywane linie pokazują implikowane relacje podczas konfiguracji dziedziczenia między Foo.
prototype i Bar.prototype, przy czym nie uwzględniono poprawionego
brakującego odwołania .constructor (patrz sekcja „Powrót konstruktora”
w rozdziale 5.). Nawet po usunięciu wspomnianych przerywanych linii model
wciąż pozostaje skomplikowany, co oznacza dla nas więcej pracy podczas korzystania z połączonych obiektów.
Teraz spójrz na pokazany na rysunku 6.3 model przeznaczony dla kodu
w stylu OLOO.
Rysunek 6.3. Graficzne przedstawienie kodu w stylu OLOO
152 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Jak możesz zobaczyć, porównując przedstawione modele, w przypadku stylu
OLOO nie musisz się przejmować wieloma szczegółami. W stylu OLOO wykorzystujemy fakt, że jedyną interesującą nas zawsze kwestią są obiekty połączone z innymi obiektami.
Wszelkie pozostałe szczegóły związane z „klasami” są mylące i niepotrzebnie
komplikują rozwiązanie, które daje taki sam efekt jak w stylu OLOO. Pozbądź
się wspomnianych elementów, a wszystko stanie się znacznie prostsze (bez
utraty jakiejkolwiek funkcjonalności).
Klasy kontra obiekty
Przedstawiłem już różne teoretyczne i mentalne modele dla rozwiązań opartych
na „klasach” oraz „wzorcu delegowania”. Warto teraz spojrzeć na konkretne scenariusze kodu, aby poznać praktyczne przykłady użycia omówionych koncepcji.
Na początek zabieramy się za typowy scenariusz programisty sieciowego, czyli
utworzenie widżetów interfejsu użytkownika (przyciski, rozwijane menu itd.).
„Klasy” widżetów
Ponieważ prawdopodobnie wciąż jesteś przywiązany do wzorca projektowego
programowania zorientowanego obiektowo, więc przygotowując rozwiązanie
danego problemu, zwykle opierasz je na klasie nadrzędnej (na przykład o nazwie Widget), definiującej całą podstawową funkcjonalność widżetu, i klasach
potomnych (na przykład Button) przeznaczonych dla poszczególnych typów
widżetów.
W tym miejscu do przeprowadzenia operacji zarówno na modelu DOM, jak i tych związanych z CSS wykorzystamy bibliotekę jQuery, ale tylko dlatego, że są to szczegóły zupełnie
nieistotne dla przedstawianego tutaj wyjaśnienia. Tego rodzaju
kod nie ma żadnego związku z frameworkami JS (jQuery, Dojo,
YUI itd.), które możesz wykorzystać do wykonania tak przyziemnych zadań.
Zaczynamy od analizy sposobu implementacji projektu „klasy” w klasycznym stylu JavaScript bez użycia jakiejkolwiek składni lub biblioteki pomocniczej „klas”:
Klasy kontra obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 153
0
// Klasa nadrzędna.
function Widget(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
Widget.prototype.render = function($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
};
// Klasa potomna.
function Button(width,height,label) {
// Wywołanie konstruktora "super".
Widget.call( this, width, height );
this.label = label || "Domyślny";
this.$elem = $( "<button>" ).text( this.label );
}
// Klasa 'Button' "dziedziczy" po klasie 'Widget'.
Button.prototype = Object.create( Widget.prototype );
// Nadpisanie bazowej "odziedziczonej" metody render(..).
Button.prototype.render = function($where) {
// Wywołanie "super".
Widget.prototype.render.call( this, $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.prototype.onClick = function(evt) {
console.log( "Przycisk '" + this.label + "' został kliknięty!" );
};
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = new Button( 125, 30, "Witaj," );
var btn2 = new Button( 150, 40, "świecie" );
btn1.render( $body );
btn2.render( $body );
} );
Wzorce projektowe programowania obiektowego nakazują zadeklarowanie metody bazowej render(..) w klasie nadrzędnej, a następnie nadpisanie jej w klasie
potomnej. Nie mamy tutaj do czynienia z całkowitym zastąpieniem metody,
154 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
ale raczej ze zmianą jej podstawowej funkcjonalności i dopasowaniem do zachowania charakterystycznego dla przycisku.
W omawianym kodzie zwróć uwagę na wyraźny pseudopolimorfizm (patrz
rozdział 4.) w postaci odwołań Widget.call i Widget.prototype.render.call,
zastosowany w celu oszukania wywołań „super” z poziomu metod „klasy”
potomnej do metod bazowych „klasy nadrzędnej”. Fuj!
Składnia class w ES6
Dokładne omówienie składni słowa kluczowego class wprowadzonego w specyfikacji ES6 znajdziesz w dodatku A. Tutaj chcę jedynie pokrótce pokazać,
jak ten sam kod można zaimplementować właśnie z użyciem class:
class Widget {
constructor(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
}
class Button extends Widget {
constructor(width,height,label) {
super( width, height );
this.label = label || "Domyślny";
this.$elem = $( "<button>" ).text( this.label );
}
render($where) {
super( $where );
this.$elem.click( this.onClick.bind( this ) );
}
onClick(evt) {
console.log( "Przycisk '" + this.label + "' został kliknięty!" );
}
}
$( document ).ready( function(){
var $body = $( document.body );
Klasy kontra obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 155
0
var btn1 = new Button( 125, 30, "Witaj," );
var btn2 = new Button( 150, 40, "świecie" );
btn1.render( $body );
btn2.render( $body );
} );
Nie ulega wątpliwości, że pewne brzydkie fragmenty składni widoczne w poprzednim podejściu klasycznym zostały wyeliminowane dzięki użyciu słowa
kluczowego class. Obecność wywołania super(..) wydaje się szczególnie
użyteczna (choć kiedy się dokładniej nad tym zastanowisz, nie wszystko jest
usłane różami!).
Pomimo syntaktycznych usprawnień nie są to rzeczywiste klasy, ponieważ ich
działanie wciąż bazuje na mechanizmie [[Prototype]]. Problemem nadal są
te same niedopasowania, które wyjaśniłem w rozdziałach 4. i 5. oraz w bieżącym.
W dodatku A dokładnie omówię wprowadzoną w specyfikacji ES6 składnię
słowa kluczowego class i związane z nim implikacje. Dowiesz się, dlaczego
rozwiązanie drobnych problemów ze składnią nie oznacza uniknięcia zamieszania dotyczącego klas w JavaScript, mimo wysiłków programistów!
Niezależnie od tego, czy używasz klasycznej składni prototypów, czy nowej,
wprowadzonej przez ES6, nadal dokonujesz wyboru modelowania problemu
(widżety interfejsu użytkownika) za pomocą „klas”. Jak to próbowałem pokazać
w kilku poprzednich rozdziałach, dokonanie tego wyboru w JavaScript wiąże się
z dodatkowymi problemami do rozwiązania.
Delegowanie obiektów widżetów
Poniżej przedstawiłem przygotowany w stylu OLOO kod prostszej wersji przykładu Widget/Button, wykorzystujący delegowanie:
var Widget = {
init: function(width,height){
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
},
insert: function($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
156 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
}
}
};
var Button = Object.create( Widget );
Button.setup = function(width,height,label){
// Delegowane wywołanie.
this.init( width, height );
this.label = label || "Domyślny";
this.$elem = $( "<button>" ).text( this.label );
};
Button.build = function($where) {
// Delegowane wywołanie.
this.insert( $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.onClick = function(evt) {
console.log( "Przycisk '" + this.label + "' został kliknięty!" );
};
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = Object.create( Button );
btn1.setup( 125, 30, "Witaj," );
var btn2 = Object.create( Button );
btn2.setup( 150, 40, "świecie" );
btn1.build( $body );
btn2.build( $body );
} );
W przypadku powyższego kodu w stylu OLOO nie traktujemy Widget jako
obiektu nadrzędnego, natomiast Button jako potomnego. Zamiast tego Widget
jest po prostu obiektem i pewnego rodzaju kolekcją pomocniczą, do której
może być delegowany dowolnego typu widżet. Button to również oddzielny
obiekt, ale oczywiście wraz z łączem delegowania prowadzącym do Widget.
Z perspektywy wzorca projektowego w obu obiektach nie współdzielimy tej
samej metody o nazwie render(..), jak można by sądzić na podstawie kodu
klasy, ale decydujemy się na wybór różnych nazw (tutaj insert(..) i build(..)),
które znacznie dokładniej opisują przeznaczenie poszczególnych zadań. Metody
inicjalizacyjne są nazwane odpowiednio init(..) i setup(..), z tych samych
powodów.
Klasy kontra obiekty
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 157
0
Przedstawiony wzorzec delegowania nie tylko sugeruje użycie różnych i bardziej opisowych nazw (zamiast współdzielonych i ogólnych), ale podejście
w stylu OLOO pozwala jednocześnie na uniknięcie wyraźnych wywołań pseudopolimorfizmu (Widget.call i Widget.prototype.render.call). W kodzie możesz natomiast dostrzec proste, względne, delegowane wywołania this.init(..)
i this.insert(..).
Pod względem syntaktycznym nie mamy również żadnych konstruktorów i tak
naprawdę .prototype i new okazują się po prostu zbędne.
Jeżeli dokładnie przyjrzysz się przedstawionemu kodowi, zauważysz, że to,
co wcześniej było pojedynczym wywołaniem (var btn1 = new Button(..)), teraz
zostało podzielone na dwa (var btn1 = Object.create(Button) i btn1.setup(..)).
Początkowo takie podejście może się wydawać wadą (większa ilość kodu).
Jednak nawet takie rozwiązanie w kodzie utworzonym w stylu OLOO można
uznać za plus w porównaniu do klasycznego stylu prototypu. Jak to możliwe?
W przypadku konstruktorów klas jesteś zmuszony (a przynajmniej silnie zachęcany) do przeprowadzenia operacji tworzenia i inicjalizacji obiektu w tym
samym kroku. Jednak bardzo często wykonanie wymienionych zadań w dwóch
krokach (jak ma to miejsce w stylu OLOO) jest znacznie elastyczniejsze.
Przyjmijmy na przykład założenie, że tworzymy wszystkie egzemplarze w puli
na początku programu, natomiast wstrzymujemy się z inicjalizacją aż do chwili
pobrania danego egzemplarza z puli i jego użycia. W kodzie pokazałem, że oba
wywołania mają miejsce jedno po drugim, ale oczywiście mogą wystąpić w różnym czasie i w odmiennych fragmentach kodu, jeśli zachodzi taka potrzeba.
Styl OLOO lepiej obsługuje zasadę podziału obowiązków, kiedy operacje
tworzenia i inicjalizacji niekoniecznie muszą być przeprowadzane jednocześnie.
Prostszy projekt
Poza tym, że styl OLOO charakteryzuje się pozornie prostszym (i znacznie
elastyczniejszym!) kodem, delegowanie jako wzorzec może faktycznie prowadzić do prostszej architektury kodu. Przeanalizujemy teraz ostatni przykład,
pokazujący, jak styl OLOO upraszcza projekt.
158 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
W omawianym projekcie znajdują się dwa obiekty kontrolera. Jeden z nich jest
przeznaczony do obsługi strony internetowej wyświetlającej formularz logowania,
podczas gdy drugi odpowiada za faktyczne przeprowadzenie uwierzytelnienia
(komunikacji) w serwerze.
Potrzebujemy funkcji pomocniczej, która będzie odpowiadała za komunikację
z serwerem za pomocą żądań Ajax. Decydujemy się na użycie biblioteki jQuery
(choć tutaj sprawdzi się dowolny framework), ponieważ nie tylko zapewnia obsługę żądań Ajax, ale również zwraca odpowiedź w postaci obietnicy. W kodzie
wywołującym żądanie możemy więc nasłuchiwać odpowiedzi za pomocą .then(..).
Wprawdzie tutaj nie przedstawię obietnic, ale ich dokładne
omówienie znajdziesz w książce Asynchroniczność i wydajność
z tej samej serii.
Stosując typowy wzorzec projektu opartego na klasach, podstawową funkcjonalność zadania umieszczamy w klasie o nazwie Controller, a następnie tworzymy dwie klasy potomne LoginController i AuthController dziedziczące
po klasie Controller i nadpisujące niektóre z jej funkcji podstawowych:
// Klasa nadrzędna.
function Controller() {
this.errors = [];
}
Controller.prototype.showDialog = function(title,msg) {
// Wyświetlenie użytkownikowi tytułu i wiadomości w oknie dialogowym.
};
Controller.prototype.success = function(msg) {
this.showDialog( "Sukces", msg );
};
Controller.prototype.failure = function(err) {
this.errors.push( err );
this.showDialog( "Błąd", err );
};
// Klasa potomna.
function LoginController() {
Controller.call( this );
}
// Połączenie klasy potomnej z nadrzędną.
LoginController.prototype =
Object.create( Controller.prototype );
LoginController.prototype.getUser = function() {
return document.getElementById( "login_username" ).value;
Prostszy projekt
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 159
0
};
LoginController.prototype.getPassword = function() {
return document.getElementById( "login_password" ).value;
};
LoginController.prototype.validateEntry = function(user,pw) {
user = user || this.getUser();
pw = pw || this.getPassword();
if (!(user && pw)) {
return this.failure(
"Proszę podać nazwę użytkownika i hasło!"
);
}
else if (pw.length < 5) {
return this.failure(
"Hasło musi zawierać przynajmniej 5 znaków!"
);
}
// Dotarłeś tutaj? Zostałeś uwierzytelniony!
return true;
};
// Nadpisanie w celu rozszerzenia metody bazowej failure().
LoginController.prototype.failure = function(err) {
// Wywołanie "super".
Controller.prototype.failure.call(
this,
"Logowanie nieprawidłowe: " + err
);
};
// Klasa potomna.
function AuthController(login) {
Controller.call( this );
// Poza dziedziczeniem potrzebujemy również kompozycji.
this.login = login;
}
// Połączenie klasy potomnej z nadrzędną.
AuthController.prototype =
Object.create( Controller.prototype );
AuthController.prototype.server = function(url,data) {
return $.ajax( {
url: url,
data: data
} );
};
AuthController.prototype.checkAuth = function() {
var user = this.login.getUser();
160 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
var pw = this.login.getPassword();
if (this.login.validateEntry( user, pw )) {
this.server( "/check-auth",{
user: user,
pw: pw
} )
.then( this.success.bind( this ) )
.fail( this.failure.bind( this ) );
}
};
// Nadpisanie w celu rozszerzenia metody bazowej success().
AuthController.prototype.success = function() {
// Wywołanie "super".
Controller.prototype.success.call( this, "Uwierzytelniony!" );
};
// Nadpisanie w celu rozszerzenia metody bazowej failure().
AuthController.prototype.failure = function(err) {
// Wywołanie "super".
Controller.prototype.failure.call(
this,
"Uwierzytelnienie nieprawidłowe: " + err
);
};
var auth = new AuthController(
// Poza dziedziczeniem potrzebujemy również kompozycji.
new LoginController()
);
auth.checkAuth();
W powyższym kodzie mamy funkcje podstawowe współdzielone przez wszystkie kontrolery. Do wspomnianego zachowania zaliczamy funkcje success(..),
failure(..) i showDialog(..). Nasze klasy potomne LoginController i Auth
Controller nadpisują metody failure(..) i success(..) w celu zmiany domyślnego zachowania klasy bazowej. Ponadto zwróć uwagę, że AuthController
wymaga egzemplarza LoginController do pracy z formularzem logowania,
a więc staje się właściwością danych składowych.
Inną kwestią, na którą trzeba zwrócić uwagę, jest fakt, że zdecydowaliśmy
się na zastosowanie pewnej kompozycji na bazie dziedziczenia. Kontroler
AuthController musi wiedzieć o istnieniu LoginController, a więc tworzymy
go (polecenie new LoginController()) i zachowujemy właściwość składowej
klasy o nazwie this.login jako odwołania, aby kontroler AuthController
mógł wywoływać zachowanie LoginController.
Prostszy projekt
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 161
0
Może pojawić się pokusa, aby kontroler AuthController dziedziczył po LoginController lub na odwrót, jakby istniała wirtualna
kompozycja przygotowana za pomocą łańcucha dziedziczenia.
Jednak to czytelny przykład pokazujący, że zastosowanie dziedziczenia klas do rozwiązania tego problemu jest niewłaściwe,
ponieważ ani AuthController, ani LoginController nie nadpisują funkcji bazowych. Dlatego też dziedziczenie między wymienionymi kontrolerami nie ma sensu za wyjątkiem sytuacji,
gdy klasy są jedynym wzorcem projektowym. Zamiast tego ułożyliśmy je w pewną prostą kompozycję, umożliwiając im tym
samym kooperację, przy czym obie wciąż korzystają z zalet
dziedziczenia po bazowej klasie nadrzędnej Controller.
Jeżeli programowanie w stylu zorientowanym obiektowo jest Ci znane, przedstawione tutaj informacje nie powinny być zaskoczeniem.
Pozbywamy się klas
Z pewnością zadajesz sobie teraz pytanie, czy do modelowania problemu naprawdę potrzebujemy klasy nadrzędnej, dwóch klas potomnych i pewnej kompozycji. Czy istnieje możliwość wykorzystania delegowania w kodzie w stylu
OLOO i przygotowania znacznie prostszego projektu? Tak!
var LoginController = {
errors: [],
getUser: function() {
return document.getElementById(
"login_username"
).value;
},
getPassword: function() {
return document.getElementById(
"login_password"
).value;
},
validateEntry: function(user,pw) {
user = user || this.getUser();
pw = pw || this.getPassword();
if (!(user && pw)) {
return this.failure(
"Proszę podać nazwę użytkownika i hasło!"
);
162 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
}
else if (pw.length < 5) {
return this.failure(
"Hasło musi zawierać przynajmniej 5 znaków!"
);
}
// Dotarłeś tutaj? Zostałeś uwierzytelniony!
return true;
},
showDialog: function(title,msg) {
// Wyświetlenie użytkownikowi komunikatu sukcesu w oknie dialogowym
},
failure: function(err) {
this.errors.push( err );
this.showDialog( "Błąd", "Logowanie nieprawidłowe: " + err );
}
};
// Obiekt AuthController zostaje połączony z LoginController i deleguje do niego.
var AuthController = Object.create( LoginController );
AuthController.errors = [];
AuthController.checkAuth = function() {
var user = this.getUser();
var pw = this.getPassword();
if (this.validateEntry( user, pw )) {
this.server( "/check-auth",{
user: user,
pw: pw
} )
.then( this.accepted.bind( this ) )
.fail( this.rejected.bind( this ) );
}
};
AuthController.server = function(url,data) {
return $.ajax( {
url: url,
data: data
} );
};
AuthController.accepted = function() {
this.showDialog( "Sukces", "Uwierzytelniony!" )
};
AuthController.rejected = function(err) {
this.failure( "Uwierzytelnienie nieprawidłowe: " + err );
};
Prostszy projekt
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 163
0
Ponieważ AuthController jest po prostu obiektem (podobnie jak LoginController),
nie musimy tworzyć egzemplarza (na przykład za pomocą new AuthController())
w celu wykonania zadania. Konieczne jest jedynie wydanie polecenia:
AuthController.checkAuth();
Oczywiście w przypadku stylu OLOO, jeśli zachodzi potrzeba utworzenia
jednego lub większej liczby obiektów dodatkowych w łańcuchu delegowania,
można to zrobić bardzo łatwo, a przy tym nadal nie ma konieczności wykonania operacji takiej jak tworzenie egzemplarzy klasy:
var controller1 = Object.create( AuthController );
var controller2 = Object.create( AuthController );
W przypadku podejścia opartego na delegowaniu AuthController i LoginController
to po prostu obiekty, ułożone poziomo obok siebie, a nie zorganizowane w postaci obiektów nadrzędnych i potomnych, jak w rozwiązaniach bazujących na
klasach. Mamy dowolność w definiowaniu delegowania. Zdecydowaliśmy, że
AuthController deleguje do LoginController, ale delegowanie równie dobrze
mogłoby zostać zdefiniowane w przeciwnym kierunku.
W omawianym tutaj fragmencie kodu mamy jedynie dwie encje (LoginController
i AuthController), a nie trzy, jak było wcześniej.
Nie potrzebujemy klasy bazowej Controller, która będzie „współdzieliła” zachowanie między dwoma wcześniej wymienionymi kontrolerami, ponieważ
delegowanie to mechanizm o potężnych możliwościach i oferuje nam całą
niezbędną funkcjonalność. Jak wcześniej wspomniałem, nie ma potrzeby tworzenia egzemplarzy klas, aby pracować z encjami, ponieważ nie mamy klas,
a jedynie obiekty. Co więcej, unikamy konieczności przygotowania kompozycji,
gdyż we wzorcu delegowania dwa obiekty mogą współpracować odmiennie,
jeśli zachodzi taka potrzeba.
Udało nam się uniknąć pułapek związanych z polimorfizmem w projekcie zorientowanym obiektowo, ponieważ nie mamy metod success(..) i failure(..)
w obu obiektach, co wymagałoby zastosowania wyraźnego pseudopolimorfizmu.
Zamiast tego w obiekcie AuthController nadajemy metodom nazwy accepted()
i rejected(), które znacznie dokładniej opisują wykonywane przez nie zadania.
Ostatecznie mamy tę samą funkcjonalność, ale przy użyciu (zdecydowanie)
prostszego projektu. To jest właśnie potęga kodu w stylu OLOO oraz wzorca
projektowego delegowania.
164 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Ładniejsza składnia
Jedną z miłych cech powodujących, że wprowadzone w specyfikacji ES6 słowo
kluczowe class wydaje się niezwykle atrakcyjne (zajrzyj do dodatku A, aby dowiedzieć się, dlaczego należy go unikać!), jest skrócona składnia przeznaczona
do deklarowania metod klasy:
class Foo {
methodName() { /* .. */ }
}
Zyskujemy możliwość pozbycia się słowa kluczowego function z deklaracji
funkcji, co niewątpliwie cieszy programistów JavaScript!
W poprzednim kodzie w stylu OLOO prawdopodobnie zauważyłeś (a nawet
mogłeś być sfrustrowany z tego powodu), że mieliśmy wiele wystąpień słowa
kluczowego function, co raczej mija się z celem, jakim jest uproszczona składnia oferowana przez styl OLOO. Tak jednak nie musi być!
Począwszy od wydania ES6, możemy używać zwięzłych deklaracji metod w dowolnym literale obiektu, a więc obiekt w stylu OLOO może zostać zadeklarowany następująco (skrót podobny jak w przypadku składni z użyciem class):
var LoginController = {
errors: [],
getUser() { // Zauważ brak słowa kluczowego 'function'!
// …
},
getPassword() {
// …
}
// …
};
Praktycznie jedyna różnica polega na tym, że literały obiektów nadal wymagają
użycia między elementami separatorów w postaci przecinka (,), podczas gdy
składnia class tego nie wymaga. To całkiem niewielkie ustępstwo w całym
rozwiązaniu.
Co więcej, począwszy od wydania ES6, cała dotychczasowa składnia (na przykład
definicja AuthController), w której właściwości są przypisywane pojedynczo i nie
wykorzystujemy literału obiektu, może być zastąpiona literałem obiektu. To
pozwala na użycie zwięzłych metod i po prostu modyfikację [[Prototype]]
obiektu za pomocą Object.setPrototypeOf(..), jak pokazałem poniżej:
Ładniejsza składnia
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 165
0
// Użycie ładniejszej składni literału obiektu wraz ze zwięzłymi metodami!
var AuthController = {
errors: [],
checkAuth() {
// …
},
server(url,data) {
// …
}
// …
};
// TERAZ łączymy AuthController z LoginController.
Object.setPrototypeOf( AuthController, LoginController );
Styl OLOO w wydaniu ES6 wraz ze zwięzłymi metodami stał się znacznie
przyjaźniejszy niż wcześniej (choć nawet wcześniej był dużo prostszy i bardziej elegancki niż klasyczny kod w stylu prototypu). Nie musisz decydować
się na użycie klas (poziom skomplikowania), aby otrzymać dostęp do ładnej,
przejrzystej składni obiektu!
Nieleksykalny
Istnieje pewna wada metod zwięzłych, która jest subtelna, choć niezwykle
ważna. Spójrz na poniższy fragment kodu:
var Foo = {
bar() { /*..*/ },
baz: function baz() { /*..*/ }
};
Poniżej pokazałem, w jaki sposób ten kod będzie działał:
var Foo = {
bar: function() { /*..*/ },
baz: function baz() { /*..*/ }
};
Czy widzisz różnicę? Skrót w postaci bar() stał się wyrażeniem funkcji anonimowej (function(..)) dołączonej do właściwości bar, ponieważ sam obiekt
funkcji nie ma identyfikatora w postaci nazwy. Porównaj to z ręcznie podanym
wyrażeniem funkcji nazwanej (function baz()..), które ma leksykalny identyfikator nazwy (tutaj baz) i jest dołączony do właściwości .baz.
I co teraz? W innej książce z tej serii (Zakresy i domknięcia) szczegółowo przedstawiłem trzy największe wady wyrażeń funkcji anonimowych. Teraz jedynie je
tutaj wymienię, aby umożliwić porównanie ze skrótem w postaci zwięzłych metod.
166 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Brak identyfikatora name w funkcji anonimowej:
1. Oznacza trudniejsze debugowanie stosu wywołań.
2. Oznacza trudniejsze odwoływanie się funkcji do samej siebie (rekurencja,
dołączanie zdarzeń itd.).
3. Oznacza powstanie kodu (nieco) trudniejszego do zrozumienia.
Punkty 1. i 3. nie mają zastosowania dla metod zwięzłych.
Nawet w przypadku wyrażeń funkcji anonimowej, która nie ma nazwy w stosie wywołań, metody zwięzłe są przygotowane do odpowiedniego ustawienia
wartości wewnętrznej właściwości name obiektu funkcji. Dlatego też stos wywołań powinien być w stanie używać tych nazw (wszystko jednak zależy od
implementacji i nie masz w tym zakresie żadnych gwarancji).
Punkt 2. niestety jest wciąż wadą metod zwięzłych, ponieważ nie będą miały one
leksykalnego identyfikatora pozwalającego na odwołanie się do samej siebie.
Spójrz na poniższy fragment kodu:
var Foo = {
bar: function(x) {
if (x < 10) {
return Foo.bar( x * 2 );
}
return x;
},
baz: function baz(x) {
if (x < 10) {
return baz( x * 2 );
}
return x;
}
};
W powyższym przykładzie ręczne odwołanie Foo.bar(x*2) okazuje się wystarczające, ale istnieje wiele przypadków, w których funkcja niekoniecznie
będzie mogła działać w taki właśnie sposób. Przykładem może być sytuacja,
gdy funkcja jest współdzielona w delegowaniu między różnymi obiektami za
pomocą wiązania this itd. Konieczne jest użycie rzeczywistego odwołania do
samej siebie, a identyfikator name obiektu funkcji będzie najlepszym sposobem
wykonania takiego zadania.
Powinieneś mieć świadomość istnienia przedstawionej wady metod zwięzłych. Jeżeli napotkasz jakiekolwiek problemy związane z brakiem odwołania
Ładniejsza składnia
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 167
0
do samej siebie, wówczas zrezygnuj ze składni metody zwięzłej dla tej konkretnej deklaracji i zamiast niej zastosuj ręczną deklarację wyrażenia funkcji nazwanej w postaci: baz: function baz(){..}.
Introspekcja
Jeżeli poświęciłeś dużo czasu na programowanie zorientowane obiektowo
(w JavaScript lub innych językach programowania), prawdopodobnie znasz
już koncepcję introspekcji typu: sprawdzenia egzemplarza w celu ustalenia,
jakiego rodzaju jest to obiekt. Podstawowym celem introspekcji typu w egzemplarzach klas jest znalezienie uzasadnienia dla struktury i możliwości
obiektu na podstawie sposobu jego utworzenia.
Spójrz na przedstawiony poniżej kod przedstawiający użycie operatora
instanceof (patrz rozdział 5.) przeznaczonego do introspekcji obiektu a1
w celu ustalenia jego możliwości.
function Foo() {
// …
}
Foo.prototype.something = function(){
// …
}
var a1 = new Foo();
// Później.
if (a1 instanceof Foo) {
a1.something();
}
Ponieważ Foo.prototype (nie Foo!) znajduje się w łańcuchu [[Prototype]]
(patrz rozdział 5.) obiektu a1, operator instanceof próbuje nam powiedzieć, że
a1 jest egzemplarzem „klasy” Foo (co jest mylącym zabiegiem). Mając tę wiedzę,
przyjmujemy założenie, że obiekt a1 ma możliwości opisane przez „klasę” Foo.
Oczywiście klasa Foo nie istnieje — mamy jedynie zwykłą funkcję Foo zawierającą odwołanie do innego obiektu (Foo.prototype), do którego a1 zostaje
oddelegowany. Z powodu stosowanej składni operator instanceof udaje, że
sprawdza związek między a1 i Foo, choć tak naprawdę informuje nas, czy a1
i Foo.prototype (dowolny obiekt, do którego się odwołuje a1) są ze sobą
powiązane.
168 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Semantyka i pośredniość składni instanceof sprawiają, że w celu użycia
opartej na tym operatorze introspekcji do sprawdzenia, czy obiekt a1 jest powiązany ze wskazanym obiektem, konieczne będzie przygotowanie funkcji zawierającej odwołanie do drugiego obiektu. Nie ma możliwości bezpośredniego
sprawdzenia, czy dwa obiekty są ze sobą powiązane.
Przypomnij sobie przykład Foo/Bar/b1 przedstawiony we wcześniejszej części
rozdziału. Poniżej zamieściłem jego skróconą wersję:
function Foo() { /* .. */ }
Foo.prototype...
function Bar() { /* .. */ }
Bar.prototype = Object.create( Foo.prototype );
var b1 = new Bar( "b1" );
Na potrzeby introspekcji typu encji wykorzystanych w przykładzie używamy
semantyki instanceof i .prototype w celu przeprowadzenia różnych operacji
sprawdzenia, które będą musiały być wykonane:
// Powiązanie obiektów 'Foo' i 'Bar' ze sobą.
Bar.prototype instanceof Foo; // Prawda.
Object.getPrototypeOf( Bar.prototype )
=== Foo.prototype; // Prawda.
Foo.prototype.isPrototypeOf( Bar.prototype ); // Prawda.
// Powiązanie 'b'1 zarówno z 'Foo', jak i z 'Bar'.
b1 instanceof Foo; // Prawda.
b1 instanceof Bar; // Prawda.
Object.getPrototypeOf( b1 ) === Bar.prototype; // Prawda.
Foo.prototype.isPrototypeOf( b1 ); // Prawda.
Bar.prototype.isPrototypeOf( b1 ); // Prawda.
Można śmiało powiedzieć, że takie rozwiązanie jest niedobre. Na przykład
intuicyjnie (z zastosowaniem klas) można użyć Bar instanceof Foo (ponieważ
łatwo zmienić znaczenie słowa „egzemplarz”, aby oznaczało także „dziedziczenie”), ale to nie będzie rozsądne porównanie w JavaScript. Zamiast tego
konieczne jest użycie Bar.prototype instanceof Foo.
Innym często spotykanym, choć mniej niezawodnym wzorcem dla introspekcji
typu, preferowanym przez wielu programistów zamiast operatora instanceof,
jest tak zwane „kacze typowanie”. To pojęcie bierze się z następującego porzekadła: „Jeżeli coś wygląda jak kaczka i wydaje dźwięki jak kaczka, musi
być kaczką”.
Introspekcja
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 169
0
Na przykład:
if (a1.something) {
a1.something();
}
Zamiast przeprowadzać introspekcję pod kątem związku między a1 i obiektem przechowującym delegowaną funkcję something(), przyjmujemy założenie, że zaliczenie testu a1.something oznacza, iż a1 ma możliwość wywołania
.something() (niezależnie od tego, czy metoda zostanie znaleziona bezpośrednio w a1, czy po delegowaniu do pewnego innego obiektu). Szczerze mówiąc,
tego rodzaju założenie jest zbyt ryzykowne.
Jednak kacze typowanie jest często rozszerzane o inne założenia dotyczące
możliwości obiektu, nie tylko te testowane, co oczywiście wprowadza jeszcze
większe ryzyko (i bardziej zawodną konstrukcję) do testu.
Jeden z wartych tutaj wymienienia przykładów kaczego typowania wiąże się
z wprowadzonymi w specyfikacji ES6 obietnicami (które jak już wcześniej
wspomniałem, nie zostaną omówione w książce).
Z wielu różnych powodów może wystąpić konieczność ustalenia, czy dany
obiekt jest odwołaniem do obietnicy. Jednak sposób przeprowadzania testu
opiera się na sprawdzeniu, czy dany obiekt zawiera funkcję then(). Innymi
słowy, jeśli obiekt zawiera funkcję then(), wówczas wprowadzony w ES6 mechanizm obietnic przyjmuje bezwarunkowe założenie, że dany obiekt jest
„thenable”. To pociąga za sobą oczekiwanie, że jego zachowanie będzie zgodne
ze standardowym zachowaniem obietnic.
Jeżeli masz obiekt inny niż obietnica, który zawiera z jakiegokolwiek powodu
metodę then(), wówczas najlepiej zrezygnuj z użycia mechanizmu obietnic ES6,
aby w ten sposób uniknąć tego rodzaju nieprawidłowych założeń.
Przedstawiony przykład wyraźnie pokazuje niebezpieczeństwa wiążące się
z kaczym typowaniem. Z tego rodzaju podejścia powinieneś korzystać jedynie
sporadycznie oraz w kontrolowanych sytuacjach.
Jeżeli ponownie spojrzysz na kod w stylu OLOO przedstawiony w rozdziale,
to okaże się, że introspekcja typu nie jest taka trudna. Poniżej przedstawiam
skróconą wersję przykładu Foo/Bar/b1 w stylu OLOO, omówionego we wcześniejszej części rozdziału:
170 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
var Foo = { /* .. */ };
var Bar = Object.create( Foo );
Bar...
var b1 = Object.create( Bar );
Dzięki użyciu podejścia w stylu OLOO mamy zwykłe obiekty powiązane ze
sobą za pomocą delegowania [[Prototype]]. Dlatego też możemy użyć całkiem uproszczonej wersji introspekcji typu:
// Powiązanie obiektów 'Foo' i 'Bar' ze sobą.
Foo.isPrototypeOf( Bar ); // Prawda.
Object.getPrototypeOf( Bar ) === Foo; // Prawda.
// Powiązanie 'b1' zarówno z 'Foo', jak i z 'Bar'.
Foo.isPrototypeOf( b1 ); // Prawda.
Bar.isPrototypeOf( b1 ); // Prawda.
Object.getPrototypeOf( b1 ) === Bar; // Prawda.
Nie używamy dłużej operatora instanceof, ponieważ błędnie wskazuje na
rozwiązanie mające coś wspólnego z klasami. Teraz po prostu zadajemy pytanie: „Czy jesteś prototypem mnie?”. Nie mamy żadnych pośrednich mechanizmów, takich jak Foo.prototype lub boleśnie rozwlekły Foo.prototype.is
PrototypeOf(..).
Jestem przekonany, że teraz można powiedzieć o znacznym uproszczeniu
wcześniejszej wersji operacji introspekcji typu. Ponownie przekonujemy się,
że kod w stylu OLOO okazuje się prostszy (choć oferuje dokładnie tę samą
funkcjonalność) niż klasyczny styl tworzenia kodu w JavaScript.
Podsumowanie
Klasy w połączeniu z dziedziczeniem to wzorzec projektowy, na stosowanie
którego możesz — choć nie musisz — się zdecydować podczas tworzenia
oprogramowania. Większość programistów jest przekonana, że klasy to jedyny
(prawidłowy) sposób organizacji kodu. Jak mogłeś zobaczyć w rozdziale, istnieje jeszcze inny, rzadziej omawiany wzorzec oferujący potężne możliwości:
delegowanie.
Delegowanie sugeruje umieszczanie obiektów obok siebie wraz z łączami
delegowania między nimi, a nie powstawanie związków typu obiekt nadrzędny i potomny. Oferowany przez JavaScript mechanizm [[Prototype]]
Podsumowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 171
0
jest doskonale zaprojektowanym wzorcem delegowania. W ten sposób masz
wybór: zmaganie się z implementacją klas w JavaScript (patrz rozdziały 4. i 5.)
lub też wykorzystanie natywnego mechanizmu delegowania opartego na
[[Prototype]].
Kiedy projektujesz kod jedynie z użyciem obiektów, nie tylko upraszczasz
używaną składnię, ale jednocześnie otrzymujesz prostszy projekt architektury kodu.
OLOO (obiekty połączone z innymi obiektami) to styl programowania polegający na bezpośrednim tworzeniu obiektów i łączeniu ich ze sobą bez użycia
abstrakcji w postaci klas. Styl OLOO całkiem naturalnie implementuje bazujący na [[Prototype]] mechanizm delegowania.
172 
Rozdział 6. Delegowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
DODATEK A
ES6 class
Jeżeli druga część książki (rozdziały od 4. do 6.) ma jakiekolwiek przesłanie,
to można je streścić następująco: klasy są opcjonalnym wzorcem projektowym
(niekoniecznie danym), często niełatwym do zaimplementowania w opartym
na mechanizmie [[Prototype]] języku takim jak JavaScript.
Niedogodność nie wynika jedynie ze składni, choć ona na pewno stanowi
ogromną część problemu. W rozdziałach 4. i 5. przedstawiłem całkiem sporą
ilość brzydkiego kodu, począwszy od rozwlekłych odwołań .prototype zaśmiecających kod, a skończywszy na wyraźnym pseudopolimorfizmie (patrz
rozdział 4.), gdy metody mają te same nazwy na różnych poziomach łańcucha
i następuje próba implementacji polimorficznego odwołania z metody znajdującej się na niższym poziomie do metody na wyższym poziomie. Właściwość .constructor jest błędnie interpretowana jako „został skonstruowany
przez”. Co więcej, wymieniona definicja jest nie tylko błędna, ale stanowi także
kolejny przykład syntaktycznej brzydoty.
Jednak problemy związane z projektem klas sięgają znacznie głębiej. W rozdziale 4. dowiedziałeś się, że klasy w tradycyjnych językach programowania
obiektowego w rzeczywistości tworzą w obiekcie potomnym kopię akcji z obiektu
nadrzędnego, podczas gdy w mechanizmie [[Prototype]] akcja nie jest kopiowana, a mamy wręcz sytuację odwrotną — łącze delegacji.
W porównaniu do prostoty kodu w stylu OLOO i mechanizmu delegowania
(patrz rozdział 6.), wykorzystującego mechanizm [[Prototype]] zamiast go
ukrywać, klasy w JavaScript wydają się utrapieniem.
173
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Słowo kluczowe class
Jednak nie musimy znów do tego powracać. Powyższe kwestie pokrótce przypomniałem, aby o nich nie zapomnieć, gdy naszą uwagę skierujemy na wprowadzony w specyfikacji ES6 mechanizm class. Przedstawię sposób jego działania
i wyjaśnię, czy w jakikolwiek sposób ma wpływ na przedstawione wcześniej
obawy dotyczące „klas”.
Poniżej przedstawiam przykład Widget/Button zaprezentowany w rozdziale 6.:
class Widget {
constructor(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
}
class Button extends Widget {
constructor(width,height,label) {
super( width, height );
this.label = label || "Domyślny";
this.$elem = $( "<button>" ).text( this.label );
}
render($where) {
super( $where );
this.$elem.click( this.onClick.bind( this ) );
}
onClick(evt) {
console.log( "Przycisk '" + this.label + "' został kliknięty!" );
}
}
Poza tym, że powyższa składnia wygląda przyjemniej, czy rozwiązuje jakiekolwiek
problemy?
174 
Dodatek A ES6 class
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
1. Kod nie zawiera już zaśmiecających go odwołań do .prototype.
2. Obiekt Button został zadeklarowany bezpośrednio do „dziedziczenia
po” (co odpowiada użyciu słowa kluczowego extends) obiekcie Widget.
Eliminujemy tym samym konieczność użycia wywołania Object.
create(..) do zastąpienia połączonego obiektu .prototype lub też
ustawienia wspomnianego obiektu za pomocą .__proto__ bądź Object.
setPrototypeOf(..).
3. Wywołanie super(..) oferuje teraz bardzo użyteczny względny polimorfizm, a więc metoda na jednym poziomie łańcucha może względnie odwoływać się do metody o tej samej nazwie istniejącej na wyższym poziomie łańcucha. Tym samym mamy wyjaśnienie kwestii (przedstawionej
w rozdziale 4.) dotyczącej dziwnego rozwiązania, gdy konstruktory nie
należą do klas i są niepowiązane — wywołanie super() działa wewnątrz
konstruktorów dokładnie tak, jak oczekujesz.
4. Literalna składnia class nie pozwala na określanie właściwości, a jedynie
metody. Dla niektórych to może się wydawać ograniczeniem. Jednak
w większości przypadków oczekuje się, że jeżeli właściwość (informacje
o stanie) istnieje w innym miejscu niż „egzemplarz” kończący łańcuch,
to zwykle mamy do czynienia z zaskakującym błędem (podobnie jak
w przypadku niejawnego „współdzielenia” informacji o stanie między
wszystkimi „egzemplarzami”). Dlatego można powiedzieć, że składnia
class chroni przed tego rodzaju błędami.
5. Słowo kluczowe extends pozwala na rozszerzenie w bardzo naturalny
sposób obiektów nawet wbudowanych (pod)typów, takich jak Array
i RegExp. Wykonanie takiego zadania bez użycia class .. extends jest
przesadnie skomplikowanym i frustrującym zadaniem. Dlatego też tylko
najbardziej doświadczeni autorzy frameworków byli w stanie prawidłowo
to zrobić. Teraz zadanie rozszerzenia zostało znacznie uproszczone!
Trzeba przyznać, że to jedynie częściowe rozwiązania wielu najbardziej oczywistych (syntaktycznych) problemów napotykanych przez programistów tworzących kod w klasycznym stylu prototypu.
Słowo kluczowe class
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 175
0
Pułapki związane z class
Jednak nie wszystko jest usłane różami. Nadal istnieją pewne głęboko zakorzenione problemy związane z użyciem „klas” jako wzorca projektowego
w języku JavaScript.
Przede wszystkim składnia class może zasugerować dodanie nowego mechanizmu „klas” do języka JavaScript, zgodnego ze specyfikacją ES6. Nic z tego.
Słowo kluczowe class to jedynie syntaktyczne rozwiązanie opracowane na
bazie istniejącego mechanizmu [[Prototype]] (delegowanie!).
Oznacza to, że class nie powoduje rzeczywistego skopiowania statycznych
definicji w trakcie deklaracji, jak ma to miejsce w tradycyjnych językach programowania obiektowego. Jeżeli zmienisz lub zastąpisz metodę (celowo bądź
przypadkowo) w „klasie” nadrzędnej, wówczas będzie to miało wpływ na „klasy”
i (lub) egzemplarze potomne — ponieważ nie otrzymują kopii w trakcie ich deklarowania, nadal korzystają z modelu delegowania opartego na [[Prototype]]:
class C {
constructor() {
this.num = Math.random();
}
rand() {
console.log( "Losowa liczba: " + this.num );
}
}
var c1 = new C();
c1.rand(); // "Losowa liczba: 0.4324299…"
C.prototype.rand = function() {
console.log( "Losowa liczba: " + Math.round( this.num * 1000 ));
};
var c2 = new C();
c2.rand(); // "Losowa liczba: 867"
c1.rand(); // "Losowa liczba: 432" -- Ups!!!
To wydaje się rozsądnym zachowaniem tylko wtedy, gdy dobrze znasz naturę
delegowania i nie oczekujesz otrzymania kopii „rzeczywistych klas”. Dlatego
też pytanie, które powinieneś sobie zadać, brzmi: dlaczego wybieram składnię
class do osiągnięcia zupełnie innego celu niż utworzenie klasy?
176 
Dodatek A ES6 class
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Czy wprowadzenie w wydaniu ES6 składni class nie utrudniło dostrzeżenia
i poznania różnic między tradycyjnymi klasami i delegowanymi obiektami?
Składnia class nie oferuje sposobu na zadeklarowanie właściwości elementu
składowego (a jedynie metody). Dlatego też jeśli musisz monitorować stan
współdzielony między egzemplarzami, możesz skorzystać jedynie z brzydkiej
składni z użyciem .prototype, na przykład:
class C {
constructor() {
// Upewniamy się co do modyfikacji współdzielonego stanu,
// a nie co do ustawienia przesłaniającej właściwości
// w egzemplarzach!
C.prototype.count++;
// W tym miejscu 'this.count' działa zgodnie z oczekiwaniami
// za pomocą delegowania.
console.log( "Witaj: " + this.count );
}
}
// Dodanie właściwości dla współdzielonego stanu
// bezpośrednio do obiektu prototypu.
C.prototype.count = 0;
var c1 = new C();
// Witaj: 1
var c2 = new C();
// Witaj: 2
c1.count === 2; // Prawda.
c1.count === c2.count; // Prawda.
Największym problemem jest w tym przypadku fakt, że składnia class udostępnia (tutaj to oznacza wyciek!) .prototype jako szczegół implementacji.
Wciąż może być zaskakujące to, że polecenie this.count++ będzie niejawnie
tworzyło oddzielną, przesłoniętą właściwość .count zarówno w obiekcie c1,
jak i c2, zamiast uaktualnić współdzielony stan. Słowo kluczowe class nie
oferuje rozwiązania tego problemu poza (przedwczesnym) zasugerowaniem,
że brak syntaktycznej obsługi oznacza zalecenie, aby w ogóle unikać tego rodzaju poleceń.
Pułapki związane z class
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 177
0
Co więcej, nadal istnieje niebezpieczeństwo wystąpienia przypadkowego
przesłonięcia:
class C {
constructor(id) {
// Ups, pułapka! Przesłaniamy metodę id()
// wartością właściwości w egzemplarzu.
this.id = id;
}
id() {
console.log( "Id: " + id );
}
}
var c1 = new C( "c1" );
c1.id(); // TypeError -- 'c1.id' to teraz ciąg tekstowy "c1".
Istnieją jeszcze pewne drobne kwestie związane ze sposobem działania super.
Możesz przyjąć założenie, że dołączenie super następuje w analogiczny sposób jak wiązanie this (patrz rozdział 2.), czyli super zawsze będzie dołączone
na poziomie o jeden wyższym niż położenie bieżącej metody w łańcuchu
[[Prototype]].
Jednak z powodu wydajności (wiązanie this zalicza się do kosztownych)
nie mamy dynamicznego wiązania super. To rodzaj dołączania „statycznego”
w trakcie deklarowania obiektu. To nie wydaje się dużym problemem, prawda?
Eh, może tak, a może nie. Jeżeli podobnie jak większość programistów JavaScript zaczniesz przypisywać funkcje odmiennym obiektom (pochodzącym
z definicji class) na różne sposoby, to prawdopodobnie nie masz świadomości, że w tych wszystkich przypadkach mechanizm super w tle musi być dołączany za każdym razem.
Ponadto w zależności od syntaktycznego podejścia zastosowanego w trakcie
wspomnianych operacji przypisania mogą wystąpić sytuacje, gdy prawidłowe
wiązanie super okaże się niemożliwe (przynajmniej nie tam, gdzie oczekujesz).
Dlatego też może zaistnieć konieczność ręcznego dołączenia super za pomocą
toMethod(..), co przypomina użycie bind(..) dla wiązania this — patrz rozdział 2. (Muszę w tym miejscu dodać, że w trakcie powstawania tej książki
w komitecie TC39 nadal trwała dyskusja poświęcona temu zagadnieniu).
178 
Dodatek A ES6 class
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Jesteś przyzwyczajony do możliwości przypisywania metod różnym obiektom
w celu automatycznego wykorzystania zalet dynamiczności mechanizmu wiązania this za pomocą reguły wiązania niejawnego (patrz rozdział 2.). To jednak
nie sprawdza się w przypadku metod używających super.
Zastanów się i spróbuj odpowiedzieć, jakie powinno być działanie super
w poniższym fragmencie kodu (względem D i E):
class P {
foo() { console.log( "P.foo" ); }
}
class C extends P {
foo() {
super();
}
}
var c1 = new C();
c1.foo(); // "P.foo"
var D = {
foo: function() { console.log( "D.foo" ); }
};
var E = {
foo: C.prototype.foo
};
// Łącze 'E' do 'D' zapewniające obsługę delegowania.
Object.setPrototypeOf( E, D );
E.foo(); // "P.foo"
Jeżeli uważasz (całkiem rozsądnie!), że dołączenie super nastąpi dynamicznie
w trakcie wywołania, to możesz oczekiwać automatycznego rozpoznania przez
super() faktu delegowania E do D. Dlatego też E.foo(), używając super(),
powinno wywołać D.foo().
Tak nie jest. Z pragmatycznych powodów wydajności super nie jest późno
wiązane (inaczej: dynamicznie dołączane), podobnie jak ma to miejsce dla this.
Zamiast tego w chwili wywołania wywodzi się z [[HomeObject]].[[Prototype]],
gdzie [[HomeObject]] jest statycznie dołączane w trakcie tworzenia.
W omawianym tutaj przykładzie super() nadal odwołuje się do P.foo(), ponieważ metodą [[HomeObject]] wciąż jest C, a C.[[Prototype]] wynosi P.
Pułapki związane z class
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 179
0
Prawdopodobnie istnieją sposoby na ręczne rozwiązanie wymienionych kwestii.
Użycie toMethod(..) w celu pierwszego lub ponownego dołączenia metod
[[HomeObject]] (wraz z ustawieniem [[Prototype]] dla tego obiektu!) wydaje
się sprawdzać w poniższym scenariuszu:
var D = {
foo: function() { console.log( "D.foo" ); }
};
// Łącze 'E' do 'D' zapewniające obsługę delegowania.
var E = Object.create( D );
// Ręczne dołączenie [[HomeObject]] dla 'foo' jako
// 'E', E.[[Prototype]] wynosi 'D', więc
// super() to D.foo().
E.foo = C.prototype.foo.toMethod( E, "foo" );
E.foo(); // "D.foo"
Funkcja toMethod(..) klonuje metodę i pobiera homeObject
jako jej pierwszy parametr (dlatego przekazujemy E), a drugi
parametr (opcjonalnie) ustawia nazwę dla nowej metody
(tutaj zachowaliśmy foo ).
Czas pokaże, czy są jakiekolwiek inne pułapki czyhające na programistów stosujących powyższe rozwiązanie. Niezależnie od tego musisz być sumienny
i wiedzieć, w których miejscach silnik automatycznie określa super za Ciebie,
a w których musisz się tym zająć osobiście. Fuj!
Statyczny > dynamiczny?
Jednak największy problem ze wszystkich związanych ze słowem kluczowym
class w ES6 wynika z przekonania, że (podobnie jak w przypadku tradycyjnych
klas) klasa utworzona za pomocą słowa kluczowego class pozostaje statyczna
po jej zadeklarowaniu (w trakcie późniejszych operacji tworzenia egzemplarzy).
Całkowicie tracisz z pola widzenia fakt, że C jest obiektem, konkretną rzeczą,
z którą można prowadzić bezpośrednią interakcję.
W tradycyjnych językach programowania obiektowego po utworzeniu klasy
później nigdy nie zmieniamy jej zachowania, więc wzorzec projektu klasy nawet
nie sugeruje istnienia tego rodzaju możliwości. Jednak jedną z największych
zalet języka JavaScript jest jego dynamiczność, a definicje dowolnych obiektów (o ile nie będą niemodyfikowalne) pozostają płynne i możliwe do zmiany.
180 
Dodatek A ES6 class
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Mechanizm class wydaje się sugerować, że nie powinieneś myśleć o modyfikacjach. Odbywa się to przez wymuszenie stosowania brzydkiej składni .prototype,
uwzględniania wszystkich pułapek związanych z super itd. Ponadto oferuje
wyjątkowo niewielką pomoc w rozwiązaniu wszelkich kwestii, które mogą być
związane ze wspomnianym dynamizmem.
Innymi słowy, mechanizm class mówi: „Zapewnienie dynamizmu jest zbyt
trudne, a więc prawdopodobnie to nie jest dobre rozwiązanie. Tutaj masz
składnię wyglądającą jak statyczna, a więc twórz kod statyczny”.
Co za smutne podsumowanie dotyczące języka JavaScript: zapewnienie
dynamizmu jest zbyt trudne, więc udajemy, że mamy do czynienia z kodem
statycznym (choć to w rzeczywistości nieprawda!).
Istnieją pewne powody, dla których wprowadzone w specyfikacji ES6 słowo
kluczowe class udaje eleganckie rozwiązanie dla syntaktycznych problemów,
choć tak naprawdę jeszcze bardziej komplikuje sprawy i utrudnia zrozumienie
kodu w JavaScript.
Jeżeli używasz funkcji pomocniczej .bind(..) w celu utworzenia na stałe dołączonej funkcji (patrz rozdział 2.), to powinieneś wiedzieć, że tak przygotowana funkcja nie umożliwia
tworzenia podklas za pomocą wprowadzonego w specyfikacji
ES6 słowa kluczowego extends, jak ma to miejsce w przypadku
zwykłych funkcji.
Podsumowanie
Słowo kluczowe class doskonale udaje, że rozwiązuje problemy związane ze
wzorcem klas i dziedziczeniem w języku JavaScript. Jednak tak naprawdę ma
zupełnie przeciwne działanie: ukrywa wiele problemów i przy okazji wprowadza kolejne — wprawdzie subtelne, ale niebezpieczne.
Istnienia słowa kluczowego class ma swój wkład w trwające niemalże od
dwóch dekad zamieszanie dotyczące „klas” w języku JavaScript. Pod pewnymi
względami stawia więcej pytań, niż udziela odpowiedzi, i wydaje się bardzo
nienaturalnym rozwiązaniem zbudowanym na eleganckiej prostocie mechanizmu [[Prototype]].
Podsumowanie
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 181
0
Ostatecznie jeśli wprowadzone w specyfikacji ES6 słowo kluczowe class
utrudnia niezawodne wykorzystanie mechanizmu [[Prototype]] i ukrywa
najważniejszą naturę mechanizmu obiektowego w JavaScript — odbywające
się na żywo delegowanie między obiektami — to czy wobec tego nie powinniśmy postrzegać go jako wprowadzającego więcej problemów niż rozwiązań,
a tym samym uznać za antywzorzec?
Naprawdę nie potrafię odpowiedzieć za Ciebie na to pytanie. Mam jednak
nadzieję, że ta książka w pełni naświetliła problem, i to w znacznie dokładniejszy sposób niż gdziekolwiek indziej, oraz dostarczyła Ci informacji niezbędnych do samodzielnego udzielenia odpowiedzi na wcześniejsze pytanie.
182 
Dodatek A ES6 class
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
DODATEK B
Podziękowania
Zarówno ta książka, jak i pozostałe z tej serii powstały dzięki pomocy wielu
osób, którym pragnę podziękować.
Przede wszystkim dziękuję mojej żonie, Christen Simpson, oraz dwójce naszych dzieci — Ethanowi i Emily, za cierpliwe znoszenie widoku taty nieustannie stukającego w klawiaturę komputera. Nawet jeśli nie piszę książek,
moje obsesyjne zainteresowanie językiem JavaScript sprawia, że wgapiam się
w ekran monitora znacznie częściej, niż powinienem. Tylko dlatego, że mam
tak wyrozumiałą rodzinę, mogły powstać książki w pełni i dogłębnie wyjaśniające JavaScript. Wszystko zawdzięczam mojej rodzinie.
Dziękuję moim redaktorom w wydawnictwie O’Reilly, w szczególności Simonowi St. Laurentowi i Brianowi MacDonaldowi, a także pozostałym redaktorom i pracownikom działu marketingu. Fantastycznie się z nimi pracuje —
okazali się niezwykle pomocni podczas eksperymentu, jakim było napisanie,
edycja i przygotowanie książki open source.
Dziękuję wielu osobom, które uczestniczyły w opracowaniu serii książek poprzez przekazywanie sugestii i przeprowadzenie korekty — na podziękowania
szczególnie zasłużyli: Shelley Powers, Tim Ferro, Evan Borden, Forrest L.
Norvell, Jennifer Davis, Jesse Harlin, Nick Berardi i wiele innych osób.
Dziękuję niezliczonej liczbie członków społeczności, zwłaszcza składowi komitetu TC39, którzy dzielili się swoją wiedzą, a moje ciągłe pytania i eksperymenty znosili z niezwykłą cierpliwością, udzielając przy tym wyczerpujących
odpowiedzi. Są to: John-David Dalton, Juriy „kangax” Zaytsev, Mathias Bynens,
Rick Waldron, Axel Rauschmayer, Nicholas Zakas, Angus Croll, Jordan Harband, Reginald Braithwaite, Dave Herman, Brendan Eich, Allen Wirfs-Brock,
183
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Bradley Meck, Domenic Denicola, David Walsh, Tim Disney, Kris Kowal,
Peter van der Zee, Andrea Giammarchi, Kit Cambridge — to Wam i jeszcze
wielu innym osobom serdecznie dziękuję.
Powstanie serii Tajniki języka JavaScript było możliwe dzięki projektowi
Kickstarter, a przede wszystkim jego (prawie) 500 hojnym uczestnikom, bez
których wsparcia finansowego ta seria nigdy by nie powstała. Są to:
Jan Szpila, nokiko, Murali Krishnamoorthy, Ryan Joy, Craig Patchett, pdqtrader,
Dale Fukami, ray hatfield, R0drigo Perez [Mx], Dan Petitt, Jack Franklin, Andrew
Berry, Brian Grinstead, Rob Sutherland, Sergi Meseguer, Phillip Gourley, Mark
Watson, Jeff Carouth, Alfredo Sumaran, Martin Sachse, Marcio Barrios, Dan,
AimelyneM, Matt Sullivan, Delnatte Pierre-Antoine, Jake Smith, Eugen Tudorancea, Iris, David Trinh, simonstl, Ray Daly, Uros Gruber, Justin Myers, Shai
Zonis, Mom & Dad, Devin Clark, Dennis Palmer, Brian Panahi Johnson, Josh
Marshall, Marshall, Dennis Kerr, Matt Steele, Erik Slagter, Sacah, Justin Rainbow, Christian Nilsson, Delapouite, D. Pereira, Nicolas Hoizey, George V. Reilly,
Dan Reeves, Bruno Laturner, Chad Jennings, Shane King, Jeremiah Lee Cohick,
od3n, Stan Yamane, Marko Vucinic, Jim B, Stephen Collins, Ægir Þorsteinsson,
Eric Pederson, Owain, Nathan Smith, Jeanetteurphy, Alexandre ELISÉ, Chris
Peterson, Rik Watson, Luke Matthews, Justin Lowery, Morten Nielsen, Vernon
Kesner, Chetan Shenoy, Paul Tregoing, Marc Grabanski, Dion Almaer, Andrew
Sullivan, Keith Elsass, Tom Burke, Brian Ashenfelter, David Stuart, Karl Swedberg,
Graeme, Brandon Hays, John Christopher, Gior, manoj reddy, Chad Smith,
Jared Harbour, Minoru TODA, Chris Wigley, Daniel Mee, Mike, Handyface,
Alex Jahraus, Carl Furrow, Rob Foulkrod, Max Shishkin, Leigh Penny Jr.,
Robert Ferguson, Mike van Hoenselaar, Hasse Schougaard, rajan venkataguru,
Jeff Adams, Trae Robbins, Rolf Langenhuijzen, Jorge Antunes, Alex Koloskov,
Hugh Greenish, Tim Jones, Jose Ochoa, Michael Brennan-White, Naga Harish
Muvva, Barkóczi Dávid, Kitt Hodsden, Paul McGraw, Sascha Goldhofer,
Andrew Metcalf, Markus Krogh, Michael Mathews, Matt Jared, Juanfran,
Georgie Kirschner, Kenny Lee, Ted Zhang, Amit Pahwa, Inbal Sinai, Dan Raine,
Schabse Laks, Michael Tervoort, Alexandre Abreu, Alan Joseph Williams,
NicolasD, Cindy Wong, Reg Braithwaite, LocalPCGuy, Jon Friskics, Chris
Merriman, John Pena, Jacob Katz, Sue Lockwood, Magnus Johansson, Jeremy
Crapsey, Grzegorz Pawłowski, nico nuzzaci, Christine Wilks, Hans Bergren,
charles montgomery, Ariel ‫לבב‬-‫ בר‬Fogel, Ivan Kolev, Daniel Campos, Hugh
Wood, Christian Bradford, Frédéric Harper, Ionuţ Dan Popa, Jeff Trimble,
Rupert Wood, Trey Carrico, Pancho Lopez, Joël kuijten, Tom A Marra, Jeff
Jewiss, Jacob Rios, Paolo Di Stefano, Soledad Penades, Chris Gerber, Andrey
Dolganov, Wil Moore III, Thomas Martineau, Kareem, Ben Thouret, Udi Nir,
Morgan Laupies, jory carsonburson, Nathan L Smith, Eric Damon Walters,
184 
Dodatek B Podziękowania
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Derry Lozano-Hoyland, Geoffrey Wiseman, mkeehner, KatieK, Scott MacFarlane,
Brian LaShomb, Adrien Mas, christopher ross, Ian Littman, Dan Atkinson,
Elliot Jobe, Nick Dozier, Peter Wooley, John Hoover, dan, Martin A. Jackson,
Héctor Fernando Hurtado, andy ennamorato, Paul Seltmann, Melissa Gore,
Dave Pollard, Jack Smith, Philip Da Silva, Guy Israeli, @megalithic, Damian
Crawford, Felix Gliesche, April Carter Grant, Heidi, jim tierney, Andrea
Giammarchi, Nico Vignola, Don Jones, Chris Hartjes, Alex Howes, john gibbon,
David J. Groom, BBox, Yu Dilys Sun, Nate Steiner, Brandon Satrom, Brian
Wyant, Wesley Hales, Ian Pouncey, Timothy Kevin Oxley, George Terezakis,
sanjay raj, Jordan Harband, Marko McLion, Wolfgang Kaufmann, Pascal
Peuckert, Dave Nugent, Markus Liebelt, Welling Guzman, Nick Cooley, Daniel
Mesquita, Robert Syvarth, Chris Coyier, Rémy Bach, Adam Dougal, Alistair
Duggin, David Loidolt, Ed Richer, Brian Chenault, GoldFire Studios, Carles
Andrés, Carlos Cabo, Yuya Saito, roberto ricardo, Barnett Klane, Mike Moore,
Kevin Marx, Justin Love, Joe Taylor, Paul Dijou, Michael Kohler, Rob Cassie,
Mike Tierney, Cody Leroy Lindley, tofuji, Shimon Schwartz, Raymond, Luc
De Brouwer, David Hayes, Rhys Brett-Bowen, Dmitry, Aziz Khoury, Dean,
Scott Tolinski — Level Up, Clement Boirie, Djordje Lukic, Anton Kotenko,
Rafael Corral, Philip Hurwitz, Jonathan Pidgeon, Jason Campbell, Joseph C.,
SwiftOne, Jan Hohner, Derick Bailey, getify, Daniel Cousineau, Chris Charlton,
Eric Turner, David Turner, Joël Galeran, Dharma Vagabond, adam, Dirk van
Bergen, dave ♥♫★ furf, Vedran Zakanj, Ryan McAllen, Natalie Patrice Tucker,
Eric J. Bivona, Adam Spooner, Aaron Cavano, Kelly Packer, Eric J, Martin
Drenovac, Emilis, Michael Pelikan, Scott F. Walter, Josh Freeman, Brandon
Hudgeons, vijay chennupati, Bill Glennon, Robin R., Troy Forster, otaku_coder,
Brad, Scott, Frederick Ostrander, Adam Brill, Seb Flippence, Michael Anderson,
Jacob, Adam Randlett, Standard, Joshua Clanton, Sebastian Kouba, Chris Deck,
SwordFire, Hannes Papenberg, Richard Woeber, hnzz, Rob Crowther, Jedidiah
Broadbent, Sergey Chernyshev, Jay-Ar Jamon, Ben Combee, luciano bonachela,
Mark Tomlinson, Kit Cambridge, Michael Melgares, Jacob Adams, Adrian
Bruinhout, Bev Wieber, Scott Puleo, Thomas Herzog, April Leone, Daniel
Mizieliński, Kees van Ginkel, Jon Abrams, Erwin Heiser, Avi Laviad, David
newell, Jean-Francois Turcot, Niko Roberts, Erik Dana, Charles Neill, Aaron
Holmes, Grzegorz Ziółkowski, Nathan Youngman, Timothy, Jacob Mather,
Michael Allan, Mohit Seth, Ryan Ewing, Benjamin Van Treese, Marcelo Santos,
Denis Wolf, Phil Keys, Chris Yung, Timo Tijhof, Martin Lekvall, Agendine,
Greg Whitworth, Helen Humphrey, Dougal Campbell, Johannes Harth, Bruno
Girin, Brian Hough, Darren Newton, Craig McPheat, Olivier Tille, Dennis
Roethig, Mathias Bynens, Brendan Stromberger, sundeep, John Meyer, Ron
Male, John F Croston III, gigante, Carl Bergenhem, B.J. May, Rebekah Tyler,
Ted Foxberry, Jordan Reese, Terry Suitor, afeliz, Tom Kiefer, Darragh Duffy,
Kevin Vanderbeken, Andy Pearson, Simon Mac Donald, Abid Din, Chris Joel,
Podziękowania
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 185
0
Tomas Theunissen, David Dick, Paul Grock, Brandon Wood, John Weis,
dgrebb, Nick Jenkins, Chuck Lane, Johnny Megahan, marzsman, Tatu Tamminen,
Geoffrey Knauth, Alexander Tarmolov, Jeremy Tymes, Chad Auld, Sean Parmelee,
Rob Staenke, Dan Bender, Yannick derwa, Joshua Jones, Geert Plaisier, Tom
LeZotte, Christen Simpson, Stefan Bruvik, Justin Falcone, Carlos Santana,
Michael Weiss, Pablo Villoslada, Peter deHaan, Dimitris Iliopoulos, seyDoggy,
Adam Jordens, Noah Kantrowitz, Amol M, Matthew Winnard, Dirk Ginader,
Phinam Bui, David Rapson, Andrew Baxter, Florian Bougel, Michael George,
Alban Escalier, Daniel Sellers, Sasha Rudan, John Green, Robert Kowalski,
David I. Teixeira (@ditma), Charles Carpenter, Justin Yost, Sam S, Denis
Ciccale, Kevin Sheurs, Yannick Croissant, Pau Fracés, Stephen McGowan,
Shawn Searcy, Chris Ruppel, Kevin Lamping, Jessica Campbell, Christopher
Schmitt, Sablons, Jonathan Reisdorf, Bunni Gek, Teddy Huff, Michael Mullany,
Michael Fürstenberg, Carl Henderson, Rick Yoesting, Scott Nichols, Hernán
Ciudad, Andrew Maier, Mike Stapp, Jesse Shawl, Sérgio Lopes, jsulak, Shawn
Price, Joel Clermont, Chris Ridmann, Sean Timm, Jason Finch, Aiden Montgomery, Elijah Manor, Derek Gathright, Jesse Harlin, Dillon Curry, Courtney
Myers, Diego Cadenas, Arne de Bree, João Paulo Dubas, James Taylor, Philipp
Kraeutli, Mihai Păun, Sam Gharegozlou, joshjs, Matt Murchison, Eric Windham,
Timo Behrmann, Andrew Hall, joshua price i Théophile Villard.
Niniejsza seria książek to projekty open source, włącznie z etapami edycji
i produkcji. Dziękuję serwisowi GitHub za umożliwienie realizacji tego rodzaju przedsięwzięcia.
Jeszcze raz dziękuję wszystkim niezliczonym osobom, którym bardzo wiele
zawdzięczam, a nie jestem im w stanie imiennie podziękować. Niech ta seria
stanie się własnością nas wszystkich i przyczyni się do zwiększenia świadomości
i zrozumienia języka JavaScript z korzyścią dla wszystkich obecnych i przyszłych członków społeczności.
186 
Dodatek B Podziękowania
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
Skorowidz
A
E
egzemplarz klasy, 86
ES6 class, 173
analiza związków klasy, 127
API, 33
F
B
format JSON, 62
funkcja
@@iterator, 79
bind(), 39
create(), 132
foo(), 16
forEach(), 78
funkcje
anonimowe, 167
klasy, 113
pomocnicze, 65
bezpieczniejsze this, 42
błąd TypeError, 65, 69
D
debugowanie, 145
delegowanie, 117, 139, 145, 171
obiektów widżetów, 156
deskryptory właściwości, 63
dodawanie właściwości, 69
domieszki, 98
jawne, 98
kopiujące, 101
niejawne, 105
dynamiczność, 180
dziedziczenie, 111, 123
klasy, 92
pasożytnicze, 103
prototypowe, 124
wielokrotne, 97
G
Get, 70
getter, 72
I
indeksy liczbowe, 60
informacje o stanie, 15
introspekcja, 127
typu, 168
iteracja, 77
187
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
J
N
JavaScript, 9
nawias kwadratowy, 57
nazwy obliczanych właściwości, 57
new, 34, 118
niejawna utrata, 28
niejawne wiązanie, 58
niemodyfikowalność, 67
K
klasa, 85, 113, 141, 153
Controller, 164
klasy
JavaScript, 88
nadrzędne, 162
potomne, 162
widżetów, 153
kod OLOO, 143
konstruktor, 35, 91, 118–121
kontekst, 14
wywołań, 33
kontroler AuthController, 162
kopia
głęboka, 61
obiektu, 115
płytka, 61
O
obiekt, 15, 51, 153
docelowy, 62
kontekstu, 14
składnia literalna, 51
skonstruowany, 51
wbudowany, 53
źródłowy, 62
Object.create(), 133
Object.prototype, 109
odwołania pośrednie, 44
OLOO, 143, 158
OOP, object-oriented programming,
85, 148
operator
delete, 66
in, 75
instanceof, 128, 168
new, 34
osłabienie wiązania, 44
L
leksykalne this, 46
Ł
łącza, 115, 135
obiektu, 131
P
M
pętla for-in, 77
pobieranie wartości właściwości, 74
polimorfizm, 87, 94, 100, 164
porównanie modeli, 148
powielanie obiektów, 61
programowanie
funkcjonalne, 87
zorientowane obiektowo, 85, 150
Prototype, 116
mechanika, 120
klas, 89
mechanizm
class, 181
Prototype, 107, 116, 135
this, 13–22
metoda, 57
modele mentalne, 148
188 
Skorowidz
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
typy, 52
proste, 52
wyliczeniowe, 75
prototypy, 107
przesłanianie
metod, 112
właściwości, 110
Put, 71
U
ustalenie this, 40
ustawianie właściwości, 110
R
rekurencja, 15
W
S
wartość NaN, 17
wiązanie
domyślne, 25
jawne, 30
new, 34, 36
niejawne, 27
this, 13–22
twarde, 31
widżet, 153
właściwość, 57
Configurable, 65
count, 17
dostępu, 70
enumerable, 67
writable, 64
wyjątki dotyczące wiązań, 41
wykonanie funkcji, 35
wyrażenie funkcji anonimowej, 166
wywołanie funkcji, 23, 119
hasOwnProperty(), 75
konstruktora, 35
Object.create(), 132
Object.defineProperty(), 81
Object.freeze(), 69
Object.seal(), 69
wyzwalacz, 120
wzajemne delegowanie, 145
wzorzec projektowy klasy, 87
schemat dziedziczenia obiektów, 91
setter, 72
składnia, 165
class, 155, 176
obiektu, 51
słowo kluczowe
class, 131, 165, 174, 180
function, 165
new, 34, 39, 118
super, 178
this, 13–22
stała obiektu, 68
stos wywołań, 23
styl OLOO, 152
T
tablica, 60
teoria
delegowania, 142
klas, 85, 141
this, 13–22
bezpieczniejsze, 42
leksykalne, 46
ustalenie, 40
zignorowane, 41
tworzenie
klas, 89
łącza, 115, 132
Skorowidz
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
 189
0
Z
Ź
zakres funkcji, 20
zawartość obiektu, 55
zignorowane this, 41
zmienna globalna, 17, 25
190 
źródło wywołania funkcji, 23
Skorowidz
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
O autorze
Kyle Simpson jest pochodzącym z Austin w Teksasie propagatorem Open Web
i wielkim pasjonatem wszystkiego, co jest związane z językiem JavaScript. Pisze
książki, prowadzi warsztaty, występuje na konferencjach o tematyce technicznej
oraz pozostaje aktywnym członkiem społeczności OSS.
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0
helion kopia dla: Dawid Karwot [email protected]
006e9c6d9c8c46149a5704179b57cf99
0

Podobne dokumenty