Konstruktor - dziedziczenie

Uwaga! Poniższe manewry mogą wydać ci się skomplikowane. Nie przejmuj się tym!
W dzisiejszych czasach takie rzeczy jeżeli będziesz robił, to raczej za pomocą składni Class, którą poznamy w kolejnym rozdziale. Jest tam to wygodniejsze, czytelniejsze i działa zwyczajnie lepiej.

Załóżmy, że mamy konstruktor, który tworzy nam przeciwnika:


function Enemy(name, x, y) {
    this.name = name;
    this.x = x;
    this.y = y;
    console.log(`Tworzę przeciwnika: ${this.x}x${this.y}`);
}

Enemy.prototype.fly = function() {
    return this.name + " lecę";
}

Chcielibyśmy stworzyć nowy typ obiektów - np. "EnemyShoot", który miałby wszystkie powyższe funkcjonalności, ale równocześnie potrafiłby strzelać.


function EnemyShoot(name, x, y) {
    this.name = name;
    this.x = x;
    this.y = y;
    this.type = "shooter";
}

EnemyShoot.prototype.shoot = function() {
    return this.name + " strzelam";
}

const enemyS = new EnemyShoot("Shooter", 20, 20);
console.log(enemyShoot.shoot()); //Shooter strzelam
console.log(enemyShoot.fly()); //błąd - nie ma takiej funkcji

No tak, błąd. Nasz obiekt nie ma swojej indywidualnej funkcji fly(). Prototyp naszych obiektów także jej nie posiada, a jego prototypem jest już prototyp obiektów.

object-prorotype

Naszym zadaniem jest więc sprawić, by prototypem nadrzędnym dla prototypu EnemyShoot był prototyp Enemy, dzięki czemu obiekty typu EnemyShoot będą mogły korzystać z wszystkich jego funkcjonalności.

Spróbujmy to zrobić przez podstawienie pod prototyp EnemyShoot prototypu Enemy:


function EnemyShoot(name, x, y) {
    this.name = name;
    this.x = x;
    this.y = y;
    this.type = "shooter";
}

EnemyShoot.prototype = Enemy.prototype;

EnemyShoot.prototype.shoot = function() {
    return this.name + " strzelam!";
}

const enemyS = new EnemyShoot("Shooter", 10, 20);
console.log(enemyShoot.fly()); //Shooter lecę
console.log(enemyShoot.shoot()); //Shooter strzelam!

Problem z powyższym rozwiązaniem jest taki, że skoro zrównaliśmy do siebie obydwa prototypy, stały się one tym samym obiektem. Jeżeli dla EnemyShoot dodamy funkcję shoot(), trafi ona też do obiektów Enemy, a przecież nie wszyscy przeciwnicy powinni strzelać.


const enemyNormal = new Enemy("Normalny", 10, 20);
enemyNormal.shoot(); //Normalny strzelam

const enemyS = new EnemyShoot("Shooter", 10, 20);
enemyShoot.shoot(); //Shooter strzelam

Rozwiązaniem nie jest zrównanie prototypów, a stworzenie nowego obiektu na bazie innego prototypu. Można to zrobić na kilka sposobów:


EnemyShoot.prototype = Object.create(Enemy.prototype);

//lub
EnemyShoot.prototype = Object.assign({}, Enemy.prototype);

//lub
EnemyShoot.prototype = Object.create(...Enemy.prototype);

function EnemyShoot(name, x, y) {
    this.name = name;
    this.x = x;
    this.y = y;
    this.type = "shooter";
}

EnemyShoot.prototype = Object.create(Enemy.prototype);

EnemyShoot.prototype.shoot = function() {
    return this.name + " strzelam";
}


const enemyNormal = new Enemy("Normalny");
console.log(enemyNormal.fly()); //Normalny latam
console.log(enemyNormal.shoot()); //błąd - nie ma takiej funkcji

const enemyS = new EnemyShoot("Shooter");
console.log(enemyShoot.fly()); //Shooter latam
console.log(enemyShoot.shoot()); //Shooter strzelam

Wszystko działa praktycznie idealnie, ale została malutka rzecz. Gdy tworzysz konstruktor dla obiektów, ich prototyp z automatu dostanie dwie właściwości. Jedna wskazuje na prototyp tego obiektu, natomiast druga - constructor - wskazuje na funkcję na bazie której został stworzony dany obiekt.

W powyższym kodzie stworzyliśmy prototyp EnemyShoot na bazie prototypu Enemy. Musimy dodatkowo zmienić konstruktor, który został z prototypu Enemy:


function EnemyShoot(name, x, y) {
    this.name = name;
    this.x = x;
    this.y = y;
    this.type = "shooter";
}

EnemyShoot.prototype = Object.create(Enemy.prototype);
EnemyShoot.prototype.constructor = EnemyShoot;

Od tej pory typ obiektów EnemyShoot dziedziczy po obiektach typu Enemy.


function Enemy(name, x, y) {
    this.name = name;
    this.x = x;
    this.y = y;
    console.log("Tworzę przeciwnika: " + this.name);
}

Enemy.prototype.fly = function() {
    return this.name + " latam";
}

function EnemyShoot(name, x, y) {
    this.name = name;
    this.x = x;
    this.y = y;
    this.type = "shooter";
}

EnemyShoot.prototype = Object.create(Enemy.prototype);
EnemyShoot.prototype.constructor = EnemyShoot;

EnemyShoot.prototype.shoot = function() {
    return this.name + " strzelam";
}


const enemyNormal = new Enemy("Normalny");
console.log(enemyNormal.fly()); //Normalny latam
console.log(enemyNormal.shoot()); //błąd - nie ma takiej funkcji

const enemyS = new EnemyShoot("Shooter");
console.log(enemyShoot.fly()); //Shooter latam
console.log(enemyShoot.shoot()); //Shooter strzelam

Odwoływanie się do konstruktora ojca

Wyobraź sobie, że w konstruktorze Enemy są przeprowadzane skomplikowane wyliczenia, które chcielibyśmy wykonywać także w konstruktorze EnemyShoot:


function Enemy(name, x, y) {
    this.name = name;
    this.x = x;
    this.y = y;
    this.speed = Math.random() * 3;
    //...tutaj 20 linii skomplikowanych wyliczeń dla naszego przeciwnika...
    console.log("Tworzę przeciwnika: " + this.name);
}

function EnemyShoot(name, x, y) {
    //tutaj chcemy odpalić kod z powyższego konstruktora Enemy
    //plus coś dodatkowego np.:
    this.type = "shooter";
}

Jak to zrobić? Wystarczyłoby wewnątrz funkcji EnemyShoot() odpalić funkcję Enemy(), co by wykonało jej kod. Musimy przy okazji sprawić, by this wskazywało na EnemyShoot.

Rozszerzamy konstruktor

Po tym przydługim odejściu od tematu, wracamy do naszych obiektów:


function Enemy(name, x, y) {
    ...
}

function EnemyShoot(name, x, y) {
    Enemy.call(this, name, x, y); //Enemy wymaga 3 parametrów
    this.type = "shooter";
}

const shooter = new EnemyShoot("Shooter"); //Tworzę przeciwnika: Shooter

Idąc za ciosem, podobnie chcielibyśmy nadpisać dla EnemyShoot funkcję fly():


function EnemyShoot(name, x, y) {
    Enemy.call(this, name, x, y);
    this.type = "shooter";
}

EnemyShoot.prototype = Object.create(Enemy.prototype);
EnemyShoot.prototype.constructor = EnemyShoot;

EnemyShoot.prototype.fly = function() {
    const text = Enemy.prototype.fly.call(this); //tamta funkcja nie ma parametrów
    return text + " i czasami strzelam!!!";
}

const enemyNormal = new Enemy("Normalny");
console.log(enemyNormal.fly()); //Normalny latam

const enemyS = new EnemyShoot("Shooter");
console.log(enemyShoot.fly()); //Shooter latam i czasami strzelam!!!

instanceof

Ostatnią rzeczą, jaką poznamy w tym rozdziale, to sprawdzanie, jakiego typu jest konkretny obiekt. Wystarczy użyć tutaj operatora instanceof:


const enemyNormal = new Enemy("Normalny");
enemyNormal.fly(); //Normalny latam

const enemyS = new EnemyShoot("Shooter");
console.log(enemyShoot.fly()); //Shooter latam i czasami strzelam!!!

console.log(enemyNormal instanceof Enemy); //true
console.log(enemyS instanceof EnemyShoot); //true
console.log(enemyNormal instanceof Object); //true
console.log(enemyS instanceof Object); //true

const ob = {
    name : "Marcin"
}

console.log(ob instanceof Object); //true
console.log(ob instanceof Enemy); //false
console.log(ob instanceof EnemyShoot); //false

Czemu obiekty simple i shooter są instancjami Object? Wynika to z faktu, że Enemy i EnemyShoot dziedziczą po Object. Ale już nasz obiekt ob nie ma żadnego powiązania z naszymi klasami więc instanceof zwraca false.

Gdybyś chciał sprawdzić czy obiekt shooter jest typu EnemyShoot, wystarczy sprawdzić jego konstruktor:


const enemyS = new EnemyShoot("Shooter");
enemyShoot.constructor === EnemyShoot;

Możemy też zmienić działanie tej instrukcji poprzez zastosowanie odpowiedniego symbolu.

Podsumowanie

Jak widzisz, trochę kodu musieliśmy napisać. Z jednej strony musimy pamiętać o składni prototypów, z drugiej zmuszeni jesteśmy do używania call() i apply(). Po drugim, trzecim użyciu da się przyzwyczaić...

Jeżeli nie podoba ci się powyższe podejście - nic straconego. W dzisiejszych czasach możemy korzystać z innej - o wiele przyjemniejszej w użyciu składni class, którą nie tylko bardzo upraszcza powyższy zapis, ale i nieco rozbudowuje nasze możliwości.

Ciekawy tutorial na powyższe tematy znajdziesz pod adresem: https://www.youtube.com/watch?v=MiKdRJc4ooE

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.