Stosowanie technik programowania obiektowego jest jednym z podstawowych paradygmatów języka JavaScript. Aby sprawnie się nimi posługiwać i tworzyć jak najlepsze aplikacje, warto przyswoić sobie odrobinę wiedzy na temat obiektów w JavaScript, gdyż prawie wszystko w tym języku jest traktowane jak obiekt. Nawet podstawowe z założenia typy danych, takie jak liczby czy łańcuchy znaków dostępne są w postaci obiektów.
Dzięki połączeniu technik programowania obiektowego oraz funkcyjnego, nasze aplikacje będą bardziej elastyczne, a kod bardziej czytelny i łatwiejszy w rozbudowie i utrzymaniu. Można powiedzieć, że JavaScript jest dosyć specyficznym językiem, jeśli chodzi o podejście do programowania obiektowego, dlatego ważne jest, aby dobrze zrozumieć mechanizmy leżące u podstaw jego działania, aby efektywnie korzystać z możliwości, które nam oferuje. Na początek zobaczmy jakie sposoby tworzenia obiektów dostępne są w JavaScript.
Notacja literałowa
Literały obiektowe są jednym z najprostszych sposobów na stworzenie obiektu w języku JavaScript. Zazwyczaj obiekt reprezentuje jakiś byt. Niech to będzie koszyk, jak w poniższym przykładzie:
const cart = {
cartItems: [],
addItem(product) {
// implementacja metody
}
};
Powyżej zdefiniowaliśmy obiekt cart
z własnością cartItems
. W stałej cart
, zapisaliśmy referencję do instancji koszyka. Tworzenie własności obiektu przy użyciu notacji literałowej polega na tworzeniu par indeks – wartość, zamkniętych między nawiasami klamrowymi. Do własności możemy przypisać dowolną wartość, a także inny obiekt. Z kolei metody obiektu reprezentują czynności, które dany obiekt może wykonywać.
Własności tak utworzonego obiektu możemy usuwać w trakcie działania programu:
delete cart.cartItems;
Konstruktory obiektów
W sytuacji kiedy potrzebujemy utworzyć kilka obiektów tego samego typu o identycznej strukturze, stosowanie notacji literałowej zmusza nas do powielania tego samego kodu, ponieważ tworzone w ten sposób obiekty nie nadają się do ponownego wykorzystania. Na szczęście z pomocą przychodzą konstruktory.
Konstruktor jest funkcją, która wywołana z operatorem new
pozwala na tworzenie wielu obiektów o tej samej strukturze. Utwórzmy sobie przykładowy konstruktor User
:
function User(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
Aby utworzyć obiekt, należy go zainicjalizować. Jak już pisałem, do tego celu wykorzystuje się słowo kluczowe new
:
const john = new User(1, "John", "john@doe.com");
console.log(john.name); // "John"
Jak widać w powyższym przykładzie do konstruktora przekazujemy argumenty, które wykorzystywane są do inicjalizacji obiektu. Zauważmy, że tak utworzony obiekt możemy modyfikować, przykładowo dodając do niego funkcję:
john.greets = function() {
console.log(`Hello ${this.name}`);
};
Object()
Ten dostarczany domyślnie przez JavaScript typ konstruktora był już przez nas wykorzystywany. Kiedy tworzyliśmy obiekt przy użyciu notacji literałowej, JavaScript niejawnie wywołał konstruktor Object()
w celu utworzenia nowego obiektu. Wróćmy do poprzedniego przykładu koszyka i przekonajmy się o tym:
const cart = {};
console.log(cart.constructor == Object); // true
Technicznie rzecz biorąc, utworzenie obiektu w ten sposób w zasadzie nie różni się od użycia literału obiektowego.
const cart = new Object();
cart.cartItems = [];
Prototypy obiektów
Prototypy są jednym z najciekawszych elementów w języku JavaScript. Zobaczmy co dają nam prototypy na prostym przykładzie:
function User(id, name, email) {
// własności
this.greets = function() {
console.log(`Hello ${this.name}`);
};
}
const john = new User(1, "John", "john@doe.com");
john.greets(); // Hello John
const bob = new User(2, "Bob", "bob@example.com");
bob.greets(); // Hello Bob
Problem z powyższym przykładem polega na tym, że jest to mało wydajny sposób tworzenia obiektów. Każda nowa instancja utworzona za pomocą konstruktora User
będzie posiadała własną metodę greets()
.
Bardziej efektywne będzie w tym przypadku wykorzystanie mechanizmu prototypowania:
function User(id, name, email) {
// własności
}
User.prototype.greets = function() {
console.log(`Hello ${this.name}`);
}
Jak widać do obiektu prototypu konstruktora (User.prototype
) przypisaliśmy metodę greets()
. Powyższy zapis sprawia, że wszystkie obiekty utworzone za pomocą konstruktora User()
będą miały dostęp do metody greets(). Innymi słowy prototyp jest własnością konstruktora, w którym definiujemy elementy, które mają być dziedziczone.
Object.create()
Obiekty możemy tworzyć również wykorzystując metodę Object.create()
. Ta metoda oprócz utworzenia nowego obiektu, pozwala na ustawienie prototypu tworzonego obiektu.
const bob = Object.create(john);
W powyższym przykładzie utworzyliśmy nowy obiekt bob
z obiektu john
użytego jako prototyp. W ten sposób obiekt bob
odziedziczył zarówno własności jak i metody obiektu john
. Jak widać w metodzie tej nie ma potrzeby stosowania konstruktora.
Klasy
Standard ES6, wprowadza jeszcze jeden sposób tworzenia obiektów. Są nim klasy. Zobaczmy jak wygląda przykładowa definicja:
class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
greets() {
// kod metody
}
}
Klasa User jest tak naprawdę funkcją, a sama koncepcja klas została wprowadzona w celu uproszczenia posługiwania się konstruktorami, prototypami i dziedziczeniem. Obiekty tworzy się w sposób identyczny jak w przypadku konstruktorów:
const john = new User(1, "John", "john@doe.com");
Warto zaznaczyć, że wszystkie metody zawarte w definicji klasy, są związane z jej prototypem. Dzięki temu metody klasy nie są powielane w nowo tworzonych obiektach.