Odświeżenie wiadomości o obiektach

Stosowanie technik programowania obiektowego jest jednym z paradygmatów jezyka JavaScript. Bez wątpienia obiekty w JavaScripcie są podstawą. Nawet podstawowe typy danych takie jak liczby czy łańcuchy są dostępne w postaci obiektów. Nie będę opisywał korzyści płynących ze stosowania podeścia obiektowego, wspomnę jedynie, że dzięki programowaniu obiektowemu nasze aplikacje będą bardziej elastyczne i łatwiejsze w rozbudowie. Aby swobodnie korzystać z technik programowania zorientowanego obiektowo, należy doskonale posługiwać się obiektami w JavaScripcie. Dlatego w tym artykule przypomnimy sobie najważniejsze wiadomości o obiektach.

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 sobie obiekt cart z własnością cartItems. Warto zauważyć, że w stałej cart, zapisaliśmy sobie referencję do instacji koszyka. Jak widać, tworzenie obiektów przy użyciu notacji literałowej polega na tworzeniu par indeks – wartość, zamkniętych między nawiasami klamrowymi. Do własności obiektu 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 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 powtarzania 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 konstrukora przekazujemy argumenty, które wykorzysywane są do inicjalizacji obiektu. Zauważmy, że tak utworzony obiekt możemy modyfikować, przykładowo dodajmy sobie 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ł konstrutor 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 obietowego.

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, protototypami 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.