Konstruktor - dziedziczenie
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.

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