Konstruktor

Ostatnia aktualizacja: 14 grudnia 2020

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 (typ obiektów), możemy 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 tworzymy za pomocą tak zwanych konstruktorów, którymi są zwykłe funkcje (z wyjątkiem funkcji strzałkowej).

W nowych wersjach JS została dodatkowo wprowadzona składnia class, która jest nakładką na klasyczną składnię konstruktorów, dzięki czemu kod jest bardziej ułożony, ale też bardziej przystępny dla programistów przychodzących z innych języków. Nowy zapis jest tak zwanym "syntactic sugar", czyli łatwiejszą składnią na ten sam mechanizm.
Wprowadzenie jego nie oznacza jednak, że nie powinniśmy poznać klasycznej metody tworzenia własnych typów. Powinniśmy, ponieważ nowa składnia nie zmienia działania mechanizmów dziejących się "po drugiej stronie lustra".

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 na bazie tej funkcji będą w przyszłości tworzone nowe obiekty.
Wszystkie właściwości i metody, które powinny znaleźć się w pojedynczych 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();

//podobnie do innych typów
const str = new String("Ala ma Konczenti");
const nr = new Number(102);
const arr = new Array(1, 2, 3);
const bool = new Boolean(true);

Prototyp

W Javascript praktycznie wszystko jest obiektem. Nasz konstruktor także. Aby to sprawdzić, wypiszmy go w konsoli za pomocą console.dir:


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

Jak zobaczysz, nasza funkcja obiekt ma kilka właściwości, które zostały automatycznie dodane przez Javascript. Są to między innymi name (nazwa funkcji), arguments (przekazane wartości), caller (funkcja, która wywołała aktualną funkcję).

Każda funkcja (poza strzałkowymi) ma też automatycznie dodaną właściwość prototype. Właściwość ta wskazuje na obiekt, który stanie się prototypem naszych obiektów, a z którego będą one mogły dziedziczyć funkcjonalności.

Funkcja prototyp

Gdy teraz utworzymy pojedynczy obiekt automatycznie dostanie on właściwość __proto__, która będzie wskazywała na ten obiekt (1).

proto to prototype

const enemy1 = new Enemy(3, 10);
console.log(enemy1.__proto__ === Enemy.prototype); //true

Tak jak już sobie o tym mówiliśmy w rozdziale o dziedziczeniu - mechanizm ten działa nie tylko dla naszych własnych typów, ale praktycznie dla każdego typu w Javascript (wyjątkiem tutaj jest null i undefined):


const tabA = [1,2,3];
const tabB = new Array(1,2,3);
tabA.__proto__ === Array.prototype //true
tabB.__proto__ === Array.prototype //true

const txt = "Ala ma kota";
const txtB = new String("Ala ma kota");
txtA.__proto__ === String.prototype //true
txtB.__proto__ === String.prototype //true

Rozbudowa prototypu

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

Prototyp 3

Prototyp jest obiektem, więc możemy go rozbudować tak samo jak to robiliśmy z innymi obiektami do tej pory.

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 tego 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} i szybkością ${this.speed}`);
}

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";
    }
}

Zastosowanie prototypu ma dwa duże atuty.

Po pierwsze bardzo ułatwia modyfikowanie wspólnych funkcjonalności danych obiektów. Dodając lub odejmując jakąś funkcjonalność do prototypu nie musisz aktualizować wcześniej utworzonych instancji.


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

const car1 = new Car("BMW");

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

Po drugie oszczędza nam 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);
}

army[0].__proto__ === Helicopter.prototype //true
army[500].__proto__ === Helicopter.prototype //true
army[999999].__proto__ === Helicopter.prototype //true
army[500].__proto__ === army[999999].__proto__ //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.firstCapital = function() {
    return this[0].toUpperCase() + this.substr(1);
}

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.firstCapital()) //wypisze Marcin

const text2 = "marcin";
console.log(text2.mixLetterSize()) //wypisze 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.

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę z tego działu, to zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania-obiekty

W repozytorium jest branch "solutions". Tam znajdziesz przykładowe rozwiązania.

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.

Menu