Kurs JavaScript i TypeScript · Moduł 2: Funkcje, tablice i obiekty
Prototypy i dziedziczenie prototypowe
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')); // trueW 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); // trueW 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); // trueTen 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); // truePrototypy 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); // trueSkł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:
- Dynamiczność - prototypy mogą być modyfikowane w czasie wykonania programu, podczas gdy klasy są zwykle statyczne.
- Ł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.
- Brak klas (przed ES6) - JavaScript tradycyjnie nie miał formalnej koncepcji klas, a jedynie funkcje konstruktora i prototypy.
- 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
- Zrozumienie mechanizmu - ważne jest zrozumienie, jak działa łańcuch prototypów, aby skutecznie korzystać z dziedziczenia.
- Korzystanie z klas (ES6+) - dla większości przypadków użycia składnia klas jest bardziej czytelna i łatwiejsza w utrzymaniu.
- Unikanie modyfikacji wbudowanych prototypów - modyfikacja prototypów wbudowanych obiektów (np. Object.prototype, Array.prototype) może prowadzić do nieoczekiwanych konfliktów.
- Stosowanie Object.create() dla dziedziczenia - przy używaniu tradycyjnego podejścia, Object.create() jest czystym sposobem implementacji dziedziczenia prototypowego.
- 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:
- Prototypy - mechanizm, dzięki któremu obiekty mogą dziedziczyć właściwości i metody od innych obiektów.
- Ł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.
- Funkcje konstruktora i prototypy - tradycyjny sposób implementacji dziedziczenia w JavaScript.
- Klasy ES6+ - nowoczesna składnia dla dziedziczenia, oparta na mechanizmie prototypów.
- 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. Co się dzieje, gdy próbujemy uzyskać dostęp do właściwości, której obiekt nie posiada?
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: