A JavaScript támogatja az objektumorientált programozást (OOP). Az OOP során objektumokat (példányokat) hozol létre „sablonokból” (osztályokból), hogy azok meghatározott adatokat és funkciókat tartalmazzanak. Az adatokat tároló tulajdonságokat az OOP szóhasználatában mezőknek, a függvényeket tároló tulajdonságokat metódusoknak nevezzük.
A JavaScriptnek egyáltalán nem voltak osztályai, mielőtt azok 2015-ben bekerültek a nyelvi specifikációba, de a prototípusalapú öröklődés segítségével már addig is lehetővé tette az objektumorientált programozást.
És bár manapság már létezik a class kulcsszó, a JavaScript továbbra is prototípusalapú nyelv.
Hogy megértsük, mit jelent az, hogy egy nyelv prototípusalapú, és hogyan is működik valójában a JavaScript, térjünk vissza abba az időbe, amikor még nem voltak osztályok.
A JavaScriptben a sablont (az osztályt) egy egyszerű függvény valósítja meg.
Amikor egy függvényt ilyen sablonként szeretnénk használni, azt konstruktorfüggvénynek nevezzük, és a szokás az, hogy a függvény neve nagybetűvel kezdődik.
A példányokat (objektumokat) a new kulcsszóval hozzuk létre a sablonból a konstruktorfüggvény meghívásakor.
function Car() {
// ...
}
const myCar = new Car();
const yourCar = new Car();
Fontos megjegyezni, hogy a JavaScriptben a példányok és a konstruktorfüggvény kapcsolatban maradnak egymással akkor is, miután a példányok létrejöttek.
Minden példányobjektum tartalmaz egy rejtett, belső tulajdonságot, amelyre a nyelvi specifikáció [[prototype]] néven hivatkozik.
Ez hivatkozást tárol a konstruktorfüggvény prototype kulcsának értékére.
Igen, jól olvastad: egy JavaScript-függvénynek lehetnek kulcs/érték párjai, mert a háttérben maga is objektum.
2015 óta a [[prototype]] a Object.getPrototypeOf() segítségével érhető el.
Az előtt sok környezetben a __proto__ kulcson keresztül volt elérhető.
Ne keverd össze egy objektum prototípusát ([[prototype]]) a konstruktorfüggvény prototype tulajdonságával.
Összefoglalva:
prototype tulajdonsága lesz a példány prototípusa.Gyakran szeretnéd, hogy a származtatott objektumok (példányok) mindegyike tartalmazzon néhány mezőt, és ezekhez kezdőértékeket adj át az objektum létrehozásakor.
Ezt a this kulcsszóval valósíthatod meg.
A konstruktorfüggvényen belül a this azt az új objektumot jelöli, amelyet a new fog létrehozni.
A this automatikusan visszatér a konstruktorfüggvényből, amikor azt a new kulcsszóval hívjuk meg.
Ez azt jelenti, hogy úgy adhatunk mezőket az új példányhoz, hogy hozzáadjuk őket a this-hez a konstruktorfüggvényben.
function Car(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false
A metódusokat a konstruktorfüggvény prototype tulajdonságán keresztül adjuk hozzá.
Egy metóduson belül a this segítségével érheted el a példány mezőit.
Ez az alábbi általános szabály miatt működik.
Amikor egy függvényt egy objektum metódusaként hívunk meg, a
thisértéke arra az objektumra áll be, amelyen a metódust meghívták. 1
function Car() {
this.engineRunning = false;
// ...
}
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
Car.prototype.addGas = function (litre) {
// ...
};
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
A fenti példában szereplő myCar egy hétköznapi JavaScript-objektum, és ha megvizsgálnánk (például a böngésző konzoljában), nem találnánk közvetlenül a myCar objektumban egy startEngine tulajdonságot, amelynek értéke függvény.
Akkor mégis hogyan működik a fenti kód?
A titok itt a prototípuslánc.
Amikor egy objektum bármely tulajdonságát (mezőjét vagy metódusát) próbálod elérni, a JavaScript először megnézi, hogy a kérdéses kulcs megtalálható-e közvetlenül magában az objektumban.
Ha nem, akkor tovább keresi a kulcsot abban az objektumban, amelyre az eredeti objektum [[prototype]] tulajdonsága hivatkozik.
Ahogy korábban említettük, a példánkban a [[prototype]] a konstruktorfüggvény prototype tulajdonságára mutat.
Itt találja meg a JavaScript a startEngine függvényt, mert oda adtuk hozzá.
function Car() {
// ...
}
Car.prototype.startEngine = function () {
// ...
};
És a lánc itt nem ér véget.
A Car.prototype [[prototype]] tulajdonsága (myCar.[[prototype]].[[prototype]]) az Object.prototype-ra hivatkozik (az Object konstruktorfüggvény prototype tulajdonságára).
Ez olyan általános metódusokat tartalmaz, amelyek minden JavaScript-objektum számára elérhetők, például a toString().
Az Object [[prototype]] tulajdonsága általában null, így a prototípuslánc ott véget ér.
Összefoglalva: meghívhatod a myCar.toString() metódust, és az létezni fog, mert a JavaScript az egész prototípusláncon végigkeresi ezt a metódust.
Részletes példát a »Inheritance and the prototype chain« című MDN-cikkben találsz.
Vedd figyelembe, hogy a prototípusláncot csak érték kiolvasásakor járjuk be. Egy példányobjektum tulajdonságának közvetlen beállítása vagy törlése csak arra a konkrét példányra hat. Ez talán nem az, amire egy osztályalapú öröklődést használó nyelvhez szokott programozó számítana.
A JavaScript lehetővé teszi, hogy metódusokat adj minden létező példányhoz, akkor is, ha azok már létrejöttek.
Megtudtuk, hogy minden példány hivatkozást tárol a konstruktorfüggvény prototype tulajdonságára.
Ez azt jelenti, hogy ha bejegyzést adsz ahhoz a prototype objektumhoz, az új bejegyzés (például egy új metódus) azonnal elérhetővé válik a konstruktorfüggvényből létrehozott összes példány számára.
function Car() {
this.engineRunning = false;
}
const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
myCar.startEngine();
// This works, even though myCar was created before the method
// was added.
Elméletben a dinamikus metódusokkal akár beépített objektumok, például az Object vagy az Array funkcióit is bővítheted a prototípusuk módosításával.
Ezt monkey patchingnek nevezik.
Mivel ez a változtatás az egész alkalmazásra hatással van, jobb elkerülni, hogy ne idézz elő nem szándékolt mellékhatásokat.
Az egyetlen észszerű felhasználási eset az, amikor egy hiányzó metódushoz nyújtasz polyfillt régebbi környezetekben.
Manapság a JavaScript támogatja az osztályok definiálását a class kulcsszóval.
Ez 2015-ben került be a nyelvi specifikációba.
Egyrészt ez szintaktikai cukor, ami könnyebben olvashatóvá és írhatóvá teszi az osztályokat.
Az új szintaxis jobban hasonlít ahhoz, ahogy az olyan nyelvekben, mint a C++ vagy a Java, az osztályokat írják.
Az ezekről a nyelvekről átváltó fejlesztők könnyebben alkalmazkodnak.
Másrészt az osztályszintaxis utat nyit olyan új nyelvi funkcióknak, amelyek a prototípus szintaxisban nem érhetők el.
Az új szintaxisban az osztályokat a class kulcsszóval definiáljuk, amelyet az osztály neve, majd kapcsos zárójelek között az osztály törzse követ.
A törzs tartalmazza a konstruktorfüggvény definícióját, azaz egy constructor nevű speciális metódust.
Ez a függvény pontosan úgy működik, mint a konstruktorfüggvény a prototípus szintaxisban.
Az osztály törzse ezen kívül az összes metódusdefiníciót is tartalmazza.
A metódusok szintaxisa hasonlít ahhoz a rövidített jelöléshez, amit a függvények objektumon belüli értékként való hozzáadásánál láttunk; lásd az Objektumok fogalmat.
class Car {
constructor(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
startEngine() {
this.engineRunning = true;
}
addGas(litre) {
// ...
}
}
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
A függvénydeklarációkhoz és függvénykifejezésekhez hasonlóan a JavaScript a fent bemutatott osztálydeklaráció mellett osztálykifejezéseket is támogat.
Tartsd észben, hogy a JavaScript a háttérben továbbra is prototípusalapú nyelv. A fenti „Prototípus szintaxis” szakaszban megismert mechanizmusok mind érvényben vannak.
A JavaScriptben alapértelmezés szerint minden példánymező nyilvános. Közvetlenül elérhetők, és értéket is adhatsz nekik.
A valódi privát mezők bekerülése a nyelvi specifikációba még folyamatban van; a részletekért lásd a javaslati dokumentumot.
Addig is támaszkodhatsz a bevált konvencióra: az aláhúzással kezdődő mezőket és metódusokat privátként kell kezelni. Ezeket soha nem szabad közvetlenül az osztályon kívülről elérni.
A privát mezőket néha getterek és setterek kísérik.
A get és set kulcsszavakkal olyan függvényeket definiálhatsz, amelyek akkor futnak le, amikor egy, a függvénnyel azonos nevű tulajdonságot elérnek vagy értéket adnak neki.
class Car {
constructor() {
this._mileage = 0;
}
get mileage() {
return this._mileage;
}
set mileage(value) {
throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}
const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.
Az OOP-ban néha olyan segédmezőket vagy segédmetódusokat szeretnél biztosítani, amelyek nem egy adott példánytól függenek.
Ehelyett magára az osztályra vannak definiálva.
Ezt a static kulcsszóval érheted el.
class Car {
static type = 'vehicle';
static isType(targetType) {
return targetType === 'vehicle';
}
}
Car.type;
// => 'vehicle'
Car.isType('road sign');
// => false
A korábban látott, a prototípuslánc mentén történő öröklődés mellett a JavaScriptben az osztályok közötti öröklődést is kifejezheted. Ezt az Öröklődés fogalom tárgyalja.
this példák - objektum metódusaként, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (hozzáférés: 2021. december 3.) ↩