Konstruktor

W Javascript mamy kilka typów danych. Większość z nich możemy tworzyć na dwa sposoby: korzystając z tak zwanego literału (skrócony zapis) lub na bazie tak zwanych konstruktorów:


const obA = {}
const obB = new Object();

const boolA = true;
const boolB = new Boolean(true);

const tabA = ["ala", "bala"];
const tabB = new Array("ala", "bala");

const txtA = "Ala ma Konczenti";
const txtB = new String("Ala ma Konczenti");

Każdy taki typ charakteryzuje się tym, że możemy dla niego odpalać różne właściwości i metody. Ogólnie jest więc bytem, który "zachowuje się w jakiś sposób".

My jako programiści możemy też tworzyć własne typy.

W wielu językach do tworzenia własnych typów obiektów korzystamy z tak zwanych klas (class). Klasy są formą wzoru, szablonu, która definiuje właściwości i funkcje, które trafią do obiektów tworzonych na jej podstawie.

W Javascript początkowo skorzystano z nieco innego podejścia, bo zamiast składni class możemy skorzystać z tak zwanych konstruktorów, czyli zwykłych funkcji. Wraz z rozwojem języka wprowadzono też inne sposoby, w tym wspomnianą składnię class, która nie tylko uprościła nam pisanie kodu, ale też ułatwiła nam niektóre czynności (plus dodała kilka nowych możliwości).

W dzisiejszych czasach prawdopodobnie na co dzień będziesz korzystał z nowszej składni. Nie oznacza to jednak, że nie warto poznać konstruktorów. Poniższy sposób tworzenia obiektów spotkasz nie jeden raz, a i w obu przypadkach zasada działania będzie praktycznie ta sama (z pewnymi wyjątkami). Różnice będą głównie w sposobie zapisu.

Tworzenie konstruktora

Stwórzmy przykładowy konstruktor, na bazie którego stworzymy nowe obiekty:


function Enemy(speed, power) {
    this.live = 3;
    this.speed = speed;
    this.power = power;

    this.print = function() {
        console.log(`Przeciwnik ma ${this.live} życia, ${this.speed} szybkości i ${this.power} ataku`);
    }
}

Jak widzisz konstruktor to zwykła funkcja i w zasadzie każda funkcja może stać się naszym szablonem. Wyjątkiem tutaj są funkcje strzałkowe, które nie dostają właściwości prototype (patrz dalej).

Nazwę funkcji napisaliśmy z dużej litery - to tylko konwencja ułatwiająca nam późniejszą pracę (klasy też piszemy z dużej litery).

Wszystkie właściwości i metody, jakie powinny otrzymać nowe obiekty tworzone na bazie tego konstruktora musimy poprzedzić słowem kluczowym this.

Aby teraz utworzyć nowe obiekty na bazie takiego szablonu (podobnie jak to było w przypadku innych obiektów) skorzystamy ze słowa kluczowego new:


function Enemy(speed, power) {
    ...
}

const enemy1 = new Enemy(3, 10);
enemy1.print();

const enemy2 = new Enemy(5, 15);
enemy2.print();

//co jest działaniem identycznym jak w przypadku wbudowanych typów
//z tym, że dla nich możemy też używać skróconej składni literałów
const str = new String("Ala ma Kotika");
const nr = new Number(102);
const arr = new Array("Ala", "Bala");
const bool = new Boolean(true);
const xhr = new XMLHttpRequest();

Prototyp

Gdy tworzysz pojedynczy obiekt jakiegoś typu, dostaje on właściwość [[Prototype]], która wskazuje na jego obiekt prototyp. Na ten sam obiekt wskazuje też właściwość prototype w konstruktorze danego typu. Zasada ta tyczy się każdego typu danych:


//podstawowe typy możemy tworzyć za pomocą składni new lub skrótowo - za pomocą literału
//w większości przypadków my używamy literału
const str = "Ala ma kota"; //lub const str = new String("Ala ma kota");

//pobieram prototyp danej zmiennej i sprawdzam z String.prototype
Object.getPrototypeOf(str) === String.prototype //true

const arr = [1,2,3]; //literał tablicy lub const arr = new Array(1,2,3);
Object.getPrototypeOf(arr) === Array.prototype //true

To samo dzieje się w przypadku naszych własnych typów.


function Enemy(speed, power) {
    ...
}

const enemy1 = new Enemy(3, 10);
Object.getPrototypeOf(enemy1) === Enemy.prototype //true

Żeby dokładniej zbadać powyższy przypadek, wypiszmy w konsoli za pomocą console.dir nasz konstruktor:


function Enemy(speed, power) {
    this.live = 3;
    this.speed = speed;
    this.power = power;

    this.print = function() {
        console.log(`Przeciwnik ma ${this.live} życia, ${this.speed} szybkości i ${this.power} ataku`);
    }
}

console.dir(Enemy);

Nasza funkcja także jest obiektem (obiektem o konstruktorze Function) i ma kilka właściwości, które zostały automatycznie dodane jej przez Javascript. Są to np. name (nazwa funkcji), arguments (przekazane wartości), caller (funkcja, która wywołała aktualną funkcję), length (długość) itp.

Każda funkcja (poza strzałkowymi) ma też automatycznie dodaną właściwość prototype, która wskazuje na obiekt, który stanie się prototypem obiektów tworzonych na bazie danego konstruktora.

proto to prototype

Rozbudowa prototypu

Początkowo nasz prototyp jest praktycznie pusty, bo ma w sobie tylko 2 właściwości: constructor oraz [[Prototype]]. Właściwość constructor wskazuje na funkcję na bazie której został utworzony dany obiekt. Właściwość [[Prototype]] wskazuje na prototyp tego prototypu (patrz poprzedni rozdział).

Prototyp 3

Skoro prototyp jest obiektem, to bez problemu możemy mu dodawać nowe metody i właściwości tak samo jak to robiliśmy z innymi obiektami.


function Enemy(speed, power) {
    this.live = 3;
    this.speed = speed;
    this.power = power;
}

//dodajemy nowe metody do prototypu
Enemy.prototype.attack = function() {
    console.log(`Atakuję z siłą ${this.power}`);
}

Enemy.prototype.fly = function() {
    console.log(`Lecę z szybkością ${this.speed}`);
}

//tworzę nowe obiekty
const enemy1 = new Enemy(3, 10);
enemy1.attack(); //Atakuję z siłą 10
enemy1.fly(); //Lecę z szybkością 3

const enemy2 = new Enemy(5, 15);
enemy2.attack(); //Atakuję z siłą 15
enemy2.fly(); //Lecę z szybkością 5

Dodając lub odejmując do prototypu jakąś funkcjonalność, nie musisz ręcznie aktualizować wcześniej utworzonych obiektów, ponieważ wszystkie one odwołują się do tego samego miejsca.


function Car(name) {
    this.name = name;
}

//tworzę pojedynczą instancję obiektu
const car1 = new Car("BMW");

//obiekt został już stworzony, a ja mimo to rozszerzam prototyp
Car.prototype.drive = function() {
    console.log(`${this.name} jedzie w świat`);
}

const car2 = new Car("Fiat");
car1.drive(); //"BMW jedzie w świat"
car2.drive(); //"Fiat jedzie w świat"

Dodatkowo oszczędza nam to zasoby, ponieważ wszystkie tworzone w ten sposób obiekty zamiast trzymać swoje indywidualne kopie danej funkcji, będą korzystać z pojedynczej mieszczącej się w danym prototypie.

Rozszerzanie wbudowanych typów

Nie tylko naszym własnym typom możemy modyfikować prototyp. Podobnie możemy ruszyć obiekty będące prototypami typów już wbudowanych.


String.prototype.scream = function() {
    return this.toUpperCase() + "!!!";
}

String.prototype.mixLetterSize = function() {
    let text = "";
    for (let i=0; i<this.length; i++) {
        text += (i % 2 === 0) ? this[i].toUpperCase() : this[i].toLowerCase();
    }
    return text;
}

String.prototype.countWords = function() {
    return this.split(" ").length;
}

const text1 = "marcin";
console.log(text1.scream()) //MARCIN!!!
console.log(text1.countWords()); //1

const text2 = "Marcin lubi koty";
console.log(text2.mixLetterSize()) //MaRcIn lUbI KoTy
console.log(text2.countWords()); //3

Nie jest to jednak do końca polecana praktyka (zwaną potocznie "monkey patching").

Właśnie przez takie rozbudowywanie wbudowanych typów jakiś czas temu w świecie JavaScript pojawiły się kontrowersje. Znana biblioteka MooTools rozszerzała tablice o własne metody. W pewnym momencie twórcy JavaScript chcieli wprowadzić swoje metody o takich nazwach i nagle zostali postawieni pod ścianą. Gdy wprowadzą dane metody, strony działające w oparciu o MooTools mogły by przestać działać lub działały by błędnie. Z drugiej strony gdy pójdą na kompromis okaże się, że nowo wprowadzane metody będą miały udziwnione nazwy...

Podsumowując. Własne typy rozwijaj do woli. Z rozwijaniem wbudowanych typów uważaj. Jeżeli robisz bibliotekę, która ma dogonić popularnością jQuery, raczej bym nie modyfikował domyślnych typów... Ewentualnie zrobił bym własne typy na bazie tych wbudowanych. Ale do tego raczej sięgnął bym po klasy, które omówimy w kolejnym rozdziale.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.