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.
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.
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.
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? :):
new Cons() zwraca ten obiekt.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.
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.
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.
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.
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] }"
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 :)
Ale masz brzydkie fonty w tym Firebugu.
Te obrazki wstawione tutaj na bloga się brzydko przeskalowaly, bo tylko w poziomie.
Dzieki. Z ciekawoscia przeczytalem zarowno pierwsza jak i druga czesc
Piotrek, jak zwykle bardzo dobry artykul. Co do ostatniego akapitu z .constructor, mozesz zrobic tak:
var fn = function() {
var obj = {};
obj.constructor = fn;
obj.cos = 1;
return obj;
};
var o = new fn;
o.constructor.toString();
Pozdrawiam
O :) Nie wiedziałem, że constructor można zmieniać.
Z ciekawości - który sposób tworzenia konstruktorów preferujesz? Pamiętam, że jeszcze o problemie z prototypem rozmawialiśmy, ale pokazałeś mi dziedziczenie funkcyjne. Tak więc wydaje się, że obydwie metody są równie funkcjonalne. Czy może jednak w praktyce wychodzi, że metoda ze zwracaniem obiektu wymaga tyle nadmiarowego kodu by zachowywać się "ładnie", że jest niewygodna?
ferrante, mi się takie konstrukcje zawsze trochę nieeleganckie wydawały ze względu na to że mam fn "na zewnątrz" i "wewnątrz", tzn jak bym np. chciał zmienić "nazwę" konstruktora "fn" na "foo" to muszę zaglądać do wnętrza kodu i tam też zmieniać, preferuję takie cuś (nie twierdzę że tak się powinno robić, tylko że mi się bardziej podoba)
js> var Cons4 = (function (f) { f = function () { var obj = { sth: 'sth' }; obj.constructor = f; return obj; }; return f; })();
js> var x = Cons4();
js> var y = Cons4();
js> x.constructor === y.constructor
true
js> x.constructor === Cons4
true
js> x.constructor
function () {
var obj = {sth:"sth"};
obj.constructor = f;
return obj;
}
Szczerze... to już dla mnie przerost formy nad treścią :D Tzn tak samo inne dziwne rozwiązania, gdzie trzeba dopisać coś żeby zaczęły działać w spodziewany sposób. Chyba to jest główny powód dla którego lubię rozwiązanie pierwsze. Po prostu tak jest i działa.
w tym konkretnym przypadku (który do niczego prócz demonstracji nie służy) owszem, tyle że wersja przyjemniejsza dla oka nie jest odporna na pewne sytuacje, a do innych w ogóle się nie nadaje ( np. kiedy masz listę konstruktorów a nie każdy przypisany do osobnej zmiennej).
tzn. pisząc o wersji przyjemniejszej miałem na myśli tę ferrente z "fn".
btw, co do przerostu formy nad treścią -- w przypadku js sprawa z tworzeniem obiektów nie jest oczywista (o czym zresztą traktuje Twój artykuł), poczytaj Crockforda: http://javascript.crockford.com/prototypal.html. Zgłosił to ale (http://www.crockford.com/javascript/recommend.html) nie obiło mi się o uszy że przeszło.
Właśnie to rozwiązanie zaproponowane przez Ferrante też uważam za niemiłe dla oka :) Tzn. lekko niemiłe, ale niemiłe. Zawsze to jest ta jedna linia, którą trzeba dopisać i o której można zapomnieć. Do tego od razu przechodzimy do dziedziczenia funkcyjnego, które co prawda jest ładne, ale też nie jest "czyste" i żeby __proto__ zaczęło działać (tylko po co komu ono) trzeba kilka linii zawsze dopisać.
Niby .constructor i .__proto__ przydatne raczej nie są. Jednak o ile z jakiegoś powodu nie musimy/wygodniej nam skorzystać z rozwiązania z object literalsami, to po co sobie kombinować życie? Tylko, że ja mam za małe doświadczenie by stwierdzić, jak to w praniu wychodzi.
A za artykuły dzięki - przeczytam w wolnej chwili.
ja też nie wiem jak to w praniu wychodzi ;)
btw, wbrew temu co kiedyś było napisane na wikipedii język na którym JS jest troszkę (prototypowanie) wzorowany wciąż żyje i można by się pobawić celem poszerzenia horyzontów ;)
http://selflanguage.org/ ;)
Wracając jeszcze do tego problemu, która z metod jest lepsza, to tak myślę, że moc JSa drzemie właśnie w tym, że... ten problem istnieje :> Bo to oznacza, że język jest elastyczny i w zależności od kontekstu można wybrać wygodniejsze rozwiązanie. A nie jak Java - jedna klasa, druga klasa, interfejs, interfejs, jakieś abstrakcyjne bzdety i znowu klasy. W sumie z podobnego powodu python mi się spodobał - też jest elastyczny. Tylko ciągle czasu brakuje, żeby się go nauczyć porządnie :)
Szymon, no ok, Twoje jest bardzo eleganckie, ale ja tylko podawalem przyklad, ze da sie przypisac do objektu .constructor ;-) No less, no more.
Inna sprawa, abstrahując od tej dyskusji to, że dawno już nie musiałem używać instanceof i bawić się .constructorami. Anonimowe obiekty w wiekszosci przypadkow wystarczaja.
Generalnie dla mnie najlepszy jest sposob podany przez Szymona, a ze tego typu dziwolagi sie uzywa.. spójrzcie sobie na generatory klas w mootools na przyklad ;-)
Pozdrawiam
Hehe ;> To ja widać jestem purystą.
Z drugiej strony zacząłem dzisiaj pisać małą biblioteczkę na własny użytek i po 100 liniach kodu dochodzę od wniosku że dziwne konstrukcje tam po tworzyłem (niekoniecznie dobre, pewnie ;). Może to ten język tak wymaga ;)?