Własne obiekty

Dopiero co zaczęliśmy naszą naukę, a już wskakujemy na ciężkie tematy w Javascript.
Od razu powiem - nie musisz na chwilę obecną umieć wszystkiego z tego działu. Poniższe informacje będą nam się przewijać w dalszych działach, dlatego umieściłem ten tekst tak blisko początku. W razie czego wróć tutaj po jakimś czasie, gdy łatwiej będzie Ci rozumieć poniższe rzeczy.
W necie jest wiele fajnych materiałów na poniższe tematy. Możliwe, że ten film lepiej wyjaśni ci poniższe zagadnienia, lub np. ta strona.

Jeżeli jeszcze nie programowałeś obiektowo, to muszę Cię zmartwić - nieświadomie już to robiłeś nawet o tym nie wiedząc, ponieważ cały Javascript opiera się o obiekty.

W waszych skryptach nieraz i nie dwa odwołujemy się do obiektów. Na przykład window, Array, Math itp. to typowe obiekty, które posiadają swoje metody i właściwości:


const tab = [1,2,3];
tab.length //widzisz tą kropkę? Przez nią odwołujemy się do metod lub właściwości obiektu - w tym przypadku obiektu typu Array

const text = "Ala ma kota";
console.log(  text.charAt(0) ); //text - typ prosty został na chwilę zamieniony na obiekt (po czym od razu po wykonaniu akcji wrócił do typu prostego)

window.height - właściwość height obiektu window

Math.floor(21.3); //metoda obiektu Math

document.cookie //właściwość cookie obiektu document

(new Data()).getFullYear(); //metoda getFullYear() obiektu Date;

itp...

Poza dostępem do gotowych obiektów, możemy tworzyć też swoje własne.

Tworzenie pojedynczego obiektu

Obiekty możemy podzielić na pojedyncze instancje (pojedyncze byty), oraz grupy obiektów o podobnych właściwościach.

Na początku zajmijmy się pojedynczym obiektem.
Aby utworzyć nowy pojedynczy obiekt możemy skorzystamy z poniższej konstrukcji:


const myObj = {
    name: "Marcin",
    height: 184,
    print : function() {
        console.log("Jesteś zwycięzcą!");
    }
}

Stworzyliśmy nasz obiekt o nazwie myObj za pomocą prostej pary klamer (tak zwany literał).
Nasz obiekt posiada dwie właściwości - name i height, oraz jedną metodę (funkcję), która wypisuje jakiś tekst. Zauważ, że definiowaniu takiego obiektu właściwości rozdzielamy przecinkiem, a zamiast znaku równości stosujemy dwukropek.

Żeby teraz się do nich odwołać napiszemy:


myObj.name //właściwość name - Marcin
myObj.height //właściwość height - 184

myObj.print(); //metoda print - wypisze w konsoli "Marcin"

Nie wydaje ci się to podobne do wcześniej używanych rzeczy?


[1,2,3,4].length //właściwość length obiektu Array
Math.max(1,2,3) //metoda max() obiektu Math
window.alert("ok"); //metoda alert() obiektu window

Można powiedzieć, że nasz obiekt myObj to taka swego rodzaju tablica. To co odróżnia go od tablicy to to, że w tablicach by odwołać się do jakiejś wartości musimy wiedzieć dokładnie, na którym miejscu (indeksie) ta wartość się znajduje. W obiektach odwołujemy się po nazwie właściwości (po kluczu), więc takiego miejsca znać nie musimy.


const tab = ["Marcin", "dog", "Szymek", "marchewka", 184];
console.log(tab[0] + " - " + tab[4]);


const myObj = {
    name: "Marcin",
    pet: "dog",
    food : "Marchewka",
    favouriteHero : "Szymek",
    height: 184,
}
console.log(myObj.name + " - " + myObj.height);

Do stworzonego wcześniej obiektu możemy dodawać metody i właściwości także poza jego ciałem:


const car = {
    brand : "Mercedes",
    color : "czerwony",
    speed : 150,
    print : function() {
        console.log(car.brand + ' koloru ' + car.color );
    }
}

car.doors = 4;
car.wheels = 4;
car.drive = function() {
    console.log('Jadę!');
}

car.print();
car.drive();

this

Aby odwołać się do danego obiektu z wnętrza którejś z jego metod (czyli z wnętrza jego funkcji) możemy użyć nazwy obiektu:


const car = {
    brand : "Mercedes",
    color : "czerwony",
    speed : 150,
    print : function() {
        console.log(car.brand + ' koloru ' + car.color );
    }
}

car.print();

Nie jest to zalecana metoda, ponieważ ogranicza używanie naszego kodu.

Zamiast konkretnej nazwy obiektu, o wiele lepszym rozwiązaniem jest zastosować słowo kluczowe this, które wskazuje na obiekt, który odpalił daną metodę (czyli w większości przypadków obiekt, w którym ta funkcja jest zawarta).

Dzięki temu możemy w łatwy sposób wywoływać metody lub właściwości danego obiektu:


const car = {
    brand : "Mercedes",
    color : "czerwony",
    speed : 150,
    print : function() {
        console.log(this.brand + ' koloru ' + this.color );
    }
}

car.print();

Przy metodach dodanych z zewnątrz obowiązuje ta sama zasada:


const car = {
    brand : "Mercedes",
    color : "czerwony",
    speed : 150,
    print : function() {
        console.log(this.brand + ' koloru ' + this.color );
    }
}

car.drive = function() {
    console.log(this.brand + " sobie jadę!");
}

car.drive();

Usuwanie właściwości i metod

Aby usunąć właściwość lub metodę obiektu, skorzystamy z operatora delete:


const car = {
    brand : "Mercedes",
    color : "czerwony"
    speed : 150,
    print : function() {
        console.log(this.brand + ' koloru ' + this.color );
    }
}

console.log(car.color); //czerwony
delete car.color;
console.log(car.color); //undefined

Jak to w ogóle działa?

Zanim przejdziemy dalej, musimy sobie wyjaśnić jak w Javascript w ogóle te wszystkie obiekty działają.

Pisałem Ci już kilka razy, że w JS praktycznie wszystko jest obiektem.
Gdy używasz na przykład obiektu Math, działasz na obiekcie.
Gdy odwołujesz się do właściwości length tabeli, odwołujesz się do właściwości length obiektu tabeli (bo tabele to też obiekty). Używasz window? - używasz obiektu, który ma metody i właściwości. Date? - obiekt - który ma metody i właściwości. Pobierasz coś ze strony? Pobierasz obiekty, które mają metody i właściwości...

No dobrze. Stwórzmy więc na chwilę prosty obiekt i odpalmy na nim metody:


const user = {
    name: "Marcin",
    height: 184,

    sayName : function() {
        console.log(this.name);
    }
}

user.sayName();
user.toString();

Ale co się stało? Co się stało? Przecież nasz obiekt nie ma żadnej metody toString().
A konsola debugera nie pokazała żadnego błędu.

Zadziałał tutaj mechanizm, na którym bazują wszystkie obiekty w Javascript.

Po zbadaniu naszego obiektu w konsoli (zrób to teraz, wypisałem go w konsoli) debugera zobaczysz w nim właściwość __proto__. Jest to właściwość dodawana przez JS automatycznie każdemu obiektowi. JS za nas dodał do obiektu mniej więcej coś takiego:


const user = {
    //tu nasze rzeczy
    __proto__ = {...to jest prototyp obiektu...}
}

__proto__ to właściwość, która wkazuje na prototyp, na którym opiera się dany obiekt.
Każdy obiekt w JS zbudowany jest na bazie jakiegoś prototypu - czyli obiektu wzorca, który zawiera metody i właściwości, które dany obiekt może wykorzystywać. Nawet jak robisz pojedyńczą instancję (jeden obiekt literał - tak jak robiliśym powyżej) to jest ona połączona z jakimś obiektem wzorca za pomocą właśnie __proto__.

Jeżeli wywołujemy jakąś metodę lub właściwość danego obiektu, Javascript początkowo szuka ich bezpośrednio w instacji danego obiektu. Jeżeli ich tam nie znajdzie, za pomocą właściwości __proto__ (która istnieje w każdym obiekcie) przechodzi do prototypu obiektu - czyli obiektu, który opisuje ogólnie wygląd obiektów danego typu (np. Array, String, Date itp.). Szuka w takim obiekcie-prototypie danej metody.

Skoro prototyp jest obiektem, to także ma swoje __proto__. Jeżeli JS nie znajdzie w tym prototypie szukanej metody, korzystając z jego __proto__ dalej przechodzi do nadrzędnego prototypu i tak pnie się w górę aż dojdzie do najwyższego prototypu - grand master rodzica wszelkich prototypów - Object, na bazie którego zbudowane są wszelkie obiekty w JS. Prototyp Object właściwości __proto__ już nie ma, więc JS nie ma gdzie iść w górę.

W naszym przypadku nasz obiekt user nie ma metody toString(), ale gdy badając go w konsoli rozwiniemy jego zawartość zobaczymy, że ma __proto__. Gdy rozwiniemy i tą właściwość zobaczymy, że wskazuje on na prototyp naszego obiektu. Prototyp ten to prototyp wszelakich Obiektów - czyli prototyp Object, który już ma tą metodę (dlatego tą metodę ma każdy obiekt w JS). JS nie znalazł jej w naszym obiekcie, więc sobie wziął z "rodzica". Nasz malutki obiekt pożyczył sobie metodę od wielkiego mistrza. Wielki zaszczyt...

prototyp z debugera 1

Tworzenie obiektu za pomocą konstruktora

Powyżej za pomocą literałów tworzyliśmy pojedyncze instancje obiektów. Nie miały one żadnej wspólnej formy (w zasadzie miały - były obiektami), a były tylko jednolitymi bytami typu Object.
Dlatego właśnie właściwość o nazwie __proto__ wskazywała na ogólny prototyp obiektów.

Przypuśćmy, że zamiast pojedynczych instancji chcemy utworzyć kilka "podobnych obiektów" - np. obiekty typu User, Bullet lub np. Enemy.
Każdy obiekt takiego typu powinien posiadać jakieś właściwości i metody np. name, surname, width, height i printDetails().
Podobnie obiekty typu Array mają np. właściwość length, metodę push(), pop(), obiekty typu Date mają metodę getFullYear(), setHours() itp, a obiekty typu String() np. metodę toUpperCase() czy właściwość length.

Aby utworzyć kilka podobnych obiektów posłużymy się klasą obiektu. Czym jest klasa? To rodzaj foremki, wydmuszki, która opisuje nam jak będą wyglądać i jak będą się zachowywać tworzone na jej podstawie nowe obiekty.

W JS w przeciwieństwie do innych języków nie mamy mechanizmu klas (wprowadzono go dopiero w ES6, ale to tylko nakładka na opisywany tutaj mechanizm), ale samą klasę możemy stworzyć za pomocą funkcji - tak zwanego konstruktora:


function Car(brand, color) {
    this.age = 0;
    this.brand = brand;
    this.color = color;

    this.print = function() {
        console.log(this.brand + ' koloru ' + this.color );
    }
}

Jak widzisz konstruktor to zwykła funkcja, pisana z dużej litery. Ta duża litera to tylko konwencja nazewnicza mówiąca nam, że na bazie tej funkcji będą w przyszłości tworzone nowe obiekty i raczej nie powinniśmy tej funkcji używać do czegoś innego.

Aby teraz utworzyć nowe obiekty na bazie tego konstruktora skorzystamy ze słowa kluczowego new:


//tworzymy 2 obiekty na bazie konstruktora
const car1 = new Car("Fiat", "czerwony");
const car2 = new Car("BMW", "czarny");

car1.print(); //Fiat koloru czerwony
car2.print(); //BMW koloru czarny

Powyżej stworzyliśmy kontruktor dla obiektów własnego typu. Podobnie można też postępować z typami, które już poznałeś.
Do tej pory tworzyliśmy zmienne typu string, number, boolean, array jako literały. Każdy taki typ możemy stworzyć też za pomocą odpowiednich konstruktorów. I tak string możemy utworzyć poprzez new String(), wartości boolowskie przez new Boolean(), numery przez new Number(), a tablice przez konstruktor tablic czyli new Array().


    const txt = new String("Ala ma kota");
    const nr = new Number(23);
    const bool = new Boolean(true);
    

Żadna z tych metod nie jest zalecana przy tworzeniu zmiennych prostych typów.
Raz - wydłuża to zapis. Dwa - zwiększa obciążenie pamięci. Normalnie typy proste są proste i tylko w sytuacji użycia na nich jakiś metod czy właściwości (np. length) na chwilę są konwertowane przez JS na obiekty, a potem od razu przywracane do typów prostych. W powyższym przykładzie wszystkie zmienne non stop są obiektami. Po trzecie - używanie konstruktorów dla takich prostych typów może dodatkowo powodować nieoczekiwawe rezultaty w działaniu skryptów:


    const txt = "Ala ma kota";
    console.log(typeof txt); //"string";

    const txt2 = new String("Ala ma kota");
    console.log(typeof txt2); //"object";


    const bool = true;
    console.log(typeof bool); //"boolean";

    const bool2 = new Boolean(true);
    console.log(typeof bool2); //"object";
    

Zamiast tych konstruktorów wybieraj klasyczne skrócone deklaracje (które używałeś do tej pory).

Prototyp

Spójrz powyżej na kod naszego konstruktora Car. Stworzyliśmy konstruktor i za pomocą słowa kluczowego new na jego bazie stworzyliśmy 2 samochody.

Słowo kluczowe new jest bardzo ważnym słowem, które gdy zostanie użyte robi kilka ważnych rzeczy.

Zanim zrozumiemy jego wagę, wypiszmy na chwilę w konsoli wcześniej utworzony konstruktor:


function Car(brand, color) {
    this.age = 0;
    this.brand = brand;
    this.color = color;

    this.print = function() {
        console.log(this.brand + ' koloru ' + this.color );
    }
}

console.dir(Car);

Funkcja prorotyp

Jak widzisz, nasza funkcja-konstruktor ma właściwość prototype. Właściwość ta wskazuje na obiekt, który stanie się w przyszłości prototypem obiektów tworzonych na bazie naszego konstruktora.

I teraz właśnie wkracza na ring słowo kluczowe new. Jeżeli za jego pomocą utworzymy nowy obiekt, JS po pierwsze ustawi takiemu obiektowi prototyp biorąc go właśnie z właściwości prototype naszego konstruktora (właśnie na ten obiekt będą wskazywać __proto__ nowych obiektów)


function Car(brand, color) {
    this.age = 0;
    this.brand = brand;
    this.color = color;

    this.print = function() {
        console.log(this.brand + ' koloru ' + this.color );
    }
}

const car1 = Car("Fiat", "czerwony"); //brakuje słowa new
console.log(car1.__proto__ === Car.prototype) //true

a dodatkowo - co równie ważne - zmieni kontekst słowa this, które od tego momentu będzie wskazywać na dany obiekt, a nie na obiekt globalny window.


function Car(brand, color) {
    this.age = 0;
    this.brand = brand;
    this.color = color;

    this.print = function() {
        console.log(this.brand + ' koloru ' + this.color );
    }
}

const car1 = Car("Fiat", "czerwony"); //brakuje słowa new
car1.brand; //Cannot read property 'brand' of undefined

const car2 = new Car("BMW", "czarny"); //tutaj już jest new
car2.brand; //BMW

Powyższy przykład pokazuje jak bardzo ważne jest używanie słowa kluczowego new do tworzenia obietków na bazie danego konstruktora.

Jeżeli ktoś wciąż zapomina o użyciu new, zawsze może skorzystać z pewnej sztuczki polegającej na skorzystaniu z instanceof, która sprawdza czt dany obiekt jest instancją zbudowaną na jakimś konstruktorze:


    const Car = function(brand, color) {
        if (!(this instanceof Car)) {
            return new Car(brand, color);
        }
        this.age = 0;
        this.brand = brand;
        this.color = color;
        this.print = function() {
            console.log(this.brand + ' koloru ' + this.color );
        }
    }

    const car1 = Car("Fiat", "czerwony"); //brakuje słowa new
    ob.brand; //Maluch
    

Gdy teraz zbadamy nasze nowe obiekty w konsoli, pokaże się coś takiego:

prototyp z debugera 2

Jak widzisz nasz nowy obiekt ma właściwości brand, color, metodę print oraz właściwość __proto__, która wskazuje na powyżej opisany prototyp brany z konstruktora.

Początkowo ten prototyp jest praktycznie pusty, bo ma w sobie tylko 2 właściwości:
constructor, który wskazuje na funkcję na bazie której został stworzony nasz obiekt (czyli nasz konstruktor), oraz ... __proto__, który wskazuje na prototyp "nadrzędny" (w naszym przypadku będzie to już prototyp Object).

Prototyp 3

Po co nam te informacje? Ano po to, że my w dowolnej chwili taki prototyp możemy zmieniać.

Co nam to daje? Zanim to wyjaśnimy, mini powtórka z poprzednich działów.

Wracamy do początkowej lekcji. W Javascript dane dzielą się na dwa typy - proste i referencyjne. Pamiętasz czym się one charakteryzują?


let a = 2;
let b = a;
a = 4; //zmieniam tylko a
console.log(a, b); //4, 2 - czyli zupełnie jak na matmie


let obA = {a : 2};
let obB = obA;
obA.a = 4; //zmieniam tylko obA
console.log(obA.a, obB.a); //4, 4

Typy proste trzymają w sobie dane bezpośrednio (można powiedzieć, że są to pojedyncze kopie danych). Typy referencyjne (obiekty) wskazują miejsce w pamięci gdzie dany obiekt się mieści.
Oznacza to, że jeżeli dwie i więcej zmiennych wskazuje na dany obiekt, to wszystkie one są "tym samym" i wskazują ten sam obiekt - jeżeli w jednej zmiennej usuniemy lub dodamy temu obiektowi jakąś właściwość lub metodę, to zrobimy to we wszystkich zmiennych, które wskazują na ten obiekt.

Wszystkie obiekty stworzone na bazie danego konstruktora za pomocą właściwości __proto__ wskazują na ten sam prototyp.
Jeżeli kiedykolwiek zmienimy w tym prototypie jakąś metodę, zmieni się ona dla wszystkich obiektów już stworzonych i tworzonych w przyszłości na bazie tego konstruktora.


function Car(brand, color) {
    this.brand = brand;
    this.color = color;
    //nie ma tutaj już metody print
}

//tworzę nowe obiekty
const car1 = new Car("Fiat", "czerwony");
const car2 = new Car("BMW", "czarny");

//dodajemy nowe właściwości i metody
Car.prototype.print = function() {
    console.log(this.brand + ' koloru ' + this.color);
}

car1.print(); //Fiat koloru czerwony
car2.print(); //BMW koloru czarny

Zauważ jak w powyższym przykładzie dodawaliśmy kolejne metody i właściwości do prototypu - tak samo jak w przypadku obiektów literałów, które omawialiśmy na początku rozdziału. W końcu prototyp to obiekt. Spójrzmy jeszcze na dwa przykłady:


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

//wlaściwości i metody
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 super bohaterem";
}
Hero.prototype.saySomething = function() {
    var greeting = this.sayHello();
    return greeting + " a pozatym lubię walczyć z nieprawością...";
}

const hero = new Hero("Superman", 30, "Ultra Instynkt");
hero.saySomething();

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? Nie koniecznie. Zapomieliśmy o jednej właściwości - constructor
SuperHero.prototype = {
    speed : "uber",
    strength : "big",
    action : function() {
        return "Ratowanie świata";
    }
}

Bardzo ważna rzecz wynikająca z zastosowania prototypów to także oszczędność zasobów. Wyobraź sobie, że tworzymy obiekt w taki sposób:


function Human(name, surname) {
    this.name = name;
    this.surname = surname;
    this.print = function() {
        return this.name + " " + this.surname;
    }
}

const humans = [];
for (let i=0; i<=1000000; i++) {
    const user = new Human("Marcin" + i, "Nowak" + i);
    users.push(user);
}

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 surname i uwaga - tyle samo kopii metody print, 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 - tym samym obiekcie dla wszystkich instancji naszych obiektów - to ta metoda będzie występować tylko w jednym miejscu - w obiekcie prototypu.


function Human(name, surname) {
    this.name = name;
    this.surname = surname;
}

Human.prototype.print = function() { //ta sama metoda
    return this.name + " " + this.surname;
}

const users = [];
for (let i=0; i<=1000000; i++) {
    const user = new Human("Marcin" + i, "Nowak" + i); //a inne właściwości obiektu
    users.push(user);
}

Pobierzmy powyższy prototyp korzystając z metody Object.getPrototypeOf(obj) i sprawdźmy:


Object.getPrototypeOf(users[0]) === Object.getPrototypeOf(users[1000000]); //true
Object.getPrototypeOf(users[0]).print === Object.getPrototypeOf(users[1000000]).print; //true

Mega siatka

Z całego powyższego opisu wyłania się nam struktura obiektów w JS, które stanowią rodzaj powiązanej pajęczej sieci.

Na samej górze jest grand master - prototyp wszystkich obiektów. Wszelkie obiekty prędzej czy później na niego wskazują za pomocą __proto__.

Jeżeli dane instancje obiektów są tylko pojedynczymi instancjami (nie zostały stworzone na bazie żadnego konstruktora typu Array, Data(), naszego konstruktora itp.), to ich __proto__ wskazuje na prototyp Object.

Jeżeli nasze obiekty zostały stworzone na bazie konstruktora, to wszystkie one wskazują na wspólny obiekt prototypu (Array, Data, NaszKonstruktor), w którym (bo wszystkie obiekty tak mają) właściwość o nazwie __proto__ wskazuje na wyższy prototyp. I tak w koło aż do "Wielkiego Żółwia"... znaczy się prototypu Object.

Mega siatka połączeń

O prototypie ciut więcej

Pozostańmy jeszcze na chwilę w temacie prototypu.

Jeżeli dodamy do siebie różne typy danych, czasami wychodzą nam dziwne rzeczy:


const txt = "ala";
const nr = 99;
const tab = [1,2,3];
const ob = {name : "Marcin"};

console.log(txt + nr); //ala99
console.log(tab + nr); //1,2,399
console.log(tab + ob); //1,2,3[object Object]
console.log(ob + txt); //[object Object]ala

Dodając do siebie różne typy, JS spradza czy powinien zamienić daną zmienną na string. Jeżeli taka konwersja jest konieczna, robi to. I tak dodając do stringa liczbę, uzyskujemy nowy string.

Ciekawa rzecz dzieje się gdy konwersji na string są poddawane tablice i obiekty. Tablica zamienia się na kolejne wartości oddzielone przecinkiem, a obiekt na tekst "[objetc Object]" - co zresztą widać na powyższym listingu.

Czemu tak się dzieje? Do konwersji JS wykorzystuje metodę toString(). No ale przecież przy obiektach i tablicach mamy inny zapis. Wynika z tego, że są używane inne wersje metody toString(). Sprawdźmy to:


const tab = [1,2,3];
console.dir(tab);


const ob = {name : "Marcin"};
console.dir(ob);

Tablice są obiektami zbudowanymi na bazie konstruktora Array. Ich właściwości __proto__ wkazują na prototyp w konstrutorze Array, który ma w sobie metodę toString - specjalną jej wersję przystosowaną właśnie dla tablic.

Właściwość __proto__ obiektu wskazuje na prototyp wszystkich obiektów. On też ma implementację metody toString - tym razem dla obiektów wszelakich.

Javascript szuka omawianej metody w instancji. Nie znajduje jej tam. Przechodzi wyżej. W przypadku tablic trafia do prototypu tablic i tam znajduje tą metodę. Odpala ją. W przypadku obiektów znajduje ją dopiero w konstruktorze obiektów i dopiero tamtą wersję odpala.

Idąc za ciosem, gdyby nasze obiekty miały własną implementację tej metody, powinna być odpalana właśnie ta implementacja:


function Human(name, surname) {
    this.name = name;
    this.surname = surname;
}
Human.prototype.toString = function() {
    return "[obiekt człowiek]";
}

const h = new Human("Marcin", "Nowak");

console.log("ala" + h); //ala[obiekt człowiek]

Nawet jeżeli nasz typ obiektów ma swoją metodę to string, każda z instancji może mieć swoją własną wersję:


function Human(name, surname) {
    this.name = name;
    this.surname = surname;
    //gdyby metoda była tutaj, odpalana była by pierwsza
}
Human.prototype.toString = function() {
    return "[obiekt człowiek]";
}

const h1 = new Human("Marcin", "Nowak");
console.log("ala" + h1); //ala[obiekt człowiek]


const h2 = new Human("Karol", "Kowalski");
h2.toString = function() {
    return "[obiekt" + this.name + " ]";
}
console.log("ala" + h2); //ala[obiekt Karol]

Rozważania ciąg dalszy.
Skoro funkcja konstruktor ma właściwość prototype, która jest obiektem, a potem na bazie tego kontruktora są tworzone obiekty, które mają właścwiości __proto__ wskazujące na ten prototyp, to spełnione jest poniższe równianie:


user.__proto__ === Human.prototype //true

Co oznacza, że w teorii powinniśmy móc zrobić tak jak poniżej:


const user = new Human("Marcin", "Nowak");

user.__proto__.angry = function() {
    return this.name + " jest agresywny";
}

const user2 = new Human("Karol", "Kowalski");
user2.angry(); //Karol jest agresywny

I rzeczywiście - powyższy kod ustawi nowe rzeczy w prototypie konstruktora, a więc i we wszystkich obiektach zbudowanych na jego bazie. Nie jest to jednak zalecana metoda, ponieważ jest wolna. Zamiast niej używaj wcześniej pokazananej metody działania na Car.prototype do którego wcześniej ustawiajmy odpowiednie składowe

Właściwość __proto__ jest ogólnie trochę kontrowersyjną właściwością. Mimo tego, że nie występuje w specyfikacji większość przeglądarek i tak zaimplementowało tą właściwość. Dla zapewnienia kompatybilności z przeglądarkami została wprowadzana do specyfikacji w EcmaScript 2015. Jest kilkanaście takich momentów w dziejach JS i CSS, gdzie ewolucja poszła inną drogą niż chcieli autorzy (np. media, html outline itp). W oryginalnej specyfikacji właściwość __proto__ widnieje pod nazwą [[Prototype]].
W dzisiejszych czasach najbezpieczniej pobierać ją za pomocą metody Object.getPrototypeOf() zamiast bezpośrednio odwoływać się do __proto__

Rozszerzanie wbudowanych obiektów

Nie tylko nasze własne konstruktory możemy modyfikować. Nic nie stoi na przeszkodzie by poruszyć prototypy konstruktorów typów już dostępnych.

Możemy więc pokusić się dla przykładu o rozbudowanie stringów:


String.prototype.firstCapital = function() {
    return this.charAt(0).toUpperCase() + this.substr(1);
}

String.prototype.mixLetterSize = function() {
    let text = '';
    for (let x=0; x<this.length; x++) {
        if (x % 2 === 0) {
            text += this.charAt(x).toUpperCase();
        } else {
            text += this.charAt(x).toLowerCase();
        }
    }

    return text;
}



const text1 = "marcinek";
console.log(text1.firstCapital()) //wypisze Marcinek

const text2 = "marcinek";
console.log(text2.mixLetterSize()) //wypisze mArCiNeK

W ramach testu moglibyśmy też pokusić się o zmianę działania tablic. Co by się stało, gdyby w ich prototypie usunąć metodę toString()? Czy JS przejdzie do wyższego prorotypu i użyje metody z prototypu wszystkich obiektów? Sprawdźmy.


console.log("ala" + [1,2,3]) //ala1,2,3

delete Array.prototype.toString;

console.log("ala" + [1,2,3]) //ala[object Array]

Rzeczywiście tak się stało

Pamiętaj też o tym, że nic nie stoi na przeszkodzie by dodawać własne metody nie tylko do prototypów, ale także do pojedynczych instancji które na co dzień używamy:

W poniższym przykładzie rozszerzam obiekt Math:


Math.randomBetween = function(min, max) {
    return Math.floor(Math.random()*(max-min+1)+min);
}

console.log(Math.randomBetween(10, 20));
console.log(Math.randomBetween(100, 200));

Czy obiekt console:


console.ourLog = function(msg) {
    let counter = 0;

    return function(msg) {
        counter++;

        //console.log umożliwia zastosowanie znaków formatujących tekst
        //%c - daje możliwość formatowania tekstu
        //%s - wstawia w to miejsce tekst przekazany w późniejszych parametrach
        console.log('%c %s', 'display:block; font: bold 1rem sans-serif; color:tomato;', counter + '. ' + msg)
    }
}

const msg = console.ourLog();
msg('Przykladowa wiadomość'); //1. Przykładowa wiadomość
msg('Inna wiadomość'); //2. Inna wiadomość
Powyższe zabawy nie są do końca polecaną praktyką (zwaną potocznie "monkey patching"). Działania takie mogą zmylić programistów, którzy przyzwyczajeni są do działania danych obiektów.
Wyobraź sobie, że na co dzień bardzo dużo działasz z elementami typu String. I nagle pewnego dnia badasz taki obiekt i się okazuje, że podochodziło do niego mnóstwo nowych metod. Jak to mawiał klasyk - A po co to? A komu to potrzebne? I następuje zdziwienie i konsternacja. Dodatkowo okazuje się, że część metod została nadpisana i działa inaczej niż zwykle. Jasne - można rozwijać obiekty bazowe, ale miej na uwadze, że czasami niektórzy mogą być zmieszani.

Swoją drogą właśnie przez takie zabawy jakiś czas temu w świecie JS pojawiły się kontrowersje. Znana biblioteka MooTools rozszerzała wbudowane obiekty o własne metody. W pewnym momencie twórcy JS chcieli wprowadzić swoje metody o podobnych nazwach i nagle zostali postawieni pod ścianą. Gdy wprowadzą dane metody, strony działające na MooTools mogły by przestać działać lub działały by błędnie. Z drugiej strony przez jedną bibliotekę nie mogą prawidłowo rozwijać języka. Bywa różnie...

Podsumowując ten wywód. 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ł natywnych obiektów... Ewentualnie zrobił bym własne typy na bazie tych wbudowanych. Ale o tym ciut poniżej.

Kontekst this

Powyżej napisałem, że this w metodzie obiektu wskazuje na obiekt, do którego należy ta metoda. Jest to dość luźne stwierdzenie i zaraz zobaczysz, że nie do końca prawdzie.
Ogólnie jednak można powiedzieć, że jeżeli jakiś obiekt odpala daną metodę, to this w tej metodzie wskazuje na ten obiekt:


Math.max(...) //this w metodzie max() wskazuje na Math
myObj.pisz() //this w metodzie pisz() wskazuje myObj
"Ala".toUpperCase() //this w toUpperCase() wskazuje na "Ala"
[1,2].push(3) //this w push() wskazuje na tablicę

Podobnie jest z eventami. Jeżeli daną funkcję wywoła jakiś obiekt/element, wtedy w jej wnętrzu this będzie wskazywać na ten obiekt/element:


const btn = document.querySelector('.btn');
btn.addEventListener('click', function() {
    console.log(this); //btn
});

Wiedząc to przechodzimy do trudniejszego przykładu:


const MyObject = function() {
    this.button = null;
    this.pets = ["kot", "pies", "żona"]

    this.bindBtn = function() {
        this.button = document.createElement('button');
        this.button.innerText = 'Kliknij';
        this.button.type = 'button';

        this.button.addEventListener('click', function() {
            //this === button
            console.log(this.innerText); //Kliknij
            console.log(this.pets); //??????
        });

        document.querySelector('body').appendChild(this.button);
    }
}

const obj = new MyObject('Marcin', 183);
obj.bindBtn();

Po wywołaniu metody bindBtn() tworzymy nowy guzik i podpinamy mu event click.
Po jego kliknięciu powinien on wypisać tablicę pets z obiektu MyObject. Jak jednak to zrobić, skoro instrukcja this wewnątrz eventu button wskazuje na niego samego?

Są na to dwa sposoby:
posłużenie się dodatkową zmienną that:


const MyObject = function() {
    this.button = null;
    this.pets = ["kot", "pies", "żona"]

    this.bindBtn = function() {
        const that = this;

        this.button = document.createElement('button');
        this.button.innerText = 'Kliknij';
        this.button.type = 'button';

        this.button.addEventListener('click', function() {
            console.log(this.innerText); //Kliknij
            console.log(that.pets);
        });

        document.querySelector('body').appendChild(this.button);
    }
}

const obj = new MyObject();
obj.bindBtn();

lub skorzystanie z instrukcji bind(newThis), za pomocą której możemy przekazać nowy kontekst dla this, które jest w danej funkcji:


...

    this.button.addEventListener('click', function(e) {
        //tutaj już this wskazuje na MyObject
        //dlatego dany przycisk musimy pobrac za pomocą e.target
        console.log('To jest ' + e.target.nodeName); //button

        console.log(this.pets);
    }.bind(this));

...

Część programistów JS neguje stosowanie dodatkowej zmiennej do przekazywania kontekstu obiektu.
Pamiętaj, że stosując instrukcję bind() this w takiej funkcji już nie wskazuje na dany element, więc aby odwołać się do elementu, który wywołał event (ten który został np kliknięty), musimy korzystać z e.target

Zobaczmy powyższy problem jeszcze na jednym przykładzie:


const ob = {
    name : "Marcin",
    print : function() {
        console.log(this.name);
    }
}

ob.print(); //Marcin

document.querySelector('#btn').addEventListener('click', ob.print);

Po kliknięciu w przycisk #btn this w wywołanej funkcji wskaże na ten przycisk, który nie ma funkcji print. Znowu - musimy zmienić to this na właściwy obiekt:


const ob = {
    name : "Marcin",
    print : function() {
        console.log(this.name);
    }
}

ob.print(); //Marcin

document.querySelector('#btn').addEventListener('click', ob.print().bind(ob));

W wersji EcmaScript 2015 istnieje jeszcze jedno rozwiązanie powyższego problemu.
Zwie się ono funkcją strzałkową, która poza krótszym zapisem, zmienia kontekst this na zewnętrzny.
Jeżeli więc w powyższym kodzie podepniemy zdarzenie za pomocą funkcji strzałkowej, we wnętrzu takiego zdarzenia this będzie wskazywać na zewnętrzny kontekst, czyli na nasz obiekt:


...

    //poniżej użyłem funkcji strzałkowej z ES6
    this.button.addEventListener('click', e => {
        console.log('To jest ' + e.target.nodeName);
        console.log( this.pets );
    });

...

Funkcję strzałkową poznasz w dziale omawiającym ES6.

Dziedziczenie

Jak już widziałeś powyżej, w JS dziedziczenie opiera się o prototypy.
Jeżeli dana instancja obiektu nie ma metody lub właściwości, JS szuka go w jego prototypie, na który wskazuje właściwość __proto__. Jeżeli jej tam nie znajdzie przechodzi do kolejnego prototypu, i tak w koło do czasu, aż znajdzie szukaną metodę, lub dojdzie do najwyższego prototypu obiektów wszelakich.

No tak - dziedziczenie. A co to właściwie jest?

Gdybym zapytał ciebie czym jest dziedziczenie w realnym świecie co byś powiedział?
Dzieci dziedziczą po rodzicach pewne cechy. Może to kolor włosów, może kolor oczu, a może talent do rysowania. Część takich właściwości sobie pobierają od rodziców, ale i też mają swoje własne. Tata był przystojny. Syn też jest. Ale syn jest o wiele wyższy od ojca. Córka po mamie odziedziczyła spojrzenie, ale dodała swoje wyjątkowe różowe poliki...
Niektóre właściwości też nadpisują, bo na przykład włosy są niby rude, ale jakieś takie nie za bardzo.
Co ważne w drugą stronę to nie działa. Rodzice nigdy nie dziedziczą po dzieciach.

Wracamy do Javascript. Wyobraź sobie, że masz ogólny abstrakcyjny konstruktor Animal:


function Animal(name, age) {
    this.age = age;
    this.name = name;
    this.type = 'animal';
}

Animal.prototype.eat = function() {
    return this.name + " właśnie je";
}

Jak widzisz nie za wiele on ma. Ani nie definiuje konkretnego typu zwierzęcia, ani co takie zwierze je, jakie wydaje dźwięki itp.
Teraz chciałbyś stworzyć nowy typ obiektów - np "Psa", który miałby powyższe właściwości i metodę, ale równocześnie dodawał by coś swojego:


function Dog(name) {
    this.name = name;
    this.type = "dog";
}
Dog.prototype.bark = function() {
    return "Wof! Wof!";
}

const dog = new Dog("dog");
console.log(dog.bark()); //Wof! Wof!
console.log(dog.eat()); //błąd - nie ma takiej metody

No tak, błąd. Nasz obiekt Dog nie ma metody eat(), a __proto__ wskazuje na prototyp Object, który też takiej metody nie ma. Jak więc zrobić, by __proto__ konstruktora Dog wskazywało na konstruktor Animal?
Skoro obiekty Animal mają swój prototyp, który definiuje ich wygląd, wystarczyło by, że dla Dog ustawilibyśmy ten sam prototyp:


function Dog(name) {
    this.name = name;
    this.type = "dog";
}

Dog.prototype = Animal.prototype;

Dog.prototype.bark = function() {
    return "Wof! Wof!";
}

const dog = new Dog("dog");
console.log(dog.bark()); //Wof! Wof!
console.log(dog.eat()); //"dog właśnie je";

Problem tutaj jest taki, że skoro zrównaliśmy do siebie obydwa prototypy, stały się one tym samym obiektem.
Jeżeli dla Dog dodaliśmy teraz metodę bark(), trafiła by ona też do obiektów tworzonych na bazie Animal, a przecież nie wszystkie zwierzęta szczekają.


const horse = new Animal("Rafał");
horse.bark(); //Wof! Wof!

Rozwiązaniem nie jest zrównywanie prototypów, a stworzenie nowego obiektu prototypu na bazie innego innego prototypu.
Wykorzystuje się do tego metodę Object.create(prototyp). Tworzy ona nowy obiekt na bazie jakiegoś prototypu.
Jeżeli teraz my stworzymy nowy obiekt (nowy prototyp) na bazie już istniejącego, to ten nowy prototyp będzie miał wszystkie metody i właściwości ze starego prototypu, ale już nie będzie tym samym prototypem:


function Dog(name) {
    this.name = name;
    this.type = "dog";
}

Dog.prototype = Object.create(Animal.prototype);

Dog.prototype.bark = function() {
    return "Wof! Wof!";
}

const dog = new Dog("dog");
console.log(dog.bark()); //Wof! Wof!
console.log(dog.eat()); //"dog właśnie je";

const animal = new Animal('Pingwin');
animal.bark(); //błąd, bo Animal nie ma metody bark()

Wszystko działa praktycznie idealnie, ale została malutka rzecz. Pamiętasz co było w naszych nowych prawie pustych prototypach? Dwie rzeczy: __proto__ i constructor. Konstruktor wskazywał na funkcję na bazie której są tworzone obiekty.
W powyższym kodzie stworzyliśmy prototyp Dog na bazie prototypu Animal. Tym samym wzięliśmy tamten konstruktor, który wskazuje na Animal. Dla Dog powinien to być Dog. Naprawmy to:


function Dog(name) {
    this.name = name;
    this.type = "dog";
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

...
...

I to wszystko. Nasz typ obiektów Dog dziedziczy po obiektach typu Animal. A Animal już po samym Object.

Klasa abstrakcyjna

W powyższych przykładach stworzyliśmy klasę Animal i podklasę Dog. Następnie tworzyliśmy obiekty na ich bazie.

Nie zawsze będzie to dobre podejście.
Wyobraź sobie, że tworzysz główną klasę (konstruktor) Bullet.
W klasie tej tworzysz właściwości takie jak speed, power, kierunek ruchu, jak taki pocisk ma się zachowywać gdy opuści planszę, może typ grafiki dla takiego pocisku.

Tworząc instancję takiego obiektu musisz ustawić ją w odpowiednim miejscu - w wylocie lufy karabinu postaci. Dodatkowo musisz ustawić takiemu pociskowi kierunek ruchu i zapewne odpalić poruszanie się.

Następnie tworzysz kilka klas dziedziczących. Dla przykładu BulletFast, BulletBig, BulletNormal.

Każda taka klasa dziedziczy po głównej klasie Bullet podstawowe właściwości.
Poza dziedziczeniem chciałbyś, by każda z tych klas brała sobie te początkowe ustawienia zmiennych, ewentualnie dodawała coś od siebie lub zmieniała (np. speed, power, grafikę, może kierunek ruchu).

Przykład pocisków w grze

W naszej grze nie chcielibyśmy tworzyć instancji nieokreslonej klasy Bullet (bo np. nie ma wyglądu), a tylko klasy pochodne - BulletFast, BulletBig, BulletNormal. Taka główna klasa - foremka dla klas pochodnych - zwie się klasą abstrakcyjną. Nie ma ona żadnych specyficznych cech, a tylko ogólne - będące formą dla podrzędnych klas.
Nie będziemy chcieli tworzyć instancji na bazie takiej klasy. Możemy się przed tym zabezpieczyć np. spradzając konstruktor:


const Bullet = function() {
    if (this.constructor === Bullet) {
        throw new Error("Nie możesz tworzyć obiektów z klasy abstrakcyjnej!");
    }
    ...
}

Odwoływanie się do obiektu ojca

Wracamy do klasy Animal i Dog.
Zauważ, że konstruktor Dog ustawia dla obiektów budowanych na jego bazie this.type i this.name.
W konstruktorze Animal natomiast tych właściwości ustawianych jest ciut więcej.
Podczas dziedziczenia chcielibyśmy by Dog wziął sobie ustawianie zmiennych z Animal:


...
this.type = 'animal';
this.name = name;
this.age = age;

A następnie tylko ustawił swoje specyficzne właściwości lub nadpisał to co powyżej:


//to z Animal
this.type = 'animal';
this.name = name;
this.age = age;

//to by Dog sobie dodał i ewentualnie nadpisał niektóre
this.type = "dog";
this.name = name;

Jak to zrobić? Wystarczyłoby w naszym konstruktorze Dog odpalić kod tamtej funkcji Animal (bo przecież konstruktor to zwykła funkcja), zmieniając jej tylko odpowiednio this by wskazywało na Dog...


function Dog(name) {
    //hej - tutaj chce kod z konstruktora Animal

    this.name = name;
    this.type = "dog";
}

Call i apply

Użyjemy do tego metody funkcja.call(this, parametr1, parametr2)

Metoda call, która jest dostępna dla każdej funkcji (zgadnij gdzie się znajduje?) w Javascript służy do wołania danej funkcji.

Powiedzmy, że mamy prostą funkcję:


function myFunc() {
    console.log('Jestem funkcją');
}

Żeby ją teraz wywołać możemy napisać:


myFunc();

//lub

myFunc.call();

Poza wołaniem o funkcję, metoda call daje nam jedną bardzo ważną funkcjonalność.

W pierwszym jej parametrze możemy przekazać obiekt, jaki zostanie podstawiony pod this danej funkcji:


const ob = {
    name : "Marcin",
    print : function() {
        console.log("Mam na imię " + this.name);
    }
}

ob.print(); //Mam na imię Marcin



const ob2 = {
    name : "Włodzimierz"
}
ob.print.call(ob2); //Mam na imię Włodzimierz


const ob3 = {
    name : "Patryk"
}
ob.print.call(ob3); //Mam na imię Patryk

Widzisz jak pożyczyliśmy sobie funkcję z innego obiektu?

Jeżeli ta funkcja wymagała by jakiś parametrów, możemy je podać jako kolejne parametry call. Jeżeli wiemy, że w funkcji nie jest obsługiwane this, wtedy jako pierwszy parametr możemy przekazać null, undefined, lub inną rzecz - nie ma to znaczenia, bo i tak nie będzie obsługiwane w danej funkcji.


const ob = {
    name : "Marcin",
    print : function(friend1, friend2) {
        console.log("Mam na imię " + this.name);
        console.log("Moi przyjaciele to: " + friend1 + " i " + friend2);
    }
    sayHiToDog : function(pet) {
        console.log("Cześć " + pet + '!') ;
    }
}

ob.print("Karol", "Magda"); //Mam na imię Marcin. Moi przyjaciele to Karol i Magda



const ob2 = {
    name : "Włodzimierz"
}



ob.print.call(ob2, "Monika", "Piotrek"); //Mam na imię Włodzimierz. Moi przyjaciele to Monika i Piotrek
ob.sayHiToDog.call(undefined, "Smyk"); //Cześć Smyk!

Pamiętasz jak w rozdziale o DOM pokazałem Ci technikę, która umożliwia zastosowanie forEach dla kolekcji elementów? Mówiłem ci wtedy, że normalnie dla kolekcji taka metoda nie istnieje (nie istnieje w momencie gdy piszę te słowa, bo taki Chrome w nowych wersjach już ją wprowadził dla kolekcji).
Przypomnijmy ten trik:


const divs = document.querySelectorAll('div.module');

[].forEach.call(divs, function(el) {
    //działamy na elementach
    el.style.color = "red";
});

Czyli tłumacząc: zawołaj z Obiektu typu Array metodę forEach, przekazując jej pod this kolekcję divów które właśnie pobrałeś.

Metoda forEach normalnie działa tak:


const tab = [1,2,3,4];
tab.forEach(function(el) {
    console.log(el); //wypisze kolejno 1, 2, 3, 4
})

Czyli właściwością this jest dana tablica po której forEach iteruje, a pierwszym parametrem jest funkcja zwrotna, do której przekazane zostaną kolejne elementy tablicy.

Porównajmy to więc do powyższej sztuczki.
Pod this trafiły divy, a pierwszym parametrem stała się funkcja, do której trafią kolejne składowe divs - czyli kolejne divy.

W powyższej funkcji forEach w funkcji callback, którą przekazaliśmy jako parametr forEach this nie będzie wskazywało na tablicę, a na window. Dzieje się tak przez sposób implementacji działania forEach. Gdybyśmy rozpisali kod forEach wyszło by, że forEach robi po this (które jest tablicą) pętlę i dla każdego elementu wywołuje funkcję callback (tę samą którą my przekazujemy) przekazując jej dany element this Przykładowa implementacja funkcji forEach wyglądała by mniej więcej tak:

        Array.forEach = function(fn) {
            for (let i=0; i<this.length; i++) {
                fn(this[i], i, this); //normalnie do forEach przekazujemy funkcję do której trafi dany element tablicy, licznik i tablica
            }
        }
    

Podobną sztuczkę co powyżej możemy też zastosować do właściwości arguments, która przypomina tablicę, ale nią nie jest:


function myF() {
    //argument nie jest tablicą więc forEach nie zadziała, ale...
    [].forEach.call(argument, function(el) {
        console.log(el)
    })
}

Bardzo podobna w działaniu do call jest metoda Funkcja.apply(this, []). Różni ją od call to, że poza obiektem dla this przyjmuje tylko jeden atrybut - będący tablicą, która zawiera w sobie parametry. Po wywołaniu funkcji za pomocą apply, składowe tej tablicy są przekazywane jako kolejne parametry danej funkcji:


const max1 = Math.max(1,2,3,4,5,2,4); //5

const max2 = Math.max.call(Math, 1,2,3,4,5,2,4);
const max3 = Math.max.apply(Math, [1,2,3,4,5,2,4]);

function multiply(a, b) {
    console.log(a * b);
}

const mult1 = multiply(2,3);
const mult2 = Multiply.call(undefined, 2, 3);
const mult3 = multiply.apply(null, [2, 3]);

Co kiedy używać? Wszystko zależy od sytuacji. Czasami mamy dane "parametry" zebrane pod postacią tablicy, czasami możemy użyć oddzielnych zmiennych.

Rozszerzamy konstruktor

Po tym bardzo nudnym wywodzie, wracamy do naszych obiektów


function Animal(name, age) {
    this.name = name;
    this.age = age;
    this.type = 'animal';
}
Animal.prototype.eat = function() {
    return this.name + " właśnie je";
}



function Dog(name, age) {
    Animal.call(this, name, age);
    this.name = name;
    this.type = "dog";
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
    return "Wof! Wof!";
}

const dog = new Dog("dog", 8);
console.log(dog.bark()); //Wof! Wof!
console.log(dog.eat()); //"dog właśnie je";

Zauważyłeś, jak w konstruktorze Dog zawołaliśmy o konstruktor Animal? Musieliśmy też dodać parametry, których Animal wymagał - między innymi age. Skoro je dodałem, to przy tworzeniu obiektu na bazie Dog musieliśmy je uzupełnić.

Podobnie chcielibyśmy nadpisać dla Dog funkcję eat() Animala, tak, by początkowo był wykonywany kod z tamtej funkcji, a następnie ten przeznaczony tylko dla Psów:


function Animal(name, age) {
    this.name = name;
    this.age = age;
    this.type = 'animal';
}

Animal.prototype.eat = function() {
    return this.name + " właśnie je";
}



function Dog(name, age) {
    Animal.call(this, name, age);
    this.name = name;
    this.type = "dog";
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.eat = function() {
    const text = Animal.prototype.eat.call(this); //tamta funkcja nie ma parametrów
    return text + " kości";
}

Dog.prototype.bark = function() {
    return "Wof! Wof!";
}

const dog = new Dog("dog", 8);
console.log(dog.bark()); //Wof! Wof!
console.log(dog.eat()); //"dog właśnie je kości";

I w zasadzie tyle. Już wiesz jak rozijać dane konstruktory lub ich metody.

Factory

Obiekty to bardzo złożony temat - bardzo dobrze nadający się na minimum pracę magisterską.

Jest wiele sposobów na tworzenie obiektów. Jednym z nich jest użycie tak zwanych fabryk.
Czym jest taka fabryka? Do tej pory tworzyliśmy instancje obiektów - np. samochodów. Robiliśmy je za pomocą literałów ale też na podstawie jakiejś formy, wydmuszki. Fabryka to miejsce, które wytwarza nam takie obiekty stosując jakiś wzór.

Przykładowy wygląd wzorca fabryki może mieć postać:


//nasza fabryka super bohaterów
const SuperHero = function(name) {
    let heroName = name; //zmienna lokalna do użycia tylko w tej fabryce

    if (typeof heroName === "undefined") {
        heroName = "";
    }

    const calculatePower = function() {
        if (heroName === "Songo") {
            return 150000000;
        }
        if (heroName === "Krillin") {
            return 75000;
        }
        if (heroName === "Gohan") {
            return 200000;
        }
        return "unknown"
    }

    //nasza fabryka zwraca nowy obiekt
    const hero = {
        name : heroName,
        power : calculatePower(heroName),
        print : function() {
            console.log(heroName);
        }
    }

    return hero;
}

const songo = SuperHero("Songo");
const krillin = SuperHero("Krilan");
const gohan = SuperHero("Gohan");
const picolo = SuperHero("Picolo");

Jedną z zalet stosowania tego wzorca jest to, że przy tworzeniu nowych instancji nie musimy pamiętać o słowie kluczowym new. Druga zaleta - jak widzisz powyżej - nasza fabryka może mieć własne metody, które wykorzystuje do dodatkowych własnych zadań.

Dodatkową zaletą tego wzorca jest też pozbycie się problemu z this, ponieważ w metodach zwracanego obiektu go nie używamy, a odwołujemy się bezpośrednio do zmiennych. W przykładzie powyżej takie odwołanie odbywa się w funkcji print:


const SuperHero = function(name) {
    let heroName = name;
    ...
    const hero = {
        name : heroName,
        power : calculatePower(heroName),
        print : function() {
            console.log(heroName);
        }
    }
...
}

const songo = SuperHero("Songo");
document.querySelector('#btn').addEventListener('click', songo.print); //Songo

Gdybyśmy tutaj zamiast wzorca factory korzystali z konstruktora i this przy użyciu takiej funkcji przy kliknięciu na button musielibyśmy ją bindować do obiektu:


function SuperHero(name) {
    this.name = name;
    print : function() {
        console.log(this.heroName);
    }
}

const songo = new SuperHero("Songo");
document.querySelector('#btn').addEventListener('click', songo.print.bind(songo)); //Songo

W przypadku wzorca factory nie musimy takiego bindowania robić.

Innym wzorcem fabryki jest ten korzystający z nowszych metod jak Object.assign() i Object.create(obiektPrototypu), w którym fabryka tworzy nowy obiekt na bazie danego prototypu:


const heroProto = {
    attack : function() {
        console.log(this.name + ' will attack!');
    }
}

const CreateHero = function(name) {
    return Object.assign(Object.create(heroProto), {
        name : name
    })
}

const songo = CreateHero('Songo');
songo.attack();

const krillin = CreateHero('Krillin');
krillin.attack();

instanceof

Powyżej mamy 3 rodzaje obiektów. Obiekty normalne - tworzone za pomocą literałów. Obiekty stworzone na bazie Animal i obiekty stworzone na bazie Dog. Jeżeli byśmy chcieli sprawdzić jakiego "typu" jest dany obiekt, wystarczy użyć operatora instanceof:


    function Animal(name, age) {
        this.name = name;
        this.age = age;
        this.type = 'animal';
    }

    Animal.prototype.eat = function() {
        return this.name + " właśnie je";
    }

    function Dog(name, age) {
        Animal.call(this, name, age);
        this.name = name;
        this.type = "dog";
    }

    Dog.prototype = Object.create(Animal.prototype);
    Dog.prototype.constructor = Dog;

    Dog.prototype.eat = function() {
        const text = Animal.prototype.eat.call(this); //tamta funkcja nie ma parametrów
        return text + " kości";
    }

    Dog.prototype.bark = function() {
        return "Wof! Wof!";
    }

    const dog = new Dog("dog", 8);
    const animal = new Animal("Animal", 10);

    const ob = {
        name : "Marcin"
    }

    console.log(dog instanceof Dog); //true
    console.log(dog instanceof Animal); //true
    console.log(dog instanceof Object); //true
    console.log(animal instanceof Animal); //true
    console.log(animal instanceof Object); //true

    console.log(ob instanceof Dog); //false
    console.log(ob instanceof Animal); //false

Czemu obiekty dog i animal są instancjami Object? Z tego względu, że Animal i Dog dziedziczą po Object. Ale już nasz obiekt literałowy ob nie ma żadnego powiązania z obiektami typu Animal czy Dog więc instanceof zwraca false.

W razie czego gdybyś chciał sprawdzić czy obiekt dog jest typu Dog, wtedy wystarczy spradzić jego konstruktor:


dog.contructor === Dog;

hasOwnProperty

Jeżeli chcemy sprawdzić, czy dana instancja obiektu ma konkretną właściwość lub metodę, użyjemy metody ob.hasOwnProperty(method):


const ob = {
    name : "Marcin",
    print : function() {}
}

console.log(ob.hasOwnProperty("print")); //true
console.log(ob.hasOwnProperty("name")); //true
console.log(ob.hasOwnProperty("surname")); //false

Metoda lub właściwość musi zawierać się w danej instancji:


const MyObj = function(name) {
    this.name = name,
    this.sayName = function() {}
}
MyObj.prototype.print = function() {}

console.log(ob.hasOwnProperty("name")); //true
console.log(ob.hasOwnProperty("sayName")); //true
console.log(ob.hasOwnProperty("print")); //false

Do czego to może się przydać?

Przde wszystkim do sprawdzenia, czy dana właściwość istnieje w danej instancji:


const myObj = {}
console.log(ob['toString']) //pokaże kod funkcji dziedziczonej z prototypu Object

"toString" in myObj; // sprawdzam czy metoda toString jest dostępna dla myObj
myObj.hasOwnProperty("toString") // false

W praktyce bardzo często będziesz robił pętle po obiektach i będziesz chciał mieć pewność, że w takiej pętli nie pokaże się coś spoza danego obiektu:


const MyObj = function() {
    this.data = null;
}
MyObj.prototype.name = "Marcin";
MyObj.prototype.print = function() {
    console.log(this.name);
}

const ob = new MyObj();
ob.data = {
    "2015-10-10" : { name: "Wycieczka za miasto", where: "Puszcza"},
    "2015-11-10" : { name: "Zakupy w markecie", where: "Market"},
    "2015-12-09" : { name: "Sprawy w szkole", where: "Szkola"}
}

for (let i in ob) { //i to kolejne klucze czyli daty
    console.log(ob[i]) //wypisze także "Marcin" i funkcje print
}

for (let i in ob) {
    if (ob.hasOwnProperty(i)) {
        console.log(ob[i]) //wypisze tylko obiekty z właściwości data
    }
}

Ogólnie więc jeżeli będziesz robił pętlę po pobranych danych (np JSON) które są w formie obiektu, zawsze dodawaj sprawdzenie hasOwnProperty:


for (var i in result.data) {
    if (result.data.hasOwnProperty(i)) {
        console.log(result.data[i])
    }
}

Różne odwołania

W powyższym kodzie zauważyłeś pewnie ciekawy sposób do odwołania się do właściwości obiektu:


const ob = {
    name : "Marcin",
    pisz : function() { ... }
}

ob.name
ob.pisz();

ob["name"]
ob["pisz"]();

Druga metoda, może rzadziej stosowana czasami bardzo się przydaje. Musisz wiedzieć, że klucze obiektów wcale nie muszą być nazywane jak zmienne. Mogą tak spokojnie wystąpić dziwne znaki czy nawet spacje. W takiej sytuacji odwoływanie się przez kropkę jest niemożliwe, wiec pozostaje tylko druga metoda:


const ob = {}
ob.my Super Name = ""; //oczywisty błąd

ob['my Super Name'] = "Marcin"; //wszystko ok
console.log(ob["my Super Name"]); //Marcin

Przydaje się to szczególnie w przypadku dynamicznie tworzonych właściwości obiektu (np. przy grupowaniu jakiś danych pod postacią obiektu).

Wyobraź sobie dla przykładu, że masz tablicę jakiś danych, które chciałbyś zliczyć:


const tab = [
    "Ala ma kota",
    3,
    "Ania lubi czekoladki",
    "Ala ma kota",
    2,
    "Piesek Rysiek",
    "Piesek Rysiek",
    2,
    "Ania lubi czekoladki",
    "Ania lubi czekoladki"
];

const ob = {};
tab.forEach(function(el) {
    if (!ob.hasOwnProperty(el)) { //by móc sumować, musimy wcześniej stworzyć taką właściwość
        ob[el] = 0;
    }
    ob[el]++;
})

console.log(ob);
Obiekt z właściwościami

Zauważ jakie właściwości ma nasz obiekt. Notacją z kropką nie byłbyś w stanie się do nich odwołać. Pozostaje notacja z nawiasami.


ob.2.length //błąd
ob["2"].length //2
ob["3"].length //1
ob["Ala ma kota"].length //2

Na chwilę przejdźmy do tematu tablic. Już kilka razy przewijało się w tym kursie stwierdzenie, że w JS praktycznie wszystko jest obiektami. Tablice też nimi są.


const tab = ["kot", "pies", "chomik super ninja"];

tab.0 //błąd
tab[0] //kot
tab["0"] //kot

tab[2] //chomik super ninja
tab["2"] //chomik super ninja
tab.2.length //błąd
tab["2"].length //18

tab.length //3
tab["length"] //3

Przy podawaniu indeksów w tablicach podajemy je jako numery. Do tego się przyzwyczailiśmy. Tak samo jak przy obiektach (bo tablice nimi są) moglibyśmy podawać je jako string, ale nie musimy tego robić, bo JS automatycznie dokonuje tutaj konwersji.

Object.create()

Wraz z rozwojem JS dochodziły nowe sposoby na tworzenie obiektów. Jednym z nich jest użycie metody Object.create(obPrototype, propertyObject). Jako pierwszy parametr przyjmuje obiekt, który stanie się prototypem nowo tworzonych obiektów:


const ob = {
    name : "Marcin"
}

const son = Object.create(ob);
son.age = 10;

console.log(son);
object create

Przy tworzeniu nowych obiektów za pomocą Object.create() ich prototypem staje się obiekt, który podaliśmy w nawiasach. Dzięki temu można to wykorzystać do stworzenia dziedziczenia:


const animalObj = {
    eat : function() {
        console.log(this.name + " właśnie je");
    }
}

const birdObj = Object.create(animalObj);
birdObj.fly = function() {
    console.log(this.name + " właśnie lata");
}

const bird = Object.create(bird);
bird.name = "Ptak 1";
bird.fly();
bird.eat();

Drugim parametrem Object.create() jest obiekt, który zawiera dodatkowe właściwości nowo tworzonego obiektu. Każdą taką zmienną możemy opisać za pomocą kilku właściwości:


const obj = Object.create(Object.prototype, {
    foo: {
        writable: true,
        configurable: true,
        value: 'hello'
    },
    bar: {
        configurable: false,
        get: function() { return 10; },
        set: function(value) {
            console.log('Ustawiam wartość bar na: ', value);
        }
    }
}

Każda taka właściwość może być opisana za pomocą właściwości:

  • writable (true/false) - czy zmienna może być zmieniana.
  • configurable (true/false) - czy zmienna może być zmieniana przez ten obiekt, lub czy może być usuwana (delete ob.nazwaWlasciwosci)
  • value - wartość tej właściwości. Nie można tego używać wraz z get/set
  • get - tak zwany getter - funkcja wywoływana gdy pobieramy daną wartość
  • set - tak zwany setter - funkcja wywoływana gdy ustawiamy daną wartość
  • enumerable - czy wartość ma być widoczna przy pętlach iteracyjnych (np. for in) - np. length nie jest

W powyższym kodzie stworzyliśmy obiekt, od razu przy tym ustawiając jego właściwości. Możemy też takie właściwości dodawać poza ciałem obiektu za pomocą defineProperty():


const ob = {
    _name : "marcin",
    _height : 184
};

Object.defineProperty(ob, 'name', {
    set : function(newName) {
        this._name = newName;
    },
    get : function() {
        return this._name.charAt(0).toUpperCase() + this._name.substr(1).toLowerCase();
    }
});

Object.defineProperty(ob, 'height', {
    get : function(height) {
        this._height = height;
    },
    set : function(height) {
        this._height = height + 'cm';
    }
});

Object.defineProperty(ob, 'gender', {
    writable : false,
    value : "male"
});

console.log(ob.name); //Marcin
ob.name = "grzegorz";
console.log(ob.name); //Grzegorz

ob.height = 180;
console.log(ob.height); //180cm

ob.gender = "woman"; //male - nie możemy zmienić tej właściwości

Powyższe definiowanie właściwości możemy też połączyć z konstruktorem:


function Car(brand, color) {
    this.brand = brand;
    this.color = color;

    var _speed; //zmienna prywatna niedostępna z zewnątrz

    Object.defineProperty(this, "speed", {
        get: function() {
            return this._speed;
        },
        set: function(value) {
            this._speed = value;
            if (this._speed > 180) {
                this._speed = 180;
            }
        }
    });
}

const car = new Car("BMW", "Black");
car.speed = 160;
car.speed += 20;
car.speed += 20; // 180 a nie 200!
console.log(car.speed); //180km

Jeżeli temat cię zainteresował polecam dwa artykuły: https://addyosmani.com/resources/essentialjsdesignpatterns/book/ i http://robotlolita.me/2011/10/09/understanding-javascript-oop.html

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Stwórz za pomocą literału obiekt currentUser. Obiekt niech ma:

    - właściwości name, surname, age, email, www, userType,
    - metodę printDetails, która wypisze wszystkie te rzeczy w konsoli.

    Wypisywany tekst powinien być ładnie sformatowany np.:
    
                console.log("Nazwa użytkownika: ", ....);
            
    Wywołaj metodę tego obiektu.
    
                    const currentUser = {
                        name : "Marcin",
                        surname : "Nowak",
                        age : 16,
                        email : "marcinnowak@gmail.com",
                        www : "nowak.pl",
                        userType : "editor",
                        printDetails : function() {
                            console.log("Imię: " + this.name);
                            console.log("Nazwisko: " + this.surname);
                            console.log("Wiek: " + this.age);
                            console.log("Email: " + this.email);
                            console.log("www: " + this.www);
                            console.log("Typ użytkownika: " + this.userType);
                        }
                    }
    
                    currentUser.printDetails();
                
  2. Chcesz stworzyć 5 obiektów typu Car. Każdy samochód powinien mieć ustawione:
    - właściwości name, brand, engine, mile, age
    - metodę printDetails, która wypisze powyższe właściwości.
    Wykorzystaj w tym zadaniu prototyp.
    
                function Car(name, brand, engine, mile, age) {
                    this.name = name;
                    this.brand = brand;
                    this.engine = engine;
                    this.mile = mile;
                    this.age = age;
                }
                Car.prototype.printDetails = function() {
                    console.log("Nazwa: " , this.name);
                    console.log("Marka: " , this.brand);
                    console.log("Silnik: " , this.engine);
                    console.log("Przejechane: " , this.mile);
                    console.log("Wiek:" , this.age);
                }
    
                const car1 = new Car("Corolla", "Toyota", 1.4, 240000, 15);
                const car2 = new Car("F10", "BMW", 520, 38000, 2);
                const car3 = new Car("BSL", "Cadilac", 1.4, 240, 15);
                const car4 = new Car("Klasa E", "Mercedes Benz", E220, 500, 1);
                const car5 = new Car("Mazda III", "Mazda", 1.4, 29000, 2);
                
  3. Stwórz 2 obiekty.

    Pierwszy powinien mieć:
    - właściwości name, age, email
    - właściwość print, która wypisze tekst np. "Marcin ma 14 lat, a jego email to lorem@gmail.com"

    Drugi obiekt powinien mieć:
    - właściwości name, age, email
    - metodę print, która porzyczy sobie kod metody print z 1 obiektu
    
                    const ob1 = {
                        name : "Marcin",
                        age : 15,
                        email : "marcinnowak@gmail.com",
                        print : function() {
                            console.log(this.name + " ma " +this.age+ " lat, a jego email to " + this.email);
                        }
                    }
    
                    const ob2 = {
                        name : "Piotr",
                        age : 18,
                        email : "piotr@gmail.com"
                    }
    
                    ob1.print.call(ob2);
                
  4. Rozbuduj obiekty typu String, dodając do ich prototypu metodę String.sortText(char).
    Powinno się dać ją użyć na dowolnym tekście.
    Po użyciu powinna ona sortować słowa w danym tekście, a następnie zwracać podobny tekst, tylko posortowany:

    
            "Marcin|Ania|Piotrek|Beata".sortText(',');
    
            output: "Ania|Beata|Marcin|Piotrek"
            
    Wykorzystaj odpowiednie metody dzielące tekst na tablicę, sortujące tablicę i łączące ją w tekst.
    
                    String.prototype.sortText = function(char) {
                        const tab = this.split(char);
                        tab.sort();
    
                        const newStr = tab.join("|");
                        return newStr;
                    }
    
                    console.log("Marcin|Ania|Piotrek|Beata".sortText(','));