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.

Aby stworzyć grupę podobnych obiektów, możemy (ale nie musimy) posłużyć się tak zwaną klasą obiektu. Czym jest klasa? To rodzaj wzoru, blueprintu, który opisuje nam jak będą wyglądać i jak będą się zachowywać tworzone na jego podstawie nowe obiekty. Taka templatka posiada metody i właściwości, które potem dostaną poszczególne egzemplarze obiektów budowane na jej podstawie.

W Javascript taki wzór możemy stworzyć za pomocą tak zwanych konstruktorów. W nowych wersjach Javascript została dodatkowo wprowadzona składnia class, która jest nakładką (tak zwany "syntactic sugar") na poniżej omawianą składnię konstruktorów, dzięki czemu kod jest bardziej ułożony, ale też dostaje kilka dodatkowych możliwości.

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:
            życia: ${this.live}
            szybkości: ${this.speed}
            siły ataku: ${this.power}
        `);
    }
}

Konstruktor to zwykła funkcja (każda funkcja poza strzałkowymi może być konstruktorem). Jego nazwę napisaliśmy z dużej litery - to tylko konwencja mówiąca nam, że dana funkcja będzie w przyszłości używana do tworzenia nowych obiektów.

Wszystkie właściwości i metody, które powinny znaleźć się w obiektach tworzonych na bazie takiego konstruktora musimy poprzedzić słowem this.

Aby teraz utworzyć nowe obiekty na bazie takiego konstruktora skorzystamy - podobnie jak to było w przypadku typów wbudowanych - 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 podobne do już istniejących typów
const str = new String("Ala ma Kotika");
const nr = new Number(102);
const arr = new Array("Ala", "Bala");
const bool = new Boolean(true);

Prototyp

Gdy tworzysz byt jakiegoś typu, dostaje on [[Prototype]], który wskazuje na obiekt prototyp mieszczący się w konstruktorze danego typu. I tak dla przykładu gdy stworzysz zmienną typu string, jej prototyp będzie się znajdował pod zmienną prototype w konstruktorze String:


const str = "Ala ma Kotika"; //lub const str = new String("Ala ma Kotika");
Object.getPrototypeOf(str) === String.prototype //true

Ta sama zasada tyczy się też naszych własnych typów. Żeby to sprawdzić, 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:
            życia: ${this.live}
            szybkości: ${this.speed}
            siły ataku: ${this.power}
        `);
    }
}

console.dir(Enemy);

Nasza funkcja (jak wszystkie funkcje w Javascript) także jest obiektem 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 właśnie na obiekt, który stanie się prototypem obiektów danego typu.

proto to prototype

const enemy = new Enemy(3, 10);

Object.getPrototypeOf(enemy) === Enemy.prototype; //true
enemy.__proto__ === Enemy.prototype //true

Rozbudowa prototypu

Początkowo prototyp naszego typu jest praktycznie pusty, bo ma w sobie tylko 2 właściwości: constructor oraz [[Prototype]].

Prototyp 3

Jest to obiekt, więc bez problemu możemy mu dodawać nowe metody i właściwości tak samo jak to robiliśmy z innymi obiektami.

Jeżeli kiedykolwiek coś do niego dodamy stanie się to dostępne dla wszystkich instancji już stworzonych i tworzonych w przyszłości na bazie danego konstruktora.


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

//dodajemy nowe metody do prototypu
Enemy.prototype.attack = function() {
    console.log(`Atakuje 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(); //Atakuje z siłą 10
enemy1.fly(); //Lecę z szybkością 3

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

Spójrzmy jeszcze na dwa przykłady:


function Hero(name, speed, power) {
    this.name = name;
    this.speed = speed;
    this.power = power;
}

Hero.prototype.kind = "human";

Hero.prototype.fly = function() {
    return "Latam sobie w koło";
}

Hero.prototype.sayHello = function() {
    return "Nazywam się " + this.name + " i jestem superbohaterem";
}

const hero = new Hero("Songo", 10000, "Ultra Instynkt");
hero.sayHello();
hero.fly();

i kolejny przykład. Tym razem nie będę dodawał do prototypu pojedynczych metod, a ustawię pod niego cały nowy obiekt:


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

//inna metoda ustawiania prototypu
//czy lepsza? Niekoniecznie. Można zapomnieć o ustawieniu niektórych rzeczy np. właściwości - constructor
SuperHero.prototype = {
    speed : "ultra",
    strength : 90001,
    action : function() {
        return "Ratowanie świata";
    }
}

Rozbudowywanie prototypu tak jak powyżej ułatwia modyfikowanie wspólnych funkcjonalności danych obiektów. Dodając lub odejmując jakąś funkcjonalność do prototypu nie musisz ręcznie aktualizować wcześniej utworzonych instancji, ponieważ wszystkie one odwołują się do tego samego miejsca.


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

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

//rozszerzam prototyp już po stworzeniu instancji
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. Wyobraź sobie, że stworzymy funkcję attack tak jak w poniższym kodzie:


function Helicopter(name) {
    this.name = name;
    this.ammo = 2000;
    this.rockets = 16;

    this.attack = function() {
        this.ammo -= 100;
        this.rockets -= 2;

        console.log(`
            Helikopter: ${this.name} atakuje
            Pozostało amunicji: ${this.ammo}
            Pozostało rakiet: ${this.rockets}
        `);
    }
}

const army = [];
for (let i=0; i<=1000000; i++) {
    const heli = new Helicopter("Apache" + i);
    army.push(heli);
}

Jeżeli teraz stworzymy na takiej bazie 1000000 obiektów, to będziemy mieli 1000000 różnych właściwości name, 1000000 różnych właściwości ammo, rockets i uwaga - tyle samo duplikatów metody attack, która przecież za każdym razem będzie taka sama (jej kod się nie zmienia, zmienia się tylko obiekt na który wskazuje this).

Jeżeli jednak zawrzemy ją w prototypie - to ta metoda będzie występować tylko w jednym miejscu w pamięci - w obiekcie prototypu.


function Helicopter(name) {
    this.name = name;
    this.ammo = 2000;
    this.rockets = 16;
}

Helicopter.prototype.attack = function() {
    this.ammo -= 100;
    this.rocket -= 2;

    console.log(`
        Helikopter: ${this.name} atakuje
        Pozostało amunicji: ${this.ammo}
        Pozostało rakiet: ${this.rockets}
    `);
}

const army = [];
for (let i=0; i<=1000000; i++) {
    const heli = new Helicopter("Apache" + i);
    army.push(heli);
}

Object.getPrototypeOf(army[0]) === Helicopter.prototype //true
Object.getPrototypeOf(army[500]) === Helicopter.prototype //true
Object.getPrototypeOf(army[999999]) === Helicopter.prototype //true
Object.getPrototypeOf(army[500]) === Object.getPrototypeOf(army[999999]) //true

Rozszerzanie wbudowanych typów

Nie tylko naszym własnym typom obiektów 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;
}


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

const text2 = "marcin";
console.log(text2.mixLetterSize()) //MaRcIn

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.