Obiektowy JavaScript cz.2. – klasa sama w sobie
Opublikowane na licencji CC przez Trufflepig.
W poprzednim artykule o obiektach w JavaScriptcie poruszyłem kwestię ich tworzenia, dostępu do właściwości, kontekstu wywoływania metod oraz wspomniałem o kilku cechach. Czyste literały obiektów same w sobie są już w tym języku bardzo przydatne — przekonał się pewnie o tym każdy kto korzystał z jakiejś biblioteki typu MooTools czy Prototype. Pozwalają na przykład w dość wygodny sposób na podanie dowolnej liczby nazwanych argumentów funkcji:
new Ajax.Request('/your/url', { parameters: { id: 12, action: 'sth' }, onSuccess: function (transport) { alert(transport.responseText); }, evalJS: false });
Pisałem ostatnio o tym, że w JavaScriptcie nie ma klas. Skąd więc w przykładzie słówko kluczowe new, które w typowym języku służy do tworzenia instancji obiektu na bazie jakiejś klasy? Otóż okazuje się, że w JavaScriptcie wprowadzono twór zwany konstruktorami obiektów.
Konstruktor obiektu
Zwyczajnie zaczyna się od definiowania klasy jakiegoś kotka, samochodu, czy figury geometrycznej. Nudy. Chciałem wymyślić coś bardziej konstruktywnego i spośród wszystkich dostępnych na moim biurku obiektów wybrałem butelkę po miodzie pitnym (pustą niestety). Myślę, że konstruktory dla napoju alkoholowego i butelki będą wystarczająco oryginalne :). Do dzieła:
var Alkohol = function (nazwa, ilosc_procentow, kolor) { this.nazwa = nazwa; this.ilosc_procentow = ilosc_procentow; this.kolor = kolor; }; var ButelkaAlkoholu = function (alkohol, pojemnosc, rok_produkcji, kraj_produkcji) { this.alkohol = alkohol; this.pojemnosc = pojemnosc; this.rok_produkcji = rok_produkcji; this.kraj_produkcji = kraj_produkcji; };
Oj tak. JavaScript to dziwoląg. Miały być konstruktory obiektów, a tu znowu pojawiły się funkcje. Z drugiej strony pojawiło się też słowo kluczowe this, więc cały przykład przypomina dwa wycięte z definicji klas napisanych w normalnym języku konstruktory. Na szczęście obiekty tworzymy już normalnie:
var miod = new Alkohol('Miód pitny', 13, '#850'); var butelka_miodu = new ButelkaAlkoholu(miod, 750, 2009, 'Polska'); console.dir(miod); // -> screenshot poniżej console.dir(butelka_miodu); // -> screenshot poniżej butelka_miodu.alkohol.nazwa; // -> "Miód pitny"

Utworzyliśmy w ten sposób dwa obiekty, których struktura została wylistowana przy pomocy Firebuga i którą przedstawiłem na screenie.
W poprzedniej części artykułu pisałem o tym, że obiekt może mieć swoje metody (inaczej funkcje przypisane jako właściwości obiektu). Musi więc być sposób aby w konstruktorze obiektu zdefiniować takie metody. Dodajmy więc kilka linii do konstruktora ButelkaAlkohol:
var ButelkaAlkoholu = function (alkohol, pojemnosc, rok_produkcji, kraj_produkcji) { //... this.pelna = true; this.oproznij = function () { this.pelna = false; }; }; var butelka_miodu = new ButelkaAlkoholu(miod, 750, 2009, 'Polska'); butelka_miodu.pelna; // -> true butelka_miodu.oproznij(); butelka_miodu.pelna; // -> false
Jak widać, aby utworzyć coś co możemy nazwać metodą, musimy przypisać funkcję do właściwości przyszłego obiektu. Uczulam znowu na działanie słowa this, o czym pisałem już poprzednim razem.
Alternatywna metoda tworzenia konstruktorów obiektów
Naszym celem jest utworzenie obiektu o zadanych przez nas wartościach właściwości. Pamiętając, że najprostszym sposobem na utworzenie obiektu jest skorzystanie z literału możemy dojść do następującej konstrukcji: stwórzmy funkcję zwracającją obiekt zapisany za pomocą literału:
var Alkohol2 = function (nazwa, ilosc_procentow, kolor) { var obj = { nazwa: nazwa, ilosc_procentow: ilosc_procentow, kolor: kolor, pelna: true, oproznij: function () { this.pelna = false; } }; return obj; }; var piwo = Alkohol2('piwo Tyskie', 5.6, '#FE3'); piwo; // -> Object nazwa=piwo Tyskie ilosc_procentow=5.6 kolor=#FE3
Tak więc skorzystaliśmy z funkcji w jej czysto funkcyjnym wymiarze. To jednak nie koniec. Dawno już nie było mowy o żadnym JavaScriptowym dziwactwie. Pora na następne. Otóż ten zapis również zadziała:
var piwo2 = new Alkohol2('piwo Tyskie', 5.6, '#FE3'); piwo2; // -> Object nazwa=piwo Tyskie ilosc_procentow=5.6 kolor=#FE3
Zonk.
Anegdota o użyciu new z funkcją
Kiedy dowiedziałem się o powyższym od razu zacząłem się zastanawiać jak w zasadzie JavaScript traktuje new w kontekście funkcji. Pierwsze co ciśnie się na palce, to modyfikacja przykładu z konstruktorem Alkohol2 tak by zwracał „nieobiekt”:
var Cons = function () { return 5; }; var obj = new Cons(); obj; // -> Object
Nic ciekawego — jakiś pusty obiekt otrzymaliśmy. Dodajmy więc właściwość (pierwszy sposób tworzenia konstruktorów):
var Cons = function () { this.a = 'a'; return 5; }; var obj = new Cons(); obj; // -> Object a=a
Ciekawe, choć do wytłumaczenia — return 5 jest pomijane. Połączmy teraz dwa sposoby tworzenia konstruktorów:
var Cons = function () { this.a = 'a'; return { b: 'b' }; }; var obj = new Cons(); obj; // -> Object b=b
Uups. JavaScript pomija właściwości zadeklarowane przy użyciu słowa this. W ten sposób dochodzimy do algorytmu, którym kieruje się ten język (a przynajmniej Firefox — może komuś się chce zajrzeć do specyfikacji? :):
- Jeśli zwracana przez funkcję wartość jest obiektem, to wyrażenie
new Cons()zwraca ten obiekt. - W przeciwnym wypadku wyrażenie to zwraca obiekt z właściwościami ustalonymi przy pomocy słowa
this.
Porównując obydwa sposoby pisania konstruktorów można dojść do wniosku, że lepsza jest ta druga, (ze zwracaniem gotowego obiektu), ponieważ działa na obydwa sposoby: new Cons() i Cons(). Osobiście uważam jednak, że ta metoda jest gorsza — dlaczego? O tym później w tej i w następnej części artykułu.
Elementy programowania obiektowego
Umiemy już tworzyć konstruktory obiektów, które można porównać do klas w zwyczajnym języku. Obiekty tworzone za ich pomocą posiadały do tej pory jednak tylko publiczne właściwości i metody. JavaScript nie udostępnia mechanizmu modyfikatorów dostępu, można jednak skorzystać z jego cech aby oprogramować w prosty sposób podobne konstrukcje.
Właściwości prywatne
var Cons = function () { var private = 'jestem prywatna'; this.getPrivate = function () { return private; }; }; var obj = new Cons(); obj.getPrivate(); // -> "jestem prywatna" obj.private; // -> undefined
Dlaczego to działa? Wszystko opiera się o kolejny dziwoląg, czyli JavaScriptowego scope’a, a także o closures (BTW. fajne tematy na kolejne artykuły). W skrócie — funkcja zadeklarowana w innej funkcji ma dostęp do zmiennych dostępnych w swoim „rodzicu”. Do tego mechanizm domknięć powoduje, że funkcja getPrivate „zapamiętuje” środowisko, w którym została zadeklarowana.
Właściwości statyczne
var Cons = function () { this.nonstatic = 'nie jestem statyczna'; }; Cons.static = 'jestem statyczna'; Cons.static; // -> "jestem statyczna" Cons.nonstatic; // -> undefined
Chyba nic nie trzeba wyjaśniać. Konstruktor Cons jak każda funkcja jest obiektem, więc można dodawać mu właściwości, co robimy w czwartej linii.
Dla jasności dodam, że w identyczny sposób tworzymy prywatne i statyczne metody obiektów i klas.
Dostęp z obiektu do konstruktora
Każdy obiekt posiada właściwość constructor, która daje dostęp do konstruktora (porównywalne do Javowego Object.getClass()):
var Cons = function () { this.sth = 'sth'; }; var obj = new Cons(); obj.constructor; // -> function() obj.constructor.toString(); // -> "function () { this.sth = "sth"; }"
A teraz niespodzianka. Dlaczego drugi sposób tworzenia konstruktorów (przez literał obiektu) uważam za gorszy? O to jeden z powodów:
var Cons2 = function () { return { sth: 'sth' }; }; var obj2 = Cons2(); var obj3 = new Cons2(); obj2.constructor; // -> Object() obj3.constructor; // -> Object() obj2.constructor.toString(); // -> "function Object() { [native code] }" obj3.constructor.toString(); // -> "function Object() { [native code] }"
Zdefiniowaliśmy konstruktor, który teoretycznie działa jak ten z poprzedniego przykładu (zwraca obiekt o tej samej właściwości). Okazuje się jednak, że kiedy spróbujemy dostać się do konstruktora tego obiektu, to nie jest nim funkcja Cons2, a zwykła, natywna Object. Dla porównania:
({}).constructor.toString(); // -> "function Object() { [native code] }"
Co dalej?
Umiemy już posługiwać się obiektami. Umiemy też tworzyć ich konstruktory. Pora więc na dziedziczenie. O tym za czas jakiś w następnej części :)
Artykuł został opublikowany także na moim blogu prywatnym.
Opublikowane na licencji CC przez 




