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:

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 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.

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:

  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:

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)
);
 
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:

var MyClass = (function() {
	var publStat = 'public static member';
	Class.__defineGetter__('publStat',function(){ return publStat; });
	Class.__defineSetter__('publStat',function(value){ publStat = value });
	
	var privStat = 'private static member';
	Class.getPrivStat = function() { return privStat; };
	
	function Class() { // constructor
		this.publ = 'public member';
		
		var priv = 'private member';
		this.getPriv = function() { return priv; };
	};
	
	return Class;
})();

var MySubClass = (function() {
	Class.__proto__ = MyClass; // parent class
	
	function Class() { // constructor
		this.__proto__.__proto__ = new Class.__proto__(); // parent's constructor
	};
	
	return Class;
})();

var oldObj = new MyClass();
var obj = new MySubClass();
obj.publ += ' is modified';
obj.constructor.publStat += ' is modified';

console.log( obj.publ ); // modified
console.log( obj.getPriv() );
console.log( obj.constructor.publStat ); // modified
console.log( oldObj.constructor.publStat ); // also modified!
console.log( MyClass.publStat ); // also modified!
console.log( MySubClass.publStat ); // also modified!
console.log( obj.constructor.getPrivStat() );
console.log( 'Osztály hierarchia: ' + MyClass.isPrototypeOf(MySubClass) + 
		' & ' + (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!