Gondolatok a JavaScript prototípusosságáról
A JavaScript objektumorientált, de nem a klasszikus OOP értelemben, ugyanis nincsenek osztályok, a JavaScript prototípus alapú. Balogh Tibor írt erről egy alapos, de könnyen emészthető cikket. Én a jelenségnek egy más aspektusát vizsgálnám meg: az osztály alapú objektumorientált programozást ismerők számára nehéz megérteni a JavaScript működését, és erre véleményem szerint a JavaScript is rájátszik egy kicsit. Miért van ez, és hogyan lehetne orvosolni?
Tibor azt írja:
Ha már láttál egy asztalt – mivel mindegyik valamiképpen hasonlít egymásra – a többit is fölismered. Ezért bármelyik asztal megismerése után felismerjük a többit is, ha a szemünk elé kerül. A feltétele a felismerésnek, hogy megismerkedjünk a fizikai világunkban az adott dologgal. A JavaScript objektumorientált szemlélete – hogy elhagyhassa az osztály fogalmát – ez utóbbi megközelítést követi. A JavaScript azt mondja, hogy: Mutass egy objektumot, ilyen a többi is. Ha mégsem ilyen, akkor majd mondd meg, hogy miben különbözik!
Tegyük fel, hogy látunk egy asztalt:
var egyAsztal = {
netto: 130000,
brutto: function () {
return this.netto * 1.25;
}
};
Ismét tőle idéznék:
Ha nem közvetlenül, objektumliterállal akarjuk létrehozni az objektumokat, akkor az objektum előállításához a
new
operátort kell használnunk. Bármelyik függvény végrehajtásával előállíthatunk objektumot, ha a függvényt anew
operátorral hívjuk meg. A függvény ekkor az objektum konstruktora (készítője, előállítója) lesz.Minden függvény rendelkezik egy
prototype
(mintadarab, prototípus) nevű tulajdonsággal, ez a tulajdonság vagyObject
vagyNull
típusú értéket tartalmaz. Aprototype
akkor kap jelentőséget, ha a függvényt anew
operátorral hívjuk meg, azaz amikor a függvény konstruktorként működik. Aprototype
tartalmazza az objektum előállításához használatos mintadarabot
Cipőt a cipőboltból, tartja a mondás, asztalt az asztalostól, teszem hozzá. Mutassuk meg az asztalosnak (István) az asztalt, és kérjük meg, hogy készítsen ehhez hasonlót, aminek fiókjai is vannak.
var Asztalos = function (fiokok) {
this.fiokok = fiokok;
this.netto += this.fiokok * 15000;
};
Asztalos.prototype = egyAsztal;
Asztalos.name = 'István';
var masikAsztal = new Asztalos(5);
Hűha, kavarás van, ez így nem lesz jó. – Haladjunk visszafele: az asztalosunk neve István, adtunk neki egy mintát, és megmondtuk, hogy miben különbözzön az általa létrehozott objektum a mintától. – Viszont a masikAsztal
egy új asztalos lenne?! Nem! Az nem lehet! – Miért, szerinted nem az asztalos konstruálja az újabb asztalokat? No jó, vegyük a következő kódot:
var FiokosAsztal = function (fiokok) {
this.fiokok = fiokok;
this.netto += this.fiokok * 15000;
};
FiokosAsztal.prototype = egyAsztal;
FiokosAsztal.licenc = 'egri';
var masikAsztal = new FiokosAsztal(5);
alert(masikAsztal instanceof FiokosAsztal); // true
Huh, így már ismerős, nem? (A fiókos asztal gyártósora az egri licenc szerint dolgozik.) – Akkor miért nem ezzel kezdted?! – A fiókos asztal nem egy osztály, pedig ez a kód azt sugallja.
A JavaScript mintadarab alapon szervezi az öröklődést, a masikAsztal
az egyAsztal
mintájára készült: egyAsztal.isPrototypeOf(masikAsztal)
, mondhatnánk a masikAsztal
prototípusa az egyAsztal
(masikAsztal.prototype == egyAsztal
). De nem mondjuk, mert nem így van: a FiokosAsztal
prototípusa az egyAsztal
! Ha viszont az a prototípusa, akkor ahhoz hasonlóan kéne működnie, nem? Pedig ez két teljesen különböző objektum!
Jaj, Máté, ne bonyolítsd túl a dolgot! A FiokosAsztal
egy osztály, ami az egyAsztal
mintájára példányosul! – Rendben, legyen egy osztály, ez esetben a FiokosAsztal.licenc
-nek egy osztály szintű változónak kellene lennie, nem? Hogyan éred el a masikAsztal
-ból a licenc
-et? Sőt, hogyan éred el a masikAsztal
-ból azt, hogy minek a mintájára készült? Sehogy. (A szabványos JavaScriptről beszélek, lásd lentebb.)
Pedig nem olyan bonyolult, csak azért nehéz felfogni, mert mást ért a JavaScript bizonyos szavak alatt, mint amit eddig megszoktunk. Mostantól a következő kifejezéseket a megjegyzés szerint ejtsük ki:
Függvény.prototype; // A Függvénynek átadott mintadarab
objektum = new Függvény(…); // Függvény, hozz létre egy objektumot
// a megadott paraméterek alapján!
objektum instanceof Függvény; // Az objektum létrehozásában a Függvény közreműködött
Készítsünk el egy honlapot. Van egy nagyszerű tervünk, ez alapján készül egy csili-vili design, és hozzá egy elosztott backend és gyors frontend.
function Designer(grafika) {
this.grafika = grafika;
};
function Developer(backend, frontend) {
this.backend = backend;
this.frontend = frontend;
};
var honlapAlpha = { terv: 'nagyszerű' };
Designer.prototype = honlapAlpha;
var honlapBeta = new Designer('csili-vili');
Developer.prototype = honlapBeta;
var honlap = new Developer('elosztott', 'gyors');
console.log(honlap);
console.log(honlap instanceof Designer);
console.log(honlap instanceof Developer);
Olvassuk végig:
- Szerződtetünk egy designert
- és egy developert,
- készítünk egy tervet,
- megadjuk a designernek, hogy ehhez a tervhez…
- …készítsen egy designt…
- …amiket átadunk mintaként a developernek…
- …aki kiegészíti frontenddel és backenddel.
- A honlap tartalmazza a szükséges adatokat,
- a készítésében közreműködött a designer
- és a developer is.
Így olvasva, remélem, érthetőbb a kód és a prototípus alapú öröklődés. A konstruktorfüggvény a minta alapján készít egy objektumot, ha más mintát adunk neki, más objektumot fog készíteni:
var benaAlpha = { terv: 'béna' };
Designer.prototype = benaAlpha;
var benaBeta = new Designer('unalmas');
Developer.prototype = benaBeta;
var bena = new Developer('lassú', 'szaggatott');
console.log(bena);
console.log(honlap);
Ebből is látszik, hogy a prototípus nem „osztály szintű változó”, hiszen a honlapunkat nem rontotta el az, hogy pedagógiai céllal készítettünk egy béna oldalt. A konstruktorfüggvény prototype
tulajdonsága annyit jelent, hogy amikor a függvény a new
operátorral van meghívva, a JavaScript létrehoz egy objektumot (this
), beállítja, hogy az új objektum a prototípus „mintájára készült, csak…” – és itt van lehetőség megadni, hogy miben tér el.
Ahogy utaltam rá, bizonyos esetekben lehet egyszerűbb dolgunk: Firefoxban az objektum __proto__
tulajdonsága a mintadarabjára mutat:
console.log(honlap.__proto__ === honlapBeta);
console.log(honlapBeta.__proto__ === honlapAlpha );
Ráadásul ez írható-olvasható tulajdonság, így az objektumunk teljes átalakuláson mehet át:
hernyo = { mivagyok: 'hernyó' };
pillango = { mivagyok: 'pillangó' };
function CreateAllat(nev) {
this.nevem = function () {
return 'A nevem: ' + nev + '.';
}
};
function babozo(h) {
h.__proto__ = pillango;
};
// A hernyó mintájára készítünk egy állatot
CreateAllat.prototype = hernyo;
var allat = new CreateAllat('Lepke');
// Ez az állat egy hernyó
alert(
allat.nevem() +
' Én egy ' +
allat.mivagyok +
' vagyok.\n' +
hernyo.isPrototypeOf(allat) +
' ' +
pillango.isPrototypeOf(allat)
);
// Elküldjük a bábozóba
babozo(allat);
// Jé, ez nem is hernyó, ez egy pillangó!
alert(
allat.nevem() +
' Én egy ' +
allat.mivagyok +
' vagyok.\n' +
hernyo.isPrototypeOf(allat) +
' ' +
pillango.isPrototypeOf(allat)
);
■
Osztályok
Gratulálok
Ezt másoknak is ajánlani fogom!