W jednym z wcześniejszych wpisów, przedstawiłem różnice pomiędzy używaniem prototype a this podczas definiowania ‚klas’ w JS. Tym razem opiszę proste sposoby tworzenia instancji obiektów w JS.
Informacje o podstawowych wzorcach tworzenia w JavaScript tu zawarte, zdecydowanie nie będą kompletnie wyczerpywać temat tego typu danych w JS. Potraktujcie to jako wprowadzeniem to bardziej obszernego tematu.
Obiekty w JavaScript to typy danych, które przechowują informacje w parach: klucz, wartość. Aby otrzymać interesującą programistę wartość, musi on użyć notacji z kropką, lub nawiasów kwadratowych. Czyli po nazwie obiektu stawia się kropkę a następnie nazwę klucza prowadzącego do wartości, lub (zamiast kropki), nawiasy kwadratowe zawierające klucz w formie łańcucha znaków:
1 2 |
Obiekt.klucz Obiekt["klucz"] |
Przechowywanymi w obiektach wartościami mogą, być zwykłe zmienne, nazywane wtedy polami obiektu, lub funkcje, nazywane wtedy metodami obiektu. Do tego muszę dodać też, że obiekty mają możliwość dziedziczenia wartości innych obiektów. Oznacza to, że jeden obiekt może posiadać zarówno własne wartości jak i wartości obiektu, z którego dziedziczy.
Ta, na pierwszy rzut oka, prosta struktura daje programistom ogromne możliwości i pozwala budować bardzo skomplikowane mechanizmy. To o czym dziś piszę, dotyczy jednak informacji raczej podstawowe, które można znaleźć w prawie każdej książce o JS dla początkujących.
Moje doświadczenie jednak jest takie, że często wspomniane wyżej książki nie przedstawiają wszystkich sposobów na tworzenie nowych obiektów. A jeżeli to robią, to nie pokazują dokładnie różnic pomiędzy tymi sposobami. Dlatego postanowiłem napisać post, w którym uporządkuję swoją wiedzę na temat różnych sposobów tworzenia obiektów w JS.
Literał obiektowy
Najprostszym sposobem na tworzenie obiektu w JS, jest użycie tak zwanego literału obiektowego. Oznacza to bezpośrednio wypisanie par klucz-wartość wewnątrz nawiasów klamrowych. Najczęściej, taki obiekt przypisuje się do jakiejś zmiennej.
1 2 3 |
var newObj = { newProp: "test" } |
W ten prosty sposób można stworzyć pierwszy obiekt stworzyć w JavaScript. Zawiera on jedno pole, którego klucz to newProp a wartość to łańcuch znaków o treści test. Oczywiście, w JavaScripcie pola i metody do obiektów mogę dodawać w każdej chwili, nawet po ich stworzeniu:
1 2 3 4 5 6 |
var newObj = {}; newObj.newProp = "test"; console.log(newObj.newProp); // loguje "test" console.log(newObj.hasOwnProperty("newProp")); // loguje true |
Tym razem tworzę pusty obiekt. Następnie przypisuję do niego pole newProp o takiej samej wartości jak wyżej. Aby pokazać, że wszystko gra jak trzeba, wywołuję newProp w konsoli.
Następnie w konsoli loguję też wynik wywołania metody hasOwnProperty obiektu newObj. Metoda ta przyjmuje jeden argument. Nazwę klucza w postaci łańcucha znaków. Jeżeli obiekt na którym wywołam hasOwnProperty posiada pole o kluczu z nazwą równą tej przekazanej w argumencie, zwrócone zostanie true. Co oznacza, że obiekt posiada to pole? Jest ono przypisane do obiektu, a nie znalezione w łańcuchu prototypów (jeżeli nie wiesz czym jest łańcuch prototypów w JS, koniecznie się dowiedz. Bez tej wiedzy, ani rusz z ‚obiektówką’ w JS ;)).
W tej chwili może nasunąć się jedno pytanie. Z jakiej paki newObj ma w sobie metodę hasOwnProperty? Nie przypisałem mu nic takiego. Przecież stworzyłem pusty obiekt. No cóż obiekt ten tylko w pewnym sensie jest pusty. Tak naprawdę posiada on też dane dziedziczone bezpośrednio z globalnego obiektu JS Object (z wielkiej litery).
Tak naprawdę, zapis:
1 |
var newObj = {} |
to to samo co:
1 |
var newObj = new Object() |
I tak przechodzę do kolejnego sposobu na tworzenie obiektów w JS.
Operator new i konstruktory
W JavaScripcie, nie ma klas. Ale jak to, przecież sam wiele razy tak nazywałem fragmenty swojego kodu. No cóż, po prostu możemy wykorzystywać funkcje tak aby trochę przypominały klasy 🙂
Używając składni funkcji, tworzy się ‚klasy’, czyli coś w rodzaju wzorca dla obiektu. Sam wzorzec na wiele się nie przyda, ale dzięki niemu bardzo łatwo i szybko jest utworzyć obiekty (wiele obiektów). Oto przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var ObjConstr = function(greeter) { this.greeter = greeter; this.greet = function(){ return greeter; }; } ObjConstr.prototype.ungreet = function(){ return "bye world!" } newObj = new ObjConstr("hello world!"); console.log(newObj.greet()); // loguje "hello world!" console.log(newObj.hasOwnProperty("greeter")); // loguje true console.log(newObj.hasOwnProperty("greet")); // loguje true console.log(newObj.ungreet()); // loguje "bye world!" console.log(newObj.hasOwnProperty("ungreet")); // loguje false |
ObjConstr to definicja mojego konstruktora. Zapis wygląda tak jak w przypadku normalnej funkcji. Jednak kod wewnątrz klamr, nie przypomina tego co w funkcji można zazwyczaj znaleźć. Zamiast tego, wewnątrz wypisuję dane, które będą polami obiektów, tworzonych na podstawie tego konstruktora. To własnie można by nazwać klasą, pamiętając, że nie jest to do końca to samo co w klasycznych językach obiektowych.
Aby stworzyć faktyczny obiekt, należy użyć operatora new, po którym następuje wywołanie konstruktora, do którego przekazać można dane dla pól nowo tworzonego obiektu. Taka operacja zwróci nowy obiekt, który dziedziczy bezpośredni z konstruktora. Będzie on posiadał własne kopie wszystkich pól. Co widać parę linijek niżej, gdzie używam metody hasOwnProperty.
Tak jak pisałem w jednym ze wcześniejszych postów, można przypisywać metody do prototypu konstruktora, które też będą dziedziczone ale nie bezpośrednio. Konsekwencje tego przedstawiłem w osobnym wpisie. Dla przypomnienia w przykładzie dodałem do prototypu mojego konstruktora metodę ungreet. Jak widać, jeśli wrzuci się ją do hasOwnProperty, zwrócone zostanie false.
Dostępny jest jeszcze jeden sposób na tworzenie obiektów, który działa trochę inaczej niż podejście z konstruktorami.
Object.create()
create to metoda głównego obiektu JS – Object. Wywołanie jej zwraca nowy obiekt, który dziedziczy z Object. Jak widać, działa podobnie to stworzenia pustego obiektu literałem. Różnica polega na tym, że do Object.create można przekazać argument będący dowolnym innym obiektem JS. Wtedy zwrócony zostanie obiekt, który dziedziczy bezpośredni z prototypu przekazanego obiektu. Co to oznacza? Najpierw przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var newObj = {}; newObj.newProp = "test"; newObj1 = Object.create(newObj); newObj2 = Object.create(newObj); newObj2.newProp = "newTest"; console.log(newObj1.newProp); //loguje "test"; console.log(newObj1.hasOwnProperty("newProp")); //loguje false!; console.log(newObj2.newProp); //loguje "newTest"; console.log(newObj2.hasOwnProperty("newProp")); //loguje true!; |
Jak widać, instancja stworzona za pomocą Object.create nie posiada własnych kopii pól, a jedynie sięga do prototypu przekazanego w argumencie create obiektu. Dowodem na to jest wartość false zwracana przez hasOwnProperty
Dopiero gdy ustawie jakieś pole bezpośrednio na nowym obiekcie (przykład newObj2), hasOwnProperty zwróci true.
To co należy zapamiętać, to to, że jeżeli zmienimy prototyp, to zmieni się też działanie obiektów. Może to być bardzo przydatne, ale też dla nieświadomych programistów, może powodować kłopotliwe błędy.
Na koniec nadmienię tylko, że metoda create może przyjąć też drugi argument. W nim można zdefiniować nowe pola na tworzonym obiekcie. Szczerze mówiąc nie używałem nigdy tej opcji, więc nie wiem do końca jak się zachowuje, dlatego nie będę udawał, że wiem 🙂 To jest coś, co każdy może doczytać na własną rękę.
Podsumowanie
Nie będę tutaj pisał, który sposób jest lepszy, a który gorszy. Szczerze mówiąc, uważam, że to wszystko zależy od okoliczności. Ale nie o tym jest wpis. Jedno jest pewne, jako programiści JS, na pewno będziemy mieli styczność z każdym z tych sposobów w swoim kodzie, lub w kodzie innych ludzi. Warto wiedzieć co się wtedy dzieje i jak działa.
Posiadając tę podstawową wiedzę, mogę zacząć tworzyć bardziej zaawansowane wzorce tworzenia i obsługiwania obiektów. To jednak opiszę w przyszłych wpisach.
Na koniec, jak zawsze, zachęcam do polubienia mojej strony na facebooku. Zawsze na bieżąco zamieszczam tam informacje o nowościach, więc warto polubić aby nie przegapić żadnego nowego wpisu.
Fajny wpis, czekam na więcej! 🙂
Do przykładu „Operator new i konstruktory” wkradł się chyba błąd – w linii 12 jest:
ObjConstr = new Constr(„hello world!”);
co skutkuje następującym błędem w konsoli: Uncaught ReferenceError: Constr is not defined.
Zgaduję, że miałeś na myśli:
var constr = new ObjConstr(„hello world!”);
i dalej: console.log(constr.greet());
Czy coś pomieszałem?
Faktycznie, babol! Nic dziwnego, próbowałem wywołać niezdefiniowany konstruktor. Już poprawione, teraz powinno śmigać 🙂 Dzięki za czujność.