Obiekty - konstruktor

W poprzednich rozdziałach tworzyliśmy pojedyncze instancje obiektów. Poza tym, że były obiektami nie łączyło ich zbyt wiele.
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, Slider, Accordion czy podobne.
Każdy obiekt takiego typu powinien posiadać jakieś właściwości i metody np. name, surname, width, height i printDetails(), slide().

Podobnie zresztą jak w przypadku innych podobnych do siebie obiektów. Wszystkie tablice (też obiekty) mają właściwość length, metodę push(), pop(), concat() itp., obiekty typu Date mają metodę getFullYear(), setHours(), a obiekty typu String() np. metodę toUpperCase() czy właściwość length. Takich obiektów należących do danej grupy w całym tym kursie będziemy używać jeszcze od groma.

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 Javascript powyższy mechanizm początkowo został wymyślony nieco inaczej, bo zamiast klas używamy tutaj konstruktorów - których role pełnią zwykłe funkcje - o czym za chwilę.
W nowych wersjach JS została wprowadzona nakładka na mechanizm konstruktorów, która upodabnia go do klasycznych klas. Pamiętaj jednak, że jest to tylko nakładka, inny zapis tego samego.
By wiedzieć co taka nakładka realnie robi, powinniśmy wiedzieć jak działa mechanizm konstruktorów. A dowiemy się o tym już za moment.

Tworzenie konstruktora

Stwórzmy przykładowy konstruktor, na bazie którego stworzymy nowe obiekty:


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. Wszystkie właściwości i metody, które mają się pojawić w obiektach tworzonych na bazie tego konstruktora musimy zapisywać z użyciem słowa kluczowego this - co pokazuje powyższy kod.

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");
car1.print(); //Fiat koloru czerwony

const car2 = new Car("BMW", "czarny");
car2.print(); //BMW koloru czarny

Właściwości, które chcemy by były podawane przy tworzeniu obiektu podawane są jako parametry konstruktora. Cała reszta właściwości ustawiana jest po prostu na sztywno.
Wszystkie właściwości, które chcemy by stały się właściwościami przyszłych obiektów musimy podawać z wykorzystaniem słowa this. Zmiennie dodatkowe służące do pomniejszych wyliczeń tworzymy klasycznie za pomocą let, const czy var. To samo będzie się tyczyło metod.


function Car(name, age, speed) {
    this.name = name;
    this.age = age;

    var risk = "small";
    if (age > 8 && age <= 15) {
        risk = "average";
    } else if (age > 15) {
        risk = "big"
    }

    this.status = risk;
}

const car1 = new Car("Fiat", 10, 120);
console.log(car1); {name: "Fiat", age: 10, status : "average"}

Powyżej stworzyliśmy konstruktor 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 (literał - skrócony zapis). 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);
    

Metody te przydają się raczej w nielicznych sytuacjach, a w codziennej pracy nie są raczej zalecane.
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 JavaScript 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ć nieoczekiwane 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

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.

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 automatycznie dodaną przez Javascript 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, JavaScript po pierwsze ustawi takiemu obiektowi prototyp biorąc go właśnie z właściwości prototype naszego konstruktora (na ten obiekt będą wskazywać __proto__ nowych obiektów), a dodatkowo zmieni kontekst słowa this, które od tego momentu będzie wskazywać na daną instancję obiektu, a nie na obiekt globalny window.

Czyli JavaScript za naszymi plecami wykona mniej więcej taki kod:


const car1 = new Object(); //tworzy pusty obiekt
car1.__proto__ = Car.prototype; //ustawia jego prototyp
Car.call(car1); //wywołanie funkcji call z ustawieniem w jej wnętrzu this na dany obiekt

Sprawdźmy na przykładzie:


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 = new Car("Fiat", "czerwony");
console.log(car1.__proto__ === Car.prototype) //true

A co się stanie gdy słowo new pominiemy? Konstruktor funkcji zostanie odpalony jak zwykła funkcja, a this nie będzie wskazywał na instancję obiektu a na obiekt window:


const Test = function() {
    console.log(this);
}

const test1 = Test("Fiat", "czerwony"); //Window

const test2 = new Test("BMW", "czarny"); //MiniCar

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 ta wiedza? Powyższy prototyp jest obiektem, więc możemy go zmieniać tak samo jak to robiliśmy z pojedynczymi obiektami w poprzednich rozdziałach.

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ą?


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


//typy referencyjne - obiekty
let obA = {nr : 2};
let obB = obA;
let obC = obB;
let obD = obC;
let obE = obD;

obA.nr = 4; //zmieniam tylko obA

console.log(obA.nr, obB.nr, obC.nr, obD.nr, obE.nr); //4, 4, 4, 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 (tak jak powyżej wszystkie zmienne obA-D wskazywały na ten sam obiekt).
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;
}

//właś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 superbohaterem";
}
Hero.prototype.saySomething = function() {
    var greeting = this.sayHello();
    return greeting + " a poza tym 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. Zapomnieliś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 Helicopter(name, age) {
    this.name = name;
    this.age = surname;
    this.attack = function() {
        return this.name + " " + this.age + " atakuję!";
    }
}

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

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

Jeżeli jednak zawrzemy ją w prototypie - tym samym obiekcie dla wszystkich instancji naszych obiektów - to ta metoda będzie występować tylko w jednym miejscu w pamięci - w obiekcie prototypu.


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

Helicopter.prototype.attack = function() {
    return this.name + " " + this.age + " atakuję!";
}

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

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


army[0].__proto__.attack === army[1000000].__proto__.attack //true

Object.getPrototypeOf(army[0]).attack === Object.getPrototypeOf(army[1000000]).attack; //true
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 JavaScript 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 odwoływać się do prototypu za pomocą metody Object.getPrototypeOf(obj) zamiast bezpośrednio poprzez __proto__

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, JavaScript sprawdza 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ć w powyższym kodzie.

Czemu tak się dzieje? Do konwersji JavaScript wykorzystuje metodę toString(). No ale przecież przy obiektach i tablicach mamy inny wynik. Dzieje się tak dlatego, że do konwersji na tekst używane są 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__ wskazują na prototyp w konstruktorze 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("Karol", "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 konstruktora są tworzone obiekty, które mają właściwości __proto__ wskazujące na ten prototyp, to spełnione jest poniższe równianie:


const h1 = new Human("Marcin", "Nowak");
h1.__proto__ === Human.prototype //true

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


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

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


const h2 = new Human("Karol", "Kowalski");
h2.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. Zamiast niej używaj wcześniej pokazanej metody działania na Car.prototype do którego wcześniej ustawiajmy odpowiednie składowe.

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 i=0; i<this.length; i++) {
        if (i % 2 === 0) {
            text += this[i].toUpperCase();
        } else {
            text += this[i].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 JavaScript przejdzie do wyższego prototypu 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('Przykładowa 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 JavaScript pojawiły się kontrowersje. Znana biblioteka MooTools rozszerzała wbudowane obiekty o własne metody. W pewnym momencie twórcy JavaScript 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.

Trening czyni mistrza

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

  1. Chcesz stworzyć 3 obiekty typu Car. Każdy samochód powinien mieć ustawione właściwości podawane przy tworzeniu obiektu: - name - string,
    - brand - string,
    - engine - string,
    - mile - number,
    - age - number,

    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 car5 = new Car("Mazda III", "Mazda", 1.4, 29000, 2);
                
  2. Stwórz konstruktor Enemy. Każdy obiekt budowany na jego bazie powinien mieć właściwości:
    - name - string - podawane przy tworzeniu - nazwa przeciwnika
    - live - number - ustawione na sztywno na 5
    - speed - number - podawany przy tworzeniu
    - attack - number - podawane przy tworzeniu instancji obiektu
    - posX - number - podawane przy tworzeniu instancji obiektu
    Za pomocą prototypu stwórz metody:
    - metodę move(), która ustawi przeciwnika w nowej pozycji. Pozycję wylicz odejmując od posX szybkość danego obiektu. Funkcja dodatkowo niech wypisze tekst w konsoli "Jestem NNN. Znajduję się na pozycji XXX", w którym wstaw odpowiednie właściwości obiektu.
    - metodę attackEnemy(), która wypisze w konsoli tekst Jestem NNN. Atakuję gracza z pozycji X z siłą XXX wstawiając do niego odpowiednie właściwości obiektu
    - metodę hit(), która odejmie przeciwnikowi jeden punkt życia (live). Dodatkowo niech wypisze w konsoli tekst "Jestem NNN. Mam teraz życia LLL".

    Stwórz 2 obiekty typu Enemy. Odpal dla niech kilka razy metodę move(). Odpal dla każdego z nich metodę attack().
    Dla drugiego z nich odpal 2x metodę hit().
    
                        const Enemy = function(name, speed, attack, posX) {
                            this.name = name;
                            this.live = 5;
                            this.speed = speed;
                            this.attack = attack;
                            this.posX = posX;
                        }
                        Enemy.prototype.printName = function() { //upraszczaj swój kod dodatkowymi metodami
                            return "Jestem " + this.name + ". "
                        }
                        Enemy.prototype.move = function() {
                            this.posX = this.posX - this.speed;
                            console.log(this.printName() + "Znajduję się na pozycji " + this.posX);
                        }
                        Enemy.prototype.attackEnemy = function() {
                            console.log(this.printName() + "Atakuję gracza z pozycji " + this.posX + " z siłą " + this.attack);
                        }
                        Enemy.prototype.hit = function() {
                            this.live--
                            console.log(this.printName() + "Mam teraz życia " + this.live);
                        }
    
    
                        const e1 = new Enemy("Enemy1", 4, 10, 250);
                        e1.move();
                        e1.move();
                        e1.move();
                        e1.attackEnemy();
    
                        const e2 = new Enemy("Enemy2", 3, 15, 250);
                        e2.move();
                        e2.move();
                        e2.move();
                        e2.attackEnemy();
                        e2.hit();
                        e2.hit();
                        e2.hit();
                    
  3. 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(char);
                            return newStr;
                        }
    
                        console.log("Marcin|Ania|Piotrek|Beata".sortText('|'));