Tematem dzisiejszego wpisu, jak możesz przeczytać w jego tytule, jest tworzenie obiektów JavaScript. W związku z tym postaram się przedstawić kilka wzorców i dobrych praktyk związanych z tym tematem, przyjrzę się także pułapkom, na które możemy natrafić. Początkowo chciałem skupić się tylko na tworzeniu własnych konstruktorów ale myślę, że omówienie tutaj również notacji literałowej (zarówno dla tworzenia zwykłych obiektów jak i tablic) będzie dobrym tłem dla tego problemu i pozwoli lepiej poznać dobre praktyki oraz wzorce tworzenia obiektów - jako kontrapunkt nie zabraknie również wyjaśnienia dlaczego stosowanie konstruktorów wbudowanych nie jest najlepszym pomysłem.

Notacja literałowa

Jak wiadomo, język JavaScript nie posiada klas jako takich, posiada natomiast obiekty, które tworzyć możemy na kilka sposobów. Jednym z nich jest notacja literałowa, która pozwala w intuicyjny sposób tworzyć obiekty rozumiane jako coś w rodzaju tablicy asocjacyjnej (znanej z innych języków programowania) czyli zestawu parametrów “para -> wartość”. Wartościami mogą być oczywiście zarówno typy proste, inne obiekty (mówimy wówczas o właściwościach obiektu) jak i funkcje (czyli metody obiektu).

Składnia literału jest prosta: zawartość obiektu należy zawrzeć pomiędzy nawiasami klamrowymi, poszczególne elementy literału oddzielamy przecinkami, a nazwę i wartość oddziela dwukropek. Spójrzmy na przykład tworzenia obiektu za pomocą notacji literałowej:

var car = {
    type: 'Skoda',
    sound: function() {
        return 'brum brum brum...';
    }
};

W powyższym kodzie mamy definicję obiektu z jednoczesnym przypisaniem go do zmiennej ‘car’. Obiekt ten zawiera właściwość ‘type’, a także metodę ‘sound’. Język JavaScript jest jak wiemy bardzo elastyczny, dlatego nie ma problemu aby w tym momencie dodać do naszego obiektu dodatkową właściwość:

car.model = 'Octavia';

W dokładnie taki sam sposób można też oczywiście dodawać kolejne właściwości oraz metody, a także zmieniać ich wartości. Możliwe jest także usuwanie właściwości lub metod z obiektu - poniżej przykład tego jak to zrobić:

delete car.model;

Z powyższych przykładów wynika, że możliwe jest także rozpoczęcie od całkiem pustego obiektu aby później dowolnie go zmieniać:

var car = {};

Oczywiście należy pamiętać, że nie jest to tak do końca “pusty obiekt” - tak naprawdę dziedziczy on metody i właściwości po obiekcie ‘Object.prototype’, tak jak każdy inny obiekt w JavaScript.

Jak widać, tworzenie obiektów JavaScript za pomoc notacji literałowej pozwala na spójną i mniej narażoną na błędy definicję obiektów, a dzięki właściwości języka JavaScript pozwalającej na dowolne dodawanie, usuwanie i modyfikację właściwości i metod jest najlepszym sposobem na tworzenie obiektów na żądanie. Taki sposób definicji obiektów (w połączeniu z literałem tablicy, o czym za chwilę) został też zastosowany w formacie przesyłu danych JSON.

Literał tablicy

Skoro jesteśmy już przy notacji literałowej podczas tworzenia obiektów, warto wspomnieć jeszcze o notacji literałowej przy tworzeniu tablic. Najlepiej spojrzeć na przykład takiej definicji tablicy:

var array = ['one', 'two', 'three'];

O powyższym nie można powiedzieć zbyt wiele - jest po prostu lista elementów tablicy oddzielona przecinkami i zawarta pomiędzy nawiasami kwadratowymi. Dodatkowo, czego tutaj w przykładzie nie widać, każdy z elementów może być innego typu.

Pisząc już o literale tablicy, warto wspomnieć dlaczego użycie zamiast niej konstruktora ‘Array’ nie jest najlepszym pomysłem. Otóż, standardowo można go użyć do inicjalizacji tablicy, w poniższy sposób:

var array = new Array('jeden', 'dwa', 3);

Kod taki spowoduje zainicjowanie tablicy trzema podanymi jako parametry elementami. Tutaj jednak kryje się pewna pułapka ponieważ konstruktor ten został przeciążony i poniższy kod zachowa się zgoła inaczej:

var array = new Array(2);

W tym przypadku, zamiast utworzyć tablicę jednoelementową i umieścić tam wartość ‘2’, utworzona zostanie pusta tablica dwuelementowa!! Widać więc dobitnie, że zastosowanie literału tablicy jest w tym przypadku zdecydowanie bezpieczniejsze.

Tworzenie obiektów JavaScript za pomocą konstruktorów wbudowanych

Oprócz opisanej powyżej notacji literałowej, możliwe jest również na tworzenie obiektów JavaScript za pomocą funkcji konstruujących, które przypominają trochę konstruktory z innych języków programowania - możliwe jest tworzenie własnych funkcji konstruujących (o czym w kolejnym paragrafie) jak i wykorzystanie funkcji wbudowanych, na przykład ‘Object()’ czy ‘String()’. Poniżej przykład użycia funkcji ‘Object()’:

car = new Object();
car.type = 'Skoda';
car.sound = function() {
    return 'brum brum brum';
};

Myślę, że widać wyraźnie przewagę notacji literałowej nad powyższą jeśli chodzi o jej zwięzłość. Inna sprawa to wydajność - w przypadku użycia funkcji konstruującej, interpreter przeszukuje zakresy zmiennych by odszukać odpowiednie konstruktor (możliwe jest wszakże, że istnieje lokalna funkcja ‘Object()’).

Jeśli zaś chodzi o sam konstruktor ‘Object()’, to kryje się za nim jeszcze jeden “ficzer”, który może potencjalnie sprawiać problemy - otóż jest możliwość przekazania do niego dodatkowego parametru służącego do inicjalizacji obiektu. Na podstawie tego parametru funkcja konstruująca ‘Object()’ może przekazać tworzenie obiektu do innej wbudowanej funkcji konstruującej. Spójrzmy na przykład:

var test = new Object('ciąg znaków');

alert(test); // wyswietla ciag 'test'
alert(test.constructor === String); // true

Jak widać w linii czwartej, mimo że do utworzenia obiektu ‘test’ użyto funkcji ‘Object()’, to tak na prawdę pod spodem wykorzystana została funkcja ‘String()’. Z tego i z wcześniej wymienionych powodów, do tworzenia obiektów lepiej jest wykorzystywać literały obiektów, a obiektów wbudowanych używać tylko jako narzędziowych “helperów” (zawierają kilka przydatnych funkcji wbudowanych).

Własne funkcje konstruujące

Na wstępie chciałbym zaznaczyć, że moją intencją w tym punkcie jest przedstawić, jak tworzyć własne konstruktory i robić to dobrze, przy użyciu dobrych wzorców - dyskusja na temat tego czy powinno się unikać tworzenia obiektów przy użyciu “new” czy też nie to zupełnie inna historia i nie jest ona częścią dzisiejszego wpisu ;)

Własne funkcje konstruujące nazywane często, z powodu swojego podobieństwa do innych języków programowania, konstruktorami to w zasadzie zwykłe funkcje - spójrzmy na przykład:

var Car = function (typeName) {
    this.type = typeName;
    this.sound = function () {
        return this.type + ' robi brum brum';
    };
};

var skoda = new Car('Skoda');
alert(skoda.sound());

W powyższym przykładzie widzimy, że do zmiennej ‘Car’ przypisywana jest funkcja przyjmująca jeden parametr wejściowy (dobra praktyka: nazwy funkcji konstruujących, zaczynamy od wielkiej litery dla odróżnienia ich od pozostałych funkcji). Następnie w ciele funkcji definiowane są właściwości i metody obiektu tworzonego przez dany konstruktor. Trzeba tutaj zwrócić uwage na to, że właściwości i metody przypisywane są do zmiennej ‘this’. Tak na prawdę kilka rzeczy dzieje się tutaj niejawnie:

  • najpierw tworzony jest pusty obiekt (do którego odwołujemy się właśnie przez ‘this’); dziedziczy on po prototypie funkcji konstruującej
  • na końcu funkcji następuje niejawne zwrócenie obiektu ‘this’

W liniach 8 i 9 widać dodatkowo jak można skorzystać z tak utworzonego konstruktora.

Zwracanie wartości przez funkcję konstruującą

Powyżej wspomniałem o tym, że domyślnie metoda konstruująca zwraca wartość zmiennej ‘this’. Jest jednak możliwość zwracania zupełnie innego obiektu - tutaj mała uwaga: nie może to być nic innego jak obiekt, a więc na przykład tekst odpada… jeśli byśmy mimo wszystko spróbowali zwrócić ciąg znaków lub na przykład wartość logiczna, wówczas interpreter zignoruje to i zamiast tego zachowa się domyślnie, czyli zwróci ‘this’.

Wróćmy jednak do zwracania innego obiektu niż ‘this’ - oto przykład:

var Car = function (typeName) {
    this.type = typeName;
    this.sound = function () {
        return this.type + ' robi brum brum';
    };

    var differentCar = { type : 'Volkswagen' };
    return differentCar;
};

var skoda = new Car('Skoda');
alert(skoda.type);

W porównaniu do poprzedniego przykładu, w liniach 8 i 9 pojawił się nowy kod, w którym tworzony jest nowy obiekt, a następnie jest on zwracany. Po wykonaniu tego kodu, na ekranie wyświetliłby się tekst “Volkswagen”, a więc wartość zawarta w tym nowo utworzonym obiekcie.

Wymuszanie użycia ‘new’

Jako, że konstruktor jest zwykłą funkcją, jeśli pominęlibyśmy słowo kluczowe ‘new’ funkcja konstruująca wykonałaby się jako zwykła funkcja. To mogłoby spowodować nieoczekiwane efekty, ponieważ zmienna ‘this’ zawarta w ciele funkcji zamiast dziedziczyć po konstruktorze funkcji, wskazywałaby na obiekt globalny (czyli w najczęstszym przypadku, w przeglądarce internetowej na obiekt ‘window’). W związku z powyższym istnieje kilka dobrych praktyk i wzorców, dzięki którym jesteśmy w stanie zabezpieczyć się przed takim niepożądanym zachowaniem.

Rozważmy pierwszy ze sposobów:

var Car = function (typeName) {
    var self = {};
    self.type = typeName;
    self.sound = function () {
        return self.type + ' robi brum brum';
    }

    return self;
};

Jak widać powyżej, z ciała funkcji konstruującej pozbyliśmy się całkowicie słowa ‘this’. Zamiast tego tworzymy zupełnie nowy obiekt ‘self’, a następnie wszystkie potrzebne właściwości i metody przypisujemy do niego. Na koniec to właśnie ten obiekt jest zwracany jako wynik działania funkcji. Dzięki takiej implementacji konstruktora mamy pewność, że zadziała on zawsze zgodnie z naszymi oczekiwaniami, niezależnie od tego czy zostanie wywołana z ‘new’ czy też bez niego.

Takie rozwiązanie może i byłoby super i w ogóle, ale nie ma tak dobrze;) W przypadku tego sposobu, jeśli dodalibyśmy nowe składowe do prototypu konstruktora, to nie byłyby one dostępne dla utworzonych przez niego obiektów, ponieważ są to zupełnie inne obiekty! Na szczęście istnieje lepszy sposób… Spójrzmy na przykład:

var Car = function (typeName) {
    if ((this instanceof Car) === false) {
        return new Car(typeName);
    }

    this.type = typeName;
    this.sound = function () {
        return this.type + ' robi brum brum';
    }
};

W kolejnej wersji naszego samochodowego przykładu, w liniach od 2 do 4 pojawił dodatkowy kod, sprawdzający czy ‘this’ wskazuje na instancję konstruktora ‘Car’ i w przypadku gdy tak nie jest wywołuje sam siebie przy użyciu ‘new’. W ten sposób mamy zapewnione prawidłowe działanie funkcji konstruującej niezależnie czy użyte zostanie ‘new’ czy też nie, a jednocześnie mamy dostęp do składowych jej prototypu.

Podsumowanie

Mam nadzieję, że teraz tworzenie obiektów JavaScript nie będzie już miało dla Ciebie tajemnic. Myślę, że prawie zawsze najlepszym wyborem jest użycie notacji literałowej, jednak skoro w języku istnieją również inne sposoby, warto je znać i wiedzieć jak korzystać z nich prawidłowo.