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!