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.

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ść obiektu window

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

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

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

itp...

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

Tworzenie pojedynczego obiektu (literału)

Aby utworzyć nowy pojedynczy obiekt możemy skorzystamy z poniższej konstrukcji:


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

Stworzyliśmy nasz obiekt o nazwie myObj za pomocą prostej pary klamer - jest to tak zwany literał.
Nasz obiekt posiada dwie właściwości - name i height, oraz jedną metodę, która wypisuje jego imię.

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


myObject.print(); //wypisze w konsoli "Marcin"
console.log(myObject.height); //wypisze 184

Można powiedzieć, że nasz obiekt to taka tablica (tak naprawdę działa to w drugą stronę - to tablica jest obiektem). Kluczową tutaj różnicą jest to, że w tablicach by odwołać się do jakiejś właściwości musimy wiedzieć, na którym miejscu (indeksie) ta właściwość się znajduje. W obiektach odwołujemy się po nazwie właściwości, więc takiego miejsca znać nie musimy.


const tab = ["Marcin", "Szamson", "Rakietowy Szymek", "marchewka", 184];

console.log(tab[0] + " - " + tab[4]);


const myObj = {
    name: "Marcin",
    pet: "Szamson",
    food : "Marchewka",
    favouriteHero : "Rakietowy Szymek",
    height: 184,
    print : function() {
        console.log(this.name);
    }
}

console.log(myObj.name + " - " + myObj.height);

this

Aby odwołać się do danego obiektu z wnętrza którejś z jego metod (czyli z wnętrza jego funkcji) stosujemy słowo kluczowe this, które wskazuje na obiekt, w którym ta funkcja jest zawarta* (zobaczysz później, że to nie jest zawsze prawdą).

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


const user = {
    name : "Karol",
    surname : "Nowak",
    age : 26,

    saySomething : function() {
        return this.name + " " + this.surname + " lat: " + this.age;
    }
}

console.log( myObject.saySomething() ); //Karol Nowak lat 26

Gdy nasz obiekt już istnieje, możemy do niego dodawać nowe metody:


const user = {
    name: "Marcin",
    height: 184,
    sayName : function() {
        console.log(this.name);
    }
}

user.weight = 73; //dodaliśmy nową właściwość

user.printDetail = function() {
    console.log(this.name + ' o wadze' + this.weight);
}

user.printDetail();

Usuwanie właściwości

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


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

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

console.log(user.height); //184
delete user.height;
console.log(user.height); //undefined

Jak to w ogóle w JS 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? 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, wystawiłem ci go) debugera zobaczysz właściwość __proto__. Jest to właściwość dodawana przez JS automatycznie każdemu obiektowi. JS za nas dodał coś takiego jak poniżej:


const ob = {
    __proto__ = {...to jest obiekt nadrzedny...}
}

__proto__ jest to wskaźnik na obiekt prototypu, na którym opiera się dany obiekt.
Każdy obiekt w JS zbudowany jest na bazie jakiegoś prototypu - obiektu wzorca. Nawet jak robisz pojedyńczą instancję (jeden obiekt literał - tak jak robiliśym powyżej) to jest ona połączona z jakimś obiektem wzorcem 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 danym obiekcie (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, Date itp.). Szuka w takm obiekcie prototypie danej metody.

Jeżeli jej tam nie znajdzie, za pomocą wskaźnika __proto__ (znajduje się w każdym obiekcie, więc także i prototypie) przechodzi do nadrzędnego prototypu (za pomocą kolejnego __proto__ który wskazuje na taki prototyp).

I szuka w prototypie, na bazie którego zbudowane są obiekty, z których dziedziczą nasze obiekty. Jeżeli tam nie znajdzie idzie znowu w górę.

I tak szuka, szuka, aż dojdzie do najwyższego prototypu - grand master rodzica wszelkich prototypów - Object, na bazie którego zbudowane są wszelkie obiekty w JS (a który to już __proto__ nie ma więc JS nie ma gdzie iść w górę).

W naszym przypadku nasz obiekt nie ma metody toString(), ale gdy badając go w konsoli rozwiniemy jego składowe zobaczymy, że ma __proto__. Gdy rozwiniemy i jego środek zobaczymy, że wskazuje on na prototyp naszego obiektu. Prototyp ten to sam grand master - 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 porzyczył sobie metodę od wielkiego mistrza. Wielki zaszczyt...

prototyp z debugera 1

Tworzenie obiektu za pomocą konstruktora

Na chwilę musimy odskoczyć od tych poważnych tematów.

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.
Każdy instancja-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 itp., obiekty Date mają metodę getFullYear(), setHours() itp, a obiekty String np. metodę toUpperCase().

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ć tworzone na jej podstawie nowe obiekty.

W JS w przeciwieństwie do innych języków nie mamy mechanizmu klas (teoretycznie wprowadzono to w ES6), ale samą klasę możemy stworzyć za pomocą zwykłej funkcji - tak zwanego konstruktora:


function SuperObj(width, height) {
    this.width = width;
    this.height = height;

    this.print = function() {
        console.log(this.width + 'x' + this.height)
    }
}


//tworzymy 2 instancje na bazie powyższej wydmuszki
const obj1 = new SuperObj(200, 100);
const obj2 = new SuperObj(300, 200);

obj1.print(); //wypisze 200x100

obj2.width = 600;
obj2.print() //wypisze 600x200

Nazwy konstruktorów piszemy z dużej litery. To tylko konwencja nazewnicza - tak by odróżnić je od funkcji do klasycznego przeznaczenia (konstruktory używa się do tworzenia nowych obiektów, i raczej do niczego innego).

W tym miejscu żałuję, że ten kurs nie jest filmem, a tylko statycznym tekstem.
Z chęcią bym Ci pokazał jak całe to zagadnienie fajnie wygląda w programie GameMaker.
Jest to program do tworzenia gier.
Aby utworzyć obiekty w grze, na początku tworzymy je sobie klikając jakieś opcje. Wyobraź sobie, że tworzę obiekt "enemy".
Dodaję jakieś właściwości, zdarzenia, jak tacy przeciwnicy mają się zachowywać przy zetknięciu np z graczem, dodaję grafiki ruchu, skoku itp.

Potem gdy układam już konkretne plansze gry (levele), przeciągam z bocznej belki obiekt przeciwników na planszę. Dzięki temu tworzę kolejne instancje obiektów na bazie jakiejś ogólnej klasy "enemy" (w js konstruktora), którą wcześniej stworzyłem.

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 itp. 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 kontruktorów wybieraj klasyczne skrócone deklaracje (które używałeś do tej pory).

Prototyp

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

Jest to bardzo ważne słowo. Gdybyśmy go nie użyli, żadne obiekty by się nie stworzyły, tylko podstawilibyśmy wywołanie kodu danej funkcji pod zmienne. Ale uwaga. Gdy w takiej funkcji-konstruktorze występowało by słowo kluczowe this, wtedy w przypadku braku użycia new przy tworzeniu instancji, this nie wskazywało by na nowo utworzony obiekt, tylko na window!


const Man = function(surname) {
    this.surname = surname; //this wskazuje na window
}

const ob = Hero("Piotr"); //brakuje słowa kluczowego new
ob.surname; //undefined

window.surname; //Piotr

Jeżeli ktoś jest mało pamiętliwy i 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ś kontruktorze:


const Man = function(name) {
    if (!(this instanceof Man)) {
        return new Man(name);
    }
    this.name = name; //this wskazune na window
}

const ob = Hero("Piotr"); //też specjalnie brakuje new
ob.surname; //Piotr

Poza możliwością tworzenia podobnych obiektów użycie konstruktora tworzy za naszymi plecami nowy obiekt prototype i ustawia go jako prototyp dla obiektów, które są tworzone na bazie naszego konstruktora.
Obiekty tworzone literałami mają __proto__, które wskazuje na ogólny prototyp (prototyp Object).
Nasze obiekty tworzone na bazie naszego konstruktora mają swój własny prototyp.

Skąd to wiemy?
Gdy 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 width, height, metodę print oraz właściwość __proto__, która wskazuje na nowo utworzony prototyp.

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 konstuktor), oraz ... __proto__, który wskazuje na prototyp "nadrzędny" (w naszym przypadku będzie to nasz prototyp Object).

Prototyp 3

W razie czego gdyby w naszym pustym prototypie JS nie znalazł odpowiedniej metody czy właściwości, przejdzie do kolejnego prototypu, na który wskazuje __proto__ - czyli zadziała mechanizm, o którym mówiliśmy powyżej.

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

Co nam to daje?

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


let a = 2;
let b = a;
a = 4; //zmieniam tylko a
console.log(a, b); //4, 2


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.
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 temu obiektowi jakąś właściwość, to usuniemy ją we wszystkich zmiennych, które wskazują na ten obiekt.

Skoro kilka naszych obiektów zbudowanych na tym samym konstruktorze wskazuje za pomocą __proto__ na ten sam prototyp, to wskazują na ten sam obiekt.
Dalej. Jeżeli w tym obiekcie jest jakaś metoda - np printName(), to jest ona w pamięci w jednym i tym samym miejscu. Jeżeli któryś z naszych obiektów by ją zmienił, zmieni się ona dla wszystkich obiektów.

Wiąże się to z dwoma rzeczami. Raz, że za pomocą prototypu możemy ustawiać lub zmieniać właściwości i metody dla wszystkich obiektów stworzonych na bazie danego konstruktora, nawet tych, które już zostały wcześniej stworzone (w końcu one też wskazują za pomocą __proto__ na ten sam obiekt-prorotyp):


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

//tworzę nowy obiekt
const mister = new Human('Marcin', 'Nowak', 26);

//dodaję nową metodę do prototypu
Human.prototype.showInfo = function () {
    return this.name + ' ' + this.surname + ' ma ' + this.height + ' lat.';
}

mister.showInfo(); //Marcin Nowak ma 26 lat

Druga rzecz tyczy się optymalizacji.
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 (var 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 100000 różnych właściwości name, 100000 różnych surname i uwaga - tyle samo metod print, która przecież za każdym razem będzie taka sama!

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.
Zwie się to - najzwyklej mówiąc - oszczędność zasobów.


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 prorotyp korzystając z metody Object.getPrototypeOf(obj) i sprawdźmy:


const user = new Human("Marcin", "Nowak");
Object.getPrototypeOf(user);
Właściwość __proto__ jest kontrowersyjną właściwością. Mimo tego, że nie występuje w specyfikacji większość przeglądarek i tak zaimplementowało tą właścwiość. Dla zapewnienia kompatybilności z przeglądarkami została wprowadzaona 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

Za pomocą prototypu możemy też rozwijać domyślne konstruktory dostępne w JS.


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

function mixLetterSize() {
    const tekst = '';
    for (const x=0; x<this.length; x++) {
        tekst += (x%2==0)?this.charAt(x).toUpperCase() : this.charAt(x).toLowerCase();
    }
    return tekst;
}
String.prototype.mixLetterSize = mixLetterSize;

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

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

Nie jest to do końca polecana praktyka, bo może 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.
Jasne - można rozwijać obiekty bazowe, ale miej na uwadze, że czasami niektórzy mogą być zmieszani...

Mega siatka

Z cełego powyższego opisu wyłania się nam (a może nie wyłania) 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 prorotypu (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ń

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(that.pets);
    }.bind(this));

...

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 policzki...
Niektóre właściwości też nadpisują, bo na przykład włosy są niby rude, ale jakieś takie nie za bardzo...

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


function Animal(name, age) {
    this.name = name;
    this.age = age;
    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 szamson = new Dog("Szamson");
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 konstuktor 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 szamson = new Dog("Szamson");
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("Horsi");
horse.bark(); //Wof! Wof!

Rozwiązaniem nie jest zrównywanie prototypów, a stworzenie nowego obiektu prototypu bazującego na innym typie 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 szamson = new Dog("Szamson");
console.log(dog.bark()); //Wof! Wof!
console.log(dog.eat()); //"dog właśnie je";

const anim = new Animal();
anim.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, 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.
Najczęściej 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 myF() {
    console.log('Jestem funkcją');
}

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


myF();

//lub

myF.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"); //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.printHiToPet.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 szamson = new Dog("Szamson", 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 szamson = new Dog("Szamson", 8);
console.log(dog.bark()); //Wof! Wof!
console.log(dog.eat()); //"dog właśnie je kości";

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

Object.create()

Metoda Object.create(obPrototype, propertyObject) służy do tworzenia nowych obiektów. 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 za pomocą operatora przyrównania (=)
  • configurable (true/false) - czy zmienna może być zmieniana przez ten obiekt
  • value - wartość tej właściwości
  • get - tak zwany getter - funkcja wywoływana gdy pobieramy wartość właściwości
  • set - tak zwany setter - funkcja wywoływana gdy ustawiamy wartość właściwości
  • enumerable - czy wartość ma być widoczna przy pętlach iteracyjnych (np. for in)

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

instanceof

Powyżej mamy 3 rodzaje obiektów. Obiekty normalne - literały. 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 szamson = new Dog("Szamson", 8);
    const animal = new Animal("Animal", 10);

    const ob = {
        name : "Marcin"
    }

    console.log(szamson instanceof Dog); //true
    console.log(szamson instanceof Animal); //true
    console.log(szamson 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 szamson 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 szamson jest typu Dog, wtedy wystarczy spradzić jego konstruktor:


szamson.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 (var i in ob) { //i to kolejne klucze czyli daty
    console.log(ob[i]) //wypisze także "Marcin" i funkcje print
}

for (var 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[2] //chomik super ninja
tab.2.length //błąd
tab["2"].length //18

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.

toString

Jeżeli dodajesz do siebie różne typy w wyniku dostajesz string:


const ob = { a: "kot" };
const text = "Ala ma kota";
const arr = [1,2,3];
const date = new Date();

console.log( arr + text ); //"1,2,3Ala ma kota"
console.log( ob + arr ); //"[object Object]1,2,3"
console.log( ob + text ); //"[object Object]Ala ma kota"
console.log( date + ob); //Sun Nov 19 2017 13:59:19 GMT+0100 (Środkowoeuropejski czas stand.)[object Object]

Jak widzisz nasze obiekty zostały skonwertowane na string. Dodatkowo dla różnych typów obiektów ten string ma nieco inną postać - dla obiektów jest to [object Object], dla tabel wartości tabeli po przecinku, a dla Date czas w formie tekstowej.

Konwersja taka wykonywana jest za pomocą metody toString().
Różne działanie tej metody wynika z tego, że niektóre typy obiektów mają swoja własną implementację tej metody, a niektóre dziedziczą ją z prototypu Object.

My dla naszych własnych obiektów też możemy zaimplementować swoją wersję tej metody. Skoro będzie ona w prototypie obiektów, to JS weźmie naszą implementację, a nie tą z prototypu Object:


const User = function(name, age) {
    this.name = name;
    this.age = age
}
User.prototype.toString = function() {
    return '[' + this.name + ' : ' + this.age + 'lat]';
}


const user = new User("Piotr", 15);
const arr = [1,2,3];
const ob = { a : 2 };

console.log("Mój obiekt to: " + user); //"Mój obiekt to: [Piotr : 15lat]"

console.log( arr + ob + user ); //"1,2,3[object Object][Piotr : 15lat]"

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(','));