Kurs JavaScript i TypeScript · Moduł 2: Funkcje, tablice i obiekty

Prototypy i dziedziczenie prototypowe

15 min czytania
W tej lekcji12

W Parku Jurajskim efektywne zarządzanie różnymi gatunkami dinozaurów wymaga zrozumienia podobieństw między nimi i możliwości dziedziczenia cech. JavaScript używa prototypów do implementacji mechanizmu dziedziczenia - jest to jeden z najbardziej unikalnych i potężnych aspektów tego języka. Zrozumienie prototypów i dziedziczenia prototypowego jest kluczowe dla pisania wydajnego, elastycznego kodu JavaScript.

Czym są prototypy?

W JavaScript każdy obiekt ma wewnętrzną właściwość zwaną prototypem, która jest referencją do innego obiektu. Gdy próbujemy uzyskać dostęp do właściwości obiektu, JavaScript najpierw sprawdza, czy sam obiekt posiada taką właściwość. Jeśli nie, sprawdza prototyp obiektu, następnie prototyp prototypu, itd., tworząc tzw. łańcuch prototypów.

1// Tworzymy prosty obiekt
2const dinosaur = {
3  species: "Tyrannosaurus",
4  makesSound: true,
5  roar() {
6    return "ROOOOAR!";
7  }
8};
9
10// Tworzymy nowy obiekt, który dziedziczy po obiekcie dinosaur
11const rex = Object.create(dinosaur);
12rex.name = "Rex";
13rex.age = 8;
14
15// rex ma dostęp do swoich własnych właściwości
16console.log(rex.name); // "Rex"
17console.log(rex.age); // 8
18
19// rex ma również dostęp do właściwości i metod dinosaur
20console.log(rex.species); // "Tyrannosaurus"
21console.log(rex.makesSound); // true
22console.log(rex.roar()); // "ROOOOAR!"
23
24// Sprawdźmy, czy rex ma własną właściwość 'species' (nie ma)
25console.log(rex.hasOwnProperty('species')); // false
26
27// Ale ma własną właściwość 'name'
28console.log(rex.hasOwnProperty('name')); // true

W powyższym przykładzie rex nie ma własnej właściwości species, ale dostęp do rex.species działa, ponieważ JavaScript szuka tej właściwości w prototypie rex, którym jest obiekt dinosaur.

Łańcuch prototypów

Prototypy tworzą łańcuch, który JavaScript przeszukuje, gdy próbujemy uzyskać dostęp do właściwości. Ten mechanizm nazywamy łańcuchem prototypów (prototype chain).

1// Podstawowy obiekt przodek
2const animal = {
3  isAlive: true,
4  eat() {
5    return `${this.name} je.`;
6  }
7};
8
9// Obiekt dinozaur dziedziczący po animal
10const dinosaur = Object.create(animal);
11dinosaur.isExtinct = false;
12dinosaur.roar = function() {
13  return `${this.name} ryczy!`;
14};
15
16// Konkretny gatunek dinozaura dziedziczący po dinosaur
17const tyrannosaurus = Object.create(dinosaur);
18tyrannosaurus.name = "Tyrannosaurus";
19tyrannosaurus.diet = "carnivore";
20tyrannosaurus.length = 12; // metry
21
22// tyrannosaurus ma dostęp do właściwości wszystkich prototypów w łańcuchu
23console.log(tyrannosaurus.name); // "Tyrannosaurus" (własna właściwość)
24console.log(tyrannosaurus.isExtinct); // false (z dinosaur)
25console.log(tyrannosaurus.isAlive); // true (z animal)
26console.log(tyrannosaurus.eat()); // "Tyrannosaurus je." (metoda z animal, this odwołuje się do tyrannosaurus)
27console.log(tyrannosaurus.roar()); // "Tyrannosaurus ryczy!" (metoda z dinosaur)

Konstruktory i prototypy

W praktyce, prototypy są najczęściej używane z konstruktorami i klasami do implementowania dziedziczenia w JavaScript.

Funkcje konstruktora i prototypy

Każda funkcja konstruktora ma właściwość prototype, która jest używana jako prototyp dla obiektów tworzonych za pomocą tej funkcji z operatorem new.

1// Funkcja konstruktora dla Dinosaur
2function Dinosaur(name, species, diet) {
3  this.name = name;
4  this.species = species;
5  this.diet = diet;
6  this.isExtinct = false;
7}
8
9// Dodajemy metody do prototypu Dinosaur
10Dinosaur.prototype.roar = function() {
11  const volume = this.diet === "carnivore" ? "głośno" : "cicho";
12  return `${this.name} ryczy ${volume}!`;
13};
14
15Dinosaur.prototype.describe = function() {
16  return `${this.name} to ${this.species}, który jest ${this.diet === "carnivore" ? "mięsożerny" : "roślinożerny"}.`;
17};
18
19// Tworzymy instancje
20const trex = new Dinosaur("Rex", "Tyrannosaurus", "carnivore");
21const tricera = new Dinosaur("Trixie", "Triceratops", "herbivore");
22
23console.log(trex.roar()); // "Rex ryczy głośno!"
24console.log(tricera.roar()); // "Trixie ryczy cicho!"
25console.log(trex.describe()); // "Rex to Tyrannosaurus, który jest mięsożerny."
26
27// Wszystkie instancje współdzielą te same metody prototypu
28console.log(trex.roar === tricera.roar); // true

W tym przykładzie, metody roar i describe są definiowane na prototypie funkcji konstruktora Dinosaur. Wszystkie instancje utworzone za pomocą new Dinosaur() współdzielą te same metody, co jest znacznie bardziej efektywne niż definiowanie tych metod w konstruktorze dla każdej instancji osobno.

Sprawdzanie prototypu obiektu

Możemy sprawdzić prototyp obiektu za pomocą kilku metod:

1// Używając Object.getPrototypeOf()
2console.log(Object.getPrototypeOf(trex) === Dinosaur.prototype); // true
3
4// Używając __proto__ (właściwość przestarzała, nie zalecana w kodzie produkcyjnym)
5console.log(trex.__proto__ === Dinosaur.prototype); // true
6
7// Sprawdzanie, czy obiekt jest instancją konstruktora
8console.log(trex instanceof Dinosaur); // true
9console.log(trex instanceof Object); // true (wszystkie obiekty dziedziczą po Object.prototype)

Dziedziczenie między konstruktorami

Możemy implementować dziedziczenie między funkcjami konstruktora, łącząc ich prototypy:

1// Funkcja konstruktora dla Animal (klasa bazowa)
2function Animal(name) {
3  this.name = name;
4  this.isAlive = true;
5}
6
7Animal.prototype.eat = function(food) {
8  return `${this.name} je ${food}.`;
9};
10
11Animal.prototype.sleep = function() {
12  return `${this.name} śpi.`;
13};
14
15// Funkcja konstruktora dla Dinosaur (klasa pochodna)
16function Dinosaur(name, species, diet) {
17  // Wywołujemy konstruktor klasy bazowej
18  Animal.call(this, name);
19
20  // Dodajemy właściwości specyficzne dla Dinosaur
21  this.species = species;
22  this.diet = diet;
23  this.isExtinct = false;
24}
25
26// Ustawiamy prototyp Dinosaur, aby dziedziczył po Animal.prototype
27Dinosaur.prototype = Object.create(Animal.prototype);
28
29// Przywracamy właściwą właściwość konstruktora
30Dinosaur.prototype.constructor = Dinosaur;
31
32// Dodajemy metody specyficzne dla Dinosaur
33Dinosaur.prototype.roar = function() {
34  const volume = this.diet === "carnivore" ? "głośno" : "cicho";
35  return `${this.name} ryczy ${volume}!`;
36};
37
38// Tworzymy instancję Dinosaur
39const velociraptor = new Dinosaur("Blue", "Velociraptor", "carnivore");
40
41// Mamy dostęp do metod z Animal
42console.log(velociraptor.eat("mięso")); // "Blue je mięso."
43console.log(velociraptor.sleep()); // "Blue śpi."
44
45// Oraz do metod specyficznych dla Dinosaur
46console.log(velociraptor.roar()); // "Blue ryczy głośno!"
47
48// Sprawdzamy łańcuch prototypów
49console.log(velociraptor instanceof Dinosaur); // true
50console.log(velociraptor instanceof Animal); // true
51console.log(velociraptor instanceof Object); // true

Ten wzorzec implementacji dziedziczenia między konstruktorami jest często nazywany "dziedziczeniem pseudoklasycznym", ponieważ emuluje dziedziczenie klasowe w języku, który oryginalnie bazuje na prototypach.

Dziedziczenie wielopoziomowe

Możemy tworzyć wielopoziomowe hierarchie dziedziczenia, łącząc łańcuchy prototypów:

1// Klasa bazowa Animal
2function Animal(name) {
3  this.name = name;
4  this.isAlive = true;
5}
6
7Animal.prototype.breathe = function() {
8  return `${this.name} oddycha.`;
9};
10
11// Klasa Dinosaur dziedzicząca po Animal
12function Dinosaur(name, period) {
13  Animal.call(this, name);
14  this.period = period;
15  this.extinct = true;
16}
17
18Dinosaur.prototype = Object.create(Animal.prototype);
19Dinosaur.prototype.constructor = Dinosaur;
20
21Dinosaur.prototype.roar = function() {
22  return `${this.name} ryczy!`;
23};
24
25// Klasa Theropod (teropod) dziedzicząca po Dinosaur
26function Theropod(name, period, speed) {
27  Dinosaur.call(this, name, period);
28  this.speed = speed; // km/h
29  this.bipedal = true;
30}
31
32Theropod.prototype = Object.create(Dinosaur.prototype);
33Theropod.prototype.constructor = Theropod;
34
35Theropod.prototype.run = function() {
36  return `${this.name} biegnie z prędkością ${this.speed} km/h!`;
37};
38
39// Klasa Tyrannosaurus dziedzicząca po Theropod
40function Tyrannosaurus(name, size) {
41  Theropod.call(this, name, "Late Cretaceous", 30);
42  this.size = size;
43  this.teeth = 60;
44}
45
46Tyrannosaurus.prototype = Object.create(Theropod.prototype);
47Tyrannosaurus.prototype.constructor = Tyrannosaurus;
48
49Tyrannosaurus.prototype.bite = function() {
50  return `${this.name} gryzie z ogromną siłą dzięki ${this.teeth} zębom!`;
51};
52
53// Tworzymy instancję Tyrannosaurus
54const rex = new Tyrannosaurus("Rex", "bardzo duży");
55
56// Mamy dostęp do metod wszystkich prototypów w łańcuchu
57console.log(rex.breathe()); // "Rex oddycha." (z Animal)
58console.log(rex.roar()); // "Rex ryczy!" (z Dinosaur)
59console.log(rex.run()); // "Rex biegnie z prędkością 30 km/h!" (z Theropod)
60console.log(rex.bite()); // "Rex gryzie z ogromną siłą dzięki 60 zębom!" (z Tyrannosaurus)
61
62// Sprawdzamy łańcuch prototypów
63console.log(rex instanceof Tyrannosaurus); // true
64console.log(rex instanceof Theropod); // true
65console.log(rex instanceof Dinosaur); // true
66console.log(rex instanceof Animal); // true
67console.log(rex instanceof Object); // true

Prototypy a klasy w ES6+

ES6 wprowadził składnię klas, która pod spodem używa prototypów, ale oferuje bardziej przyjazną, podobną do klasycznej obiektowości składnię:

1// Klasa bazowa Animal
2class Animal {
3  constructor(name) {
4    this.name = name;
5    this.isAlive = true;
6  }
7
8  eat(food) {
9    return `${this.name} je ${food}.`;
10  }
11
12  sleep() {
13    return `${this.name} śpi.`;
14  }
15}
16
17// Klasa Dinosaur dziedzicząca po Animal
18class Dinosaur extends Animal {
19  constructor(name, species, diet) {
20    super(name); // Wywołanie konstruktora klasy bazowej
21    this.species = species;
22    this.diet = diet;
23    this.isExtinct = false;
24  }
25
26  roar() {
27    const volume = this.diet === "carnivore" ? "głośno" : "cicho";
28    return `${this.name} ryczy ${volume}!`;
29  }
30
31  // Nadpisujemy metodę eat z klasy bazowej
32  eat(food) {
33    if (this.diet === "carnivore" && food === "mięso") {
34      return `${this.name} z apetytem pożera ${food}!`;
35    } else if (this.diet === "herbivore" && food === "rośliny") {
36      return `${this.name} spokojnie skubie ${food}.`;
37    } else {
38      return `${this.name} nie jest zainteresowany jedzeniem ${food}.`;
39    }
40  }
41}
42
43// Klasa pochodna Velociraptor dziedzicząca po Dinosaur
44class Velociraptor extends Dinosaur {
45  constructor(name, speed) {
46    super(name, "Velociraptor", "carnivore");
47    this.speed = speed;
48    this.packHunter = true;
49  }
50
51  hunt() {
52    return `${this.name} poluje w grupie z prędkością ${this.speed} km/h!`;
53  }
54}
55
56// Tworzymy instancje
57const blue = new Velociraptor("Blue", 70);
58
59console.log(blue.eat("mięso")); // "Blue z apetytem pożera mięso!"
60console.log(blue.sleep()); // "Blue śpi."
61console.log(blue.roar()); // "Blue ryczy głośno!"
62console.log(blue.hunt()); // "Blue poluje w grupie z prędkością 70 km/h!"
63
64// Sprawdzamy łańcuch prototypów
65console.log(blue instanceof Velociraptor); // true
66console.log(blue instanceof Dinosaur); // true
67console.log(blue instanceof Animal); // true
68console.log(blue instanceof Object); // true

Składnia klas w ES6 jest tylko "cukrem składniowym" (syntactic sugar) nad istniejącym mechanizmem prototypów w JavaScript. Pod spodem, JavaScript nadal używa prototypów do implementacji dziedziczenia.

Wielodziedziczenie i miksy (mixins)

JavaScript nie obsługuje bezpośrednio wielodziedziczenia (dziedziczenia po wielu klasach jednocześnie), ale możemy używać wzorca "mixins" do uzyskania podobnego efektu:

1// Klasa bazowa
2class Dinosaur {
3  constructor(name, species) {
4    this.name = name;
5    this.species = species;
6  }
7
8  roar() {
9    return `${this.name} ryczy!`;
10  }
11}
12
13// Mixin dla zachowań drapieżnych (carnivore behavior)
14const CarnivoreMixin = {
15  hunt(prey) {
16    return `${this.name} poluje na ${prey}!`;
17  },
18
19  bite() {
20    return `${this.name} gryzie z dużą siłą!`;
21  }
22};
23
24// Mixin dla zachowań związanych ze stadnym polowaniem (pack hunting behavior)
25const PackHunterMixin = {
26  coordinateAttack() {
27    return `${this.name} koordynuje atak stada!`;
28  },
29
30  communicateWithPack() {
31    return `${this.name} komunikuje się z innymi członkami stada.`;
32  }
33};
34
35// Mixin dla zachowań związanych z terytorium (territorial behavior)
36const TerritorialMixin = {
37  markTerritory() {
38    return `${this.name} zaznacza swoje terytorium.`;
39  },
40
41  defendTerritory(intruder) {
42    return `${this.name} broni swojego terytorium przed ${intruder}!`;
43  }
44};
45
46// Funkcja do aplikowania mixinów do klasy
47function applyMixins(targetClass, ...mixins) {
48  mixins.forEach(mixin => {
49    Object.getOwnPropertyNames(mixin).forEach(name => {
50      if (name !== 'constructor') {
51        targetClass.prototype[name] = mixin[name];
52      }
53    });
54  });
55}
56
57// Klasa Velociraptor z kilkoma mixinami
58class Velociraptor extends Dinosaur {
59  constructor(name) {
60    super(name, "Velociraptor");
61    this.diet = "carnivore";
62    this.speed = 70; // km/h
63  }
64}
65
66// Aplikujemy mixiny do Velociraptor
67applyMixins(Velociraptor, CarnivoreMixin, PackHunterMixin, TerritorialMixin);
68
69// Tworzymy instancję Velociraptor
70const blue = new Velociraptor("Blue");
71
72// Teraz blue ma dostęp do metod z różnych mixinów
73console.log(blue.roar()); // "Blue ryczy!" (z Dinosaur)
74console.log(blue.hunt("Gallimimus")); // "Blue poluje na Gallimimus!" (z CarnivoreMixin)
75console.log(blue.bite()); // "Blue gryzie z dużą siłą!" (z CarnivoreMixin)
76console.log(blue.coordinateAttack()); // "Blue koordynuje atak stada!" (z PackHunterMixin)
77console.log(blue.defendTerritory("Tyrannosaurus")); // "Blue broni swojego terytorium przed Tyrannosaurus!" (z TerritorialMixin)

Mixiny pozwalają na "mieszanie" funkcjonalności z różnych źródeł bez konieczności stosowania wielodziedziczenia, którego JavaScript formalnie nie obsługuje.

Object.prototype

Wszystkie obiekty w JavaScript ostatecznie dziedziczą po Object.prototype. Zawiera on wiele przydatnych metod, takich jak toString(), hasOwnProperty(), valueOf(), itp.

1const dino = { name: "Rex" };
2
3// dino dziedziczy metody z Object.prototype
4console.log(dino.toString()); // "[object Object]"
5console.log(dino.hasOwnProperty("name")); // true
6console.log(dino.hasOwnProperty("age")); // false
7
8// Możemy nadpisać metody z Object.prototype
9dino.toString = function() {
10  return `Dinozaur: ${this.name}`;
11};
12
13console.log(dino.toString()); // "Dinozaur: Rex"

Dziedziczenie prototypowe w praktyce: System dinozaurów Parku Jurajskiego

Poniżej znajduje się przykład bardziej złożonego systemu zarządzania dinozaurami, wykorzystującego dziedziczenie prototypowe:

1// Podstawowa klasa dla wszystkich istot w parku
2class Creature {
3  constructor(name, age) {
4    this.name = name;
5    this.age = age;
6    this.health = 100;
7    this.alive = true;
8  }
9
10  eat(food) {
11    this.health = Math.min(100, this.health + 10);
12    return `${this.name} je ${food}. Zdrowie: ${this.health}%`;
13  }
14
15  sleep() {
16    this.health = Math.min(100, this.health + 5);
17    return `${this.name} śpi. Zdrowie: ${this.health}%`;
18  }
19
20  damage(amount) {
21    this.health = Math.max(0, this.health - amount);
22    if (this.health === 0 && this.alive) {
23      this.alive = false;
24      return `${this.name} nie żyje!`;
25    }
26    return `${this.name} otrzymuje ${amount} obrażeń. Pozostałe zdrowie: ${this.health}%`;
27  }
28}
29
30// Klasa dla dinozaurów, dziedzicząca po Creature
31class Dinosaur extends Creature {
32  constructor(name, species, age, diet) {
33    super(name, age);
34    this.species = species;
35    this.diet = diet;
36    this.containmentLevel = this.calculateContainmentLevel();
37  }
38
39  calculateContainmentLevel() {
40    if (this.diet === "carnivore") {
41      return this.age > 5 ? "Maximum" : "High";
42    } else {
43      return this.age > 10 ? "Medium" : "Standard";
44    }
45  }
46
47  roar() {
48    const volume = this.diet === "carnivore" ? "głośno" : "cicho";
49    return `${this.name} ryczy ${volume}!`;
50  }
51
52  getStatus() {
53    return `
54Dinozaur: ${this.name} (${this.species})
55Wiek: ${this.age} lat
56Dieta: ${this.diet === "carnivore" ? "Mięsożerna" : "Roślinożerna"}
57Zdrowie: ${this.health}%
58Status: ${this.alive ? "Żywy" : "Nieżywy"}
59Poziom bezpieczeństwa: ${this.containmentLevel}
60`;
61  }
62}
63
64// Klasa dla mięsożernych dinozaurów (drapieżników)
65class Carnivore extends Dinosaur {
66  constructor(name, species, age, attackPower) {
67    super(name, species, age, "carnivore");
68    this.attackPower = attackPower;
69    this.huntingSuccess = 0.7; // 70% szans na udane polowanie
70  }
71
72  hunt(prey) {
73    if (!this.alive) return `${this.name} nie żyje, nie może polować.`;
74    if (!prey.alive) return `${prey.name} jest już martwy.`;
75
76    const huntSuccess = Math.random() < this.huntingSuccess;
77
78    if (huntSuccess) {
79      const damage = Math.floor(this.attackPower * (0.7 + Math.random() * 0.6)); // 70-130% mocy ataku
80      const result = prey.damage(damage);
81      this.eat("mięso");
82      return `${this.name} skutecznie poluje na ${prey.name}! ${result}`;
83    } else {
84      return `${this.name} próbuje upolować ${prey.name}, ale nie udaje się.`;
85    }
86  }
87
88  clawAttack(target) {
89    if (!this.alive) return `${this.name} nie żyje, nie może atakować.`;
90    if (!target.alive) return `${target.name} jest już martwy.`;
91
92    const damage = Math.floor(this.attackPower * 0.6);
93    const result = target.damage(damage);
94    return `${this.name} atakuje ${target.name} pazurami! ${result}`;
95  }
96
97  biteAttack(target) {
98    if (!this.alive) return `${this.name} nie żyje, nie może atakować.`;
99    if (!target.alive) return `${target.name} jest już martwy.`;
100
101    const damage = Math.floor(this.attackPower * 1.2);
102    const result = target.damage(damage);
103    return `${this.name} gryzie ${target.name}! ${result}`;
104  }
105}
106
107// Klasa dla roślinożernych dinozaurów
108class Herbivore extends Dinosaur {
109  constructor(name, species, age, defenseRating) {
110    super(name, species, age, "herbivore");
111    this.defenseRating = defenseRating;
112    this.fleeSuccess = 0.6; // 60% szans na udaną ucieczkę
113  }
114
115  grazeVegetation() {
116    if (!this.alive) return `${this.name} nie żyje, nie może jeść.`;
117
118    this.health = Math.min(100, this.health + 15);
119    return `${this.name} skubie rośliny. Zdrowie wzrasta do ${this.health}%.`;
120  }
121
122  flee(predator) {
123    if (!this.alive) return `${this.name} nie żyje, nie może uciekać.`;
124
125    const escapeSuccess = Math.random() < this.fleeSuccess;
126
127    if (escapeSuccess) {
128      this.health = Math.max(0, this.health - 5); // Ucieczka kosztuje energię
129      return `${this.name} skutecznie ucieka przed ${predator.name}!`;
130    } else {
131      return `${this.name} próbuje uciec przed ${predator.name}, ale nie udaje się.`;
132    }
133  }
134
135  // Defensywna metoda - zmniejsza otrzymane obrażenia
136  damage(amount) {
137    // Roślinożercy otrzymują mniej obrażeń dzięki defenseRating
138    const reducedDamage = Math.max(0, amount - this.defenseRating);
139    return super.damage(reducedDamage);
140  }
141}
142
143// Specyficzne gatunki dinozaurów
144class Tyrannosaurus extends Carnivore {
145  constructor(name, age) {
146    super(name, "Tyrannosaurus Rex", age, 35);
147    this.intimidationFactor = 9;
148    this.territorialRange = 5; // km
149  }
150
151  intimidate(target) {
152    return `${this.name} wydaje ogłuszający ryk, który przeraża ${target.name}!`;
153  }
154
155  // Nadpisujemy metodę roar
156  roar() {
157    return `${this.name} wydaje potężny, ogłuszający ryk, który rozchodzi się na kilometry!`;
158  }
159}
160
161class Velociraptor extends Carnivore {
162  constructor(name, age) {
163    super(name, "Velociraptor", age, 20);
164    this.speed = 70; // km/h
165    this.intelligence = 9;
166    this.packHunting = true;
167  }
168
169  coordinateAttack(targets) {
170    if (!Array.isArray(targets) || targets.length < 2) {
171      return `${this.name} potrzebuje co najmniej dwóch celów do koordynacji ataku.`;
172    }
173
174    let results = `${this.name} koordynuje atak stada na ${targets.map(t => t.name).join(', ')}!\n`;
175    targets.forEach(target => {
176      const damage = Math.floor(this.attackPower * 1.5); // Wzmocniony atak stadny
177      results += target.damage(damage) + '\n';
178    });
179
180    return results;
181  }
182
183  ambush(target) {
184    const damage = Math.floor(this.attackPower * 2); // Podwójne obrażenia z zasadzki
185    const result = target.damage(damage);
186    return `${this.name} urządza zasadzkę na ${target.name}! ${result}`;
187  }
188}
189
190class Triceratops extends Herbivore {
191  constructor(name, age) {
192    super(name, "Triceratops", age, 25);
193    this.hornStrength = 30;
194  }
195
196  chargeAttack(target) {
197    const damage = this.hornStrength + Math.floor(Math.random() * 10);
198    const result = target.damage(damage);
199    return `${this.name} szarżuje na ${target.name} swoimi rogami! ${result}`;
200  }
201
202  defendFromAttack(attacker) {
203    const counterDamage = Math.floor(this.hornStrength * 0.5);
204    const result = attacker.damage(counterDamage);
205    return `${this.name} broni się przed ${attacker.name} używając swoich rogów! ${result}`;
206  }
207}
208
209class Brachiosaurus extends Herbivore {
210  constructor(name, age) {
211    super(name, "Brachiosaurus", age, 15);
212    this.height = 13; // metry
213    this.tailLength = 9; // metry
214  }
215
216  reachHighVegetation() {
217    this.health = Math.min(100, this.health + 20);
218    return `${this.name} sięga swoją długą szyją po liście wysokich drzew. Zdrowie wzrasta do ${this.health}%.`;
219  }
220
221  tailWhip(target) {
222    const damage = Math.floor(this.tailLength * 2);
223    const result = target.damage(damage);
224    return `${this.name} uderza ${target.name} swoim ogromnym ogonem! ${result}`;
225  }
226}
227
228// Symulacja parku jurajskiego
229function runJurassicParkSimulation() {
230  console.log("=== SYMULACJA PARKU JURAJSKIEGO ===\n");
231
232  // Tworzenie dinozaurów
233  const rex = new Tyrannosaurus("Rex", 8);
234  const blue = new Velociraptor("Blue", 4);
235  const delta = new Velociraptor("Delta", 4);
236  const trixie = new Triceratops("Trixie", 12);
237  const bronty = new Brachiosaurus("Bronty", 25);
238
239  // Wyświetlanie informacji o dinozaurach
240  console.log(rex.getStatus());
241  console.log(blue.getStatus());
242  console.log(trixie.getStatus());
243  console.log(bronty.getStatus());
244
245  console.log("\n=== INTERAKCJE DINOZAURÓW ===\n");
246
247  // Interakcje między dinozaurami
248  console.log(rex.roar());
249  console.log(blue.roar());
250  console.log(trixie.grazeVegetation());
251  console.log(bronty.reachHighVegetation());
252
253  console.log("\n=== POJEDYNEK: REX VS TRIXIE ===\n");
254
255  console.log(rex.intimidate(trixie));
256  console.log(trixie.flee(rex));
257  console.log(rex.hunt(trixie));
258  console.log(trixie.defendFromAttack(rex));
259  console.log(rex.biteAttack(trixie));
260
261  console.log("\n=== POLOWANIE STADA VELOCIRAPTORÓW ===\n");
262
263  console.log(blue.coordinateAttack([trixie, bronty]));
264  console.log(delta.ambush(bronty));
265  console.log(bronty.tailWhip(delta));
266
267  console.log("\n=== STAN DINOZAURÓW PO INTERAKCJACH ===\n");
268
269  console.log(rex.getStatus());
270  console.log(blue.getStatus());
271  console.log(delta.getStatus());
272  console.log(trixie.getStatus());
273  console.log(bronty.getStatus());
274}
275
276// Uruchomienie symulacji
277runJurassicParkSimulation();

Dziedziczenie prototypowe vs klasyczne dziedziczenie

Dziedziczenie prototypowe w JavaScript różni się od klasycznego dziedziczenia w innych językach obiektowych:

  1. Dynamiczność - prototypy mogą być modyfikowane w czasie wykonania programu, podczas gdy klasy są zwykle statyczne.
  2. Łańcuch prototypów - właściwości są wyszukiwane w łańcuchu prototypów, podczas gdy w klasycznym dziedziczeniu metody są rozwiązywane przez hierarchię klas.
  3. Brak klas (przed ES6) - JavaScript tradycyjnie nie miał formalnej koncepcji klas, a jedynie funkcje konstruktora i prototypy.
  4. Dziedziczenie pojedyncze - JavaScript obsługuje tylko dziedziczenie pojedyncze (każdy obiekt ma tylko jeden prototyp), podczas gdy niektóre języki obsługują wielodziedziczenie.

Dobre praktyki przy pracy z prototypami

  1. Zrozumienie mechanizmu - ważne jest zrozumienie, jak działa łańcuch prototypów, aby skutecznie korzystać z dziedziczenia.
  2. Korzystanie z klas (ES6+) - dla większości przypadków użycia składnia klas jest bardziej czytelna i łatwiejsza w utrzymaniu.
  3. Unikanie modyfikacji wbudowanych prototypów - modyfikacja prototypów wbudowanych obiektów (np. Object.prototype, Array.prototype) może prowadzić do nieoczekiwanych konfliktów.
  4. Stosowanie Object.create() dla dziedziczenia - przy używaniu tradycyjnego podejścia, Object.create() jest czystym sposobem implementacji dziedziczenia prototypowego.
  5. Kompozycja nad dziedziczeniem - czasami lepiej jest używać kompozycji (łączenia obiektów) zamiast głębokiego dziedziczenia, aby uniknąć problemów z hierarchią.

Podsumowanie

Prototypy i dziedziczenie prototypowe są fundamentalnymi koncepcjami w JavaScript. Nawet gdy używamy nowoczesnej składni klas (ES6+), ważne jest zrozumienie leżącego u podstaw mechanizmu prototypów, aby w pełni wykorzystać możliwości języka. W naszym Parku Jurajskim dziedziczenie prototypowe pomaga nam modelować różne typy dinozaurów i ich zachowań w spójny, wydajny sposób.

Główne koncepcje, które omawialiśmy, to:

  1. Prototypy - mechanizm, dzięki któremu obiekty mogą dziedziczyć właściwości i metody od innych obiektów.
  2. Łańcuch prototypów - sekwencja obiektów połączonych przez ich prototypy, którą JavaScript przeszukuje, gdy próbuje uzyskać dostęp do właściwości.
  3. Funkcje konstruktora i prototypy - tradycyjny sposób implementacji dziedziczenia w JavaScript.
  4. Klasy ES6+ - nowoczesna składnia dla dziedziczenia, oparta na mechanizmie prototypów.
  5. Mixiny - sposób na łączenie funkcjonalności z różnych źródeł.

Zrozumienie prototypów i dziedziczenia w JavaScript jest kluczem do pisania efektywnego, dobrze zorganizowanego kodu, który może modelować złożone relacje między obiektami, takie jak te w naszym wirtualnym Parku Jurajskim.

Kod do tej lekcji: index.js
1// Prototypy i dziedziczenie - Park Jurajski
2console.log("Prototypy i Dziedziczenie");
3console.log("Zaawansowane koncepcje OOP w JavaScript\n");
4
5// ===========================================
6// 1. PODSTAWY PROTOTYPÓW
7// ===========================================
8console.log("=== 1. Czym są prototypy? ===");
9
10function Dinosaur(name, species) {
11  this.name = name;
12  this.species = species;
13}
14
15// Dodanie metody do prototypu
16Dinosaur.prototype.roar = function() {
17  return `${this.name} says: ROAARRR!`;
18};
19
20Dinosaur.prototype.getInfo = function() {
21  return `${this.name} (${this.species})`;
22};
23
24const trex = new Dinosaur("Rexy", "T-Rex");
25const raptor = new Dinosaur("Blue", "Velociraptor");
26
27console.log("\n", trex.getInfo());
28console.log("   ", trex.roar());
29console.log("\n", raptor.getInfo());
30console.log("   ", raptor.roar());
31
32// Sprawdzenie prototypu
33console.log("\nSprawdzenie prototypu:");
34console.log(`   trex.__proto__ === Dinosaur.prototype: ${trex.__proto__ === Dinosaur.prototype}`);
35console.log(`   raptor.constructor === Dinosaur: ${raptor.constructor === Dinosaur}`);
36
37// ===========================================
38// 2. ŁAŃCUCH PROTOTYPÓW
39// ===========================================
40console.log("\n=== 2. Łańcuch prototypów (Prototype Chain) ===");
41
42console.log("\nŁańcuch prototypów dla trex:");
43console.log(`   trex → Dinosaur.prototype → Object.prototype → null`);
44
45console.log("\nMetody dostępne przez łańcuch:");
46console.log(`   trex.getInfo() (z Dinosaur.prototype): ${trex.getInfo()}`);
47console.log(`   trex.toString() (z Object.prototype): ${trex.toString()}`);
48console.log(`   trex.hasOwnProperty('name'): ${trex.hasOwnProperty('name')}`);
49
50// Sprawdzanie w łańcuchu
51console.log("\ninstanceof:");
52console.log(`   trex instanceof Dinosaur: ${trex instanceof Dinosaur}`);
53console.log(`   trex instanceof Object: ${trex instanceof Object}`);
54
55// ===========================================
56// 3. DZIEDZICZENIE PROTOTYPOWE
57// ===========================================
58console.log("\n=== 3. Dziedziczenie prototypowe ===");
59
60function Animal(name, age) {
61  this.name = name;
62  this.age = age;
63}
64
65Animal.prototype.breathe = function() {
66  return `${this.name} oddycha`;
67};
68
69Animal.prototype.getAge = function() {
70  return `${this.name} ma ${this.age} lat`;
71};
72
73// Carnivore dziedziczy po Animal
74function Carnivore(name, age, huntingSkill) {
75  Animal.call(this, name, age); // Wywołanie konstruktora rodzica
76  this.huntingSkill = huntingSkill;
77}
78
79// Ustawienie dziedziczenia
80Carnivore.prototype = Object.create(Animal.prototype);
81Carnivore.prototype.constructor = Carnivore;
82
83// Dodanie własnych metod
84Carnivore.prototype.hunt = function() {
85  return `${this.name} poluje (skill: ${this.huntingSkill}/10)`;
86};
87
88Carnivore.prototype.roar = function() {
89  return `${this.name} ryczy: ROAARRR!`;
90};
91
92const rex = new Carnivore("Rex", 8, 10);
93
94console.log("\nCarnivore (dziedziczy po Animal):");
95console.log(`   ${rex.getAge()}`); // Z Animal
96console.log(`   ${rex.breathe()}`); // Z Animal
97console.log(`   ${rex.hunt()}`); // Własna metoda
98console.log(`   ${rex.roar()}`); // Własna metoda
99
100// ===========================================
101// 4. KLASY ES6 (SYNTACTIC SUGAR)
102// ===========================================
103console.log("\n=== 4. Klasy ES6 ===");
104
105class DinoBase {
106  constructor(name, species, age) {
107    this.name = name;
108    this.species = species;
109    this.age = age;
110  }
111
112  getInfo() {
113    return `${this.name} (${this.species}) - ${this.age} lat`;
114  }
115
116  breathe() {
117    return `${this.name} oddycha`;
118  }
119}
120
121class CarnivoreClass extends DinoBase {
122  constructor(name, species, age, huntingSkill) {
123    super(name, species, age);
124    this.huntingSkill = huntingSkill;
125    this.type = "Mięsożerca";
126  }
127
128  hunt() {
129    return `${this.name} poluje (skill: ${this.huntingSkill}/10)`;
130  }
131
132  roar() {
133    return `${this.name} ryczy: ROAARRR!`;
134  }
135
136  // Nadpisanie metody z klasy bazowej
137  getInfo() {
138    return `${super.getInfo()} | Typ: ${this.type}`;
139  }
140}
141
142class HerbivoreClass extends DinoBase {
143  constructor(name, species, age, plantPreference) {
144    super(name, species, age);
145    this.plantPreference = plantPreference;
146    this.type = "Roślinożerca";
147  }
148
149  eat() {
150    return `${this.name} je ${this.plantPreference}`;
151  }
152
153  graze() {
154    return `${this.name} spokojnie się pasie`;
155  }
156
157  getInfo() {
158    return `${super.getInfo()} | Typ: ${this.type}`;
159  }
160}
161
162const rexy = new CarnivoreClass("Rexy", "T-Rex", 8, 10);
163const trike = new HerbivoreClass("Trike", "Triceratops", 7, "paprocie");
164
165console.log("\nMięsożerca:");
166console.log(`   ${rexy.getInfo()}`);
167console.log(`   ${rexy.breathe()}`);
168console.log(`   ${rexy.hunt()}`);
169console.log(`   ${rexy.roar()}`);
170
171console.log("\nRoślinożerca:");
172console.log(`   ${trike.getInfo()}`);
173console.log(`   ${trike.breathe()}`);
174console.log(`   ${trike.eat()}`);
175console.log(`   ${trike.graze()}`);
176
177// ===========================================
178// 5. WIELOPOZIOMOWE DZIEDZICZENIE
179// ===========================================
180console.log("\n=== 5. Wielopoziomowe dziedziczenie ===");
181
182class LivingBeing {
183  constructor(name) {
184    this.name = name;
185  }
186
187  live() {
188    return `${this.name} żyje`;
189  }
190}
191
192class Reptile extends LivingBeing {
193  constructor(name, scaleType) {
194    super(name);
195    this.scaleType = scaleType;
196  }
197
198  shedSkin() {
199    return `${this.name} zrzuca skórę`;
200  }
201}
202
203class PrehistoricReptile extends Reptile {
204  constructor(name, scaleType, period) {
205    super(name, scaleType);
206    this.period = period;
207    this.extinct = true;
208  }
209
210  getHistory() {
211    return `${this.name} żył w okresie ${this.period}`;
212  }
213}
214
215const velociraptor = new PrehistoricReptile("Blue", "małe", "Kredowy");
216
217console.log("\nVelociraptor (3 poziomy dziedziczenia):");
218console.log(`   ${velociraptor.live()}`); // Z LivingBeing
219console.log(`   ${velociraptor.shedSkin()}`); // Z Reptile
220console.log(`   ${velociraptor.getHistory()}`); // Z PrehistoricReptile
221
222console.log("\ninstanceof (wielopoziomowe):");
223console.log(`   instanceof PrehistoricReptile: ${velociraptor instanceof PrehistoricReptile}`);
224console.log(`   instanceof Reptile: ${velociraptor instanceof Reptile}`);
225console.log(`   instanceof LivingBeing: ${velociraptor instanceof LivingBeing}`);
226console.log(`   instanceof Object: ${velociraptor instanceof Object}`);
227
228// ===========================================
229// 6. MIXINY (COMPOSITION OVER INHERITANCE)
230// ===========================================
231console.log("\n=== 6. Mixiny - kompozycja zamiast dziedziczenia ===");
232
233// Mixin dla możliwości latania
234const FlyingMixin = {
235  fly() {
236    return `${this.name} leci z prędkością ${this.flyingSpeed} km/h`;
237  },
238  land() {
239    return `${this.name} ląduje`;
240  }
241};
242
243// Mixin dla możliwości pływania
244const SwimmingMixin = {
245  swim() {
246    return `${this.name} pływa`;
247  },
248  dive() {
249    return `${this.name} nurkuje`;
250  }
251};
252
253// Funkcja pomocnicza do dodawania mixinów
254function addMixins(targetClass, ...mixins) {
255  mixins.forEach(mixin => {
256    Object.assign(targetClass.prototype, mixin);
257  });
258}
259
260class Pteranodon {
261  constructor(name, flyingSpeed) {
262    this.name = name;
263    this.flyingSpeed = flyingSpeed;
264  }
265
266  roar() {
267    return `${this.name} krzyczy: SCREECH!`;
268  }
269}
270
271class Mosasaurus {
272  constructor(name) {
273    this.name = name;
274  }
275
276  roar() {
277    return `${this.name} ryczy pod wodą: ROAARRR!`;
278  }
279}
280
281// Dodanie mixinów
282addMixins(Pteranodon, FlyingMixin);
283addMixins(Mosasaurus, SwimmingMixin);
284
285const ptero = new Pteranodon("Pterry", 80);
286const mosa = new Mosasaurus("Mosa");
287
288console.log("\nPteranodon (z mixinem latania):");
289console.log(`   ${ptero.roar()}`);
290console.log(`   ${ptero.fly()}`);
291console.log(`   ${ptero.land()}`);
292
293console.log("\nMosasaurus (z mixinem pływania):");
294console.log(`   ${mosa.roar()}`);
295console.log(`   ${mosa.swim()}`);
296console.log(`   ${mosa.dive()}`);
297
298// ===========================================
299// 7. PRZYKŁAD KOMPLEKSOWY
300// ===========================================
301console.log("\n=== 7. System ekosystemu - przykład kompleksowy ===");
302
303class Ecosystem {
304  constructor(name) {
305    this.name = name;
306    this.creatures = [];
307  }
308
309  addCreature(creature) {
310    this.creatures.push(creature);
311    console.log(`Dodano ${creature.name} do ekosystemu ${this.name}`);
312  }
313
314  simulate() {
315    console.log(`\n============================================================`);
316    console.log(`SYMULACJA EKOSYSTEMU: ${this.name}`);
317    console.log("=".repeat(60));
318
319    console.log(`\nŁącznie stworzeń: ${this.creatures.length}`);
320
321    const carnivores = this.creatures.filter(c => c.type === "Mięsożerca");
322    const herbivores = this.creatures.filter(c => c.type === "Roślinożerca");
323
324    console.log(`   Mięsożercy: ${carnivores.length}`);
325    console.log(`   Roślinożercy: ${herbivores.length}`);
326
327    console.log("\nAktywności stworzeń:");
328    this.creatures.forEach((creature, i) => {
329      console.log(`\n  ${i + 1}. ${creature.getInfo()}`);
330      console.log(`     ${creature.breathe()}`);
331
332      if (creature.hunt) {
333        console.log(`     ${creature.hunt()}`);
334        console.log(`     ${creature.roar()}`);
335      }
336
337      if (creature.eat) {
338        console.log(`     ${creature.eat()}`);
339        console.log(`     ${creature.graze()}`);
340      }
341    });
342
343    this.showFoodChain();
344  }
345
346  showFoodChain() {
347    console.log("\nŁańcuch pokarmowy:");
348
349    const carnivores = this.creatures.filter(c => c.type === "Mięsożerca");
350    const herbivores = this.creatures.filter(c => c.type === "Roślinożerca");
351
352    console.log(`   Rośliny → Roślinożercy (${herbivores.map(h => h.name).join(", ")})`);
353    console.log(`   Roślinożercy → Mięsożercy (${carnivores.map(c => c.name).join(", ")})`);
354  }
355}
356
357// Utworzenie ekosystemu
358const jurassicEcosystem = new Ecosystem("Park Jurajski");
359
360// Dodanie stworzeń
361jurassicEcosystem.addCreature(new CarnivoreClass("Rexy", "T-Rex", 8, 10));
362jurassicEcosystem.addCreature(new CarnivoreClass("Blue", "Velociraptor", 5, 9));
363jurassicEcosystem.addCreature(new HerbivoreClass("Trike", "Triceratops", 7, "paprocie"));
364jurassicEcosystem.addCreature(new HerbivoreClass("Stego", "Stegosaurus", 6, "rośliny"));
365jurassicEcosystem.addCreature(new CarnivoreClass("Delta", "Velociraptor", 5, 8));
366
367// Uruchom symulację
368jurassicEcosystem.simulate();
369
370console.log("\nGratulacje! Opanowałeś prototypy i dziedziczenie!");

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co się dzieje, gdy próbujemy uzyskać dostęp do właściwości, której obiekt nie posiada?

  2. 2. Co robi metoda Object.create(proto)?

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj kroki szukania właściwości w łańcuchu prototypów:

Przydatne artykuły