ugrás a tartalomhoz

Gondolatok a JavaScript prototípusosságáról

presidento · 2010. Május. 19. (Sze), 13.06

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:

  1. var egyAsztal = {  
  2.     netto: 130000,  
  3.       
  4.     brutto: function () {  
  5.         return this.netto * 1.25;  
  6.     }  
  7. };  

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 a new 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 vagy Object vagy Null típusú értéket tartalmaz. A prototype akkor kap jelentőséget, ha a függvényt a new operátorral hívjuk meg, azaz amikor a függvény konstruktorként működik. A prototype 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.

  1. var Asztalos = function (fiokok) {  
  2.     this.fiokok = fiokok;  
  3.     this.netto += this.fiokok * 15000;  
  4. };  
  5.   
  6. Asztalos.prototype = egyAsztal;  
  7. Asztalos.name      = 'István';  
  8.   
  9. 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:

  1. var FiokosAsztal = function (fiokok) {  
  2.     this.fiokok = fiokok;  
  3.     this.netto += this.fiokok * 15000;  
  4. };  
  5.   
  6. FiokosAsztal.prototype = egyAsztal;  
  7. FiokosAsztal.licenc    = 'egri';  
  8.   
  9. var masikAsztal = new FiokosAsztal(5);  
  10.   
  11. 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:

  1. Függvény.prototype; // A Függvénynek átadott mintadarab  
  2.   
  3. objektum = new Függvény(…); // Függvény, hozz létre egy objektumot   
  4.                             // a megadott paraméterek alapján!  
  5.                               
  6. 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.

  1. function Designer(grafika) {  
  2.     this.grafika = grafika;  
  3. };  
  4.   
  5. function Developer(backend, frontend) {  
  6.     this.backend  = backend;  
  7.     this.frontend = frontend;  
  8. };  
  9.   
  10. var honlapAlpha = { terv: 'nagyszerű' };  
  11.   
  12. Designer.prototype = honlapAlpha;  
  13. var honlapBeta     = new Designer('csili-vili');  
  14.   
  15. Developer.prototype = honlapBeta;  
  16. var honlap          = new Developer('elosztott''gyors');  
  17.   
  18. console.log(honlap);  
  19. console.log(honlap instanceof Designer);  
  20. console.log(honlap instanceof Developer);  

Olvassuk végig:

  1. Szerződtetünk egy designert
  2. és egy developert,
  3. készítünk egy tervet,
  4. megadjuk a designernek, hogy ehhez a tervhez…
  5. …készítsen egy designt…
  6. …amiket átadunk mintaként a developernek…
  7. …aki kiegészíti frontenddel és backenddel.
  8. A honlap tartalmazza a szükséges adatokat,
  9. a készítésében közreműködött a designer
  10. é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:

  1. var benaAlpha = { terv: 'béna' };  
  2.   
  3. Designer.prototype = benaAlpha;  
  4. var benaBeta       = new Designer('unalmas');  
  5.   
  6. Developer.prototype = benaBeta;  
  7. var bena            = new Developer('lassú''szaggatott');  
  8.   
  9. console.log(bena);  
  10. 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:

  1. console.log(honlap.__proto__ === honlapBeta);  
  2. console.log(honlapBeta.__proto__ === honlapAlpha );  

Ráadásul ez írható-olvasható tulajdonság, így az objektumunk teljes átalakuláson mehet át:

  1. hernyo   = { mivagyok: 'hernyó' };  
  2. pillango = { mivagyok: 'pillangó' };  
  3.   
  4. function CreateAllat(nev) {  
  5.     this.nevem = function () {  
  6.         return 'A nevem: ' + nev + '.';  
  7.     }  
  8. };  
  9.   
  10. function babozo(h) {  
  11.     h.__proto__ = pillango;  
  12. };  
  13.   
  14. // A hernyó mintájára készítünk egy állatot  
  15. CreateAllat.prototype = hernyo;  
  16. var allat             = new CreateAllat('Lepke');  
  17.   
  18. // Ez az állat egy hernyó  
  19. alert(  
  20.     allat.nevem() +  
  21.     ' Én egy ' +  
  22.     allat.mivagyok +  
  23.     ' vagyok.\n' +   
  24.     hernyo.isPrototypeOf(allat) +  
  25.     ' ' +  
  26.     pillango.isPrototypeOf(allat)  
  27. );  
  28.   
  29. // Elküldjük a bábozóba  
  30. babozo(allat);  
  31.   
  32. // Jé, ez nem is hernyó, ez egy pillangó!  
  33. alert(  
  34.     allat.nevem() +  
  35.     ' Én egy ' +  
  36.     allat.mivagyok +  
  37.     ' vagyok.\n' +   
  38.     hernyo.isPrototypeOf(allat) +  
  39.     ' ' +  
  40.     pillango.isPrototypeOf(allat)  
  41. );  
 
2

Osztályok

presidento · 2010. Május. 19. (Sze), 18.20
Hozzátenném, Firefox-jellegű böngészők alatt jól szimulálható az osztály-központú öröklődés is:
  1. var MyClass = (function() {  
  2.     var publStat = 'public static member';  
  3.     Class.__defineGetter__('publStat',function(){ return publStat; });  
  4.     Class.__defineSetter__('publStat',function(value){ publStat = value });  
  5.       
  6.     var privStat = 'private static member';  
  7.     Class.getPrivStat = function() { return privStat; };  
  8.       
  9.     function Class() { // constructor  
  10.         this.publ = 'public member';  
  11.           
  12.         var priv = 'private member';  
  13.         this.getPriv = function() { return priv; };  
  14.     };  
  15.       
  16.     return Class;  
  17. })();  
  18.   
  19. var MySubClass = (function() {  
  20.     Class.__proto__ = MyClass; // parent class  
  21.       
  22.     function Class() { // constructor  
  23.         this.__proto__.__proto__ = new Class.__proto__(); // parent's constructor  
  24.     };  
  25.       
  26.     return Class;  
  27. })();  
  28.   
  29. var oldObj = new MyClass();  
  30. var obj = new MySubClass();  
  31. obj.publ += ' is modified';  
  32. obj.constructor.publStat += ' is modified';  
  33.   
  34. console.log( obj.publ ); // modified  
  35. console.log( obj.getPriv() );  
  36. console.log( obj.constructor.publStat ); // modified  
  37. console.log( oldObj.constructor.publStat ); // also modified!  
  38. console.log( MyClass.publStat ); // also modified!  
  39. console.log( MySubClass.publStat ); // also modified!  
  40. console.log( obj.constructor.getPrivStat() );  
  41. console.log( 'Osztály hierarchia: ' + MyClass.isPrototypeOf(MySubClass) +   
  42.         ' & ' + (obj instanceof MyClass) + ' & ' + (obj instanceof MySubClass) );  
1

Gratulálok

zzrek · 2010. Május. 19. (Sze), 17.17
Gratulálok, ez nekem nagyon tetszett!
Ezt másoknak is ajánlani fogom!