Konstruktor

Tak jak już sobie powiedzieliśmy, 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 wymyślono to ciut inaczej, ponieważ składni class nie było, natomiast dane typy tworzyliśmy na podstawie zwykłych funkcji, które tutaj nazwano konstruktorami. Wraz z rozwojem języka wprowadzono też inne sposoby tworzenia obiektów, w tym wspomnianą składnię class, która nie tylko uprościła nam pisanie kodu, ale też ułatwiła niektóre czynności (plus dodała kilka nowych możliwości).

W dzisiejszych czasach z pewnością będziesz używał nowszej składni. Nie oznacza to jednak, że nie warto poznać poniższej składni. 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órych w tym przypadku użyć nie możemy.

Nazwę funkcji tak samo jak nazwy klas piszemy z dużej litery - to tylko konwencja ułatwiająca nam późniejszą pracę.

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 prototyp danego obiektu. 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ą funkcję:


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);

Jak zobaczysz w konsoli funkcja także jest obiektem - w tym przypadku typu Function. Obowiązuje ją więc ta sama zasada co inne typy, czyli dostała właściwość [[prototype]], która wskazuje na prototyp wszystkich funkcji, a na który wskazuje też właściwość Function.prototype. Dzięki temu dla każdej funkcji możemy wykonać kilka dodatkowych operacji.

Poza tym nasza funkcja ma kilka innych właściwości takich jak name (nazwa funkcji), arguments (przekazane wartości), caller (funkcja, która wywołała aktualną funkcję), length (długość), oraz prototype.

Gdy zbudujemy nowy obiekt na bazie naszej funkcji, dostanie on właściwość [[Prototype]], która będzie wskazywać właśnie na tą ostatnią właściwość, która stanie się jego prototypem.

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

Prototyp jest obiektem, więc 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;
    //prototype = { ... }
}

//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"

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.

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania

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.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.