JavaScript öröklődés
Az általánosítás és specializáció az objektumorientált programozás központi eleme, mely során egy ősosztályból származtatott alosztály újabb tulajdonságokat és metódusokat kap. Mindez jól modellezhető JavaScriptben is, de akad itt egy kis probléma, ha az ősosztály konstruktora paramétereket vár…
Az embereknek van nevük, és képesek bemutatkozni. John egy ember. John felnő és programozó lesz, elmélyül a PHP hackelésében. Mindezt könnyen leprogramozhatjuk JavaScriptben:
var Human = function(name) {
this.name = name;
};
Human.prototype.toString = function () {
return 'My name is ' + this.name + '.';
};
var john = new Human('John');
alert(john);
var Programmer = function (language) {
this.language = language;
};
Programmer.prototype = john;
john = null;
Programmer.prototype.toString = function () {
return this.name + ' hacks ' + this.language + '.';
};
var johnTheProgrammer = new Programmer('PHP');
alert(johnTheProgrammer);
(Sajnos John teljes átalakuláson volt kénytelen átmenni, hiszen Programmer.prototype === john.prototype
lett, ezért a john.toString()
már nem a megfelelő reprezentációt adná. De tekintsünk el ettől a kis affértól.) John sikeres egyéni vállalkozóként bővíteni szeretné a cégét, és felvenné Jeremy-t, aki ért a JavaScript hackeléséhez – Johnt nem érdekli Jeremy múltja, csupán az, hogy milyen jó szakember. És itt ért véget a történet.
Ugyanis míg az objektumorientált nyelvekben osztályok hierarchiájáról beszélhetünk, a JavaScript prototípus alapú, a megszokott specializációt nem alkalmazhatjuk. Mivel itt a konstruktorfüggvény prototípusa egy objektum lehet, OOP nyelven szólva az osztály definiálásakor példányosítani kell az ősosztályt, ez elég nagy probléma, ha az ősosztály példányosításához szükséges paramétereket futásidőben kapjuk meg.
A legtöbb esetben nem okoz gondot, hiszen ha a konstruktorfüggvény nem kér paramétert, akkor az „osztály” minden objektuma létrehozáskor megegyezik, így megadható prototípusként (sajnos a programozó Johnnak itt semmilyen kapcsolata nincs a civil Johnnal):
var Human = function () {};
Human.prototype.toString = function () {
return 'My name is ' + this.name() + '.';
};
Human.prototype.name = function (name) {
if (arguments.length) {
this._name = name;
return this;
} else {
return this._name;
}
};
var Programmer = function () {};
Programmer.prototype = new Human();
Programmer.prototype.toString = function () {
return this.name() + ' hacks ' + this.language() + '.';
};
Programmer.prototype.language = function (language) {
if (arguments.length) {
this._language = language;
return this;
} else {
return this._language;
}
};
var john = new Human().name('John');
var johnTheProgrammer = new Programmer().name('John').language('PHP');
var jeremy = new Programmer().name('Jeremy').language('JavaScript');
alert(john + '\n' + johnTheProgrammer + '\n' + jeremy);
Esetenként nincs lehetőségünk az ősosztályt ilyen módon felkészíteni, és a konstruktorfüggvény paramétereket vár. Szerencsére az öröklődés ebben az esetben is megvalósítható, igaz, nem olyan elegánsan, mint az osztályalapú nyelvekben, de ennek is megvan a szépsége:
var Human = function(name) {
this.name = name;
};
Human.prototype.toString = function () {
return 'My name is ' + this.name + '.';
};
var Programmer = (function () {
// Static functions
var toString = function () {
return this.name + ' hacks ' + this.hacks + '.';
};
return function (name, hacks) {
// We need to create a new one of ourself
if (arguments.callee.caller === arguments.callee) {
return;
}
var human = name instanceof Human ? name : new Human(name);
Programmer.prototype = human;
var that = new Programmer();
that.toString = toString;
that.hacks = hacks;
return that;
};
})();
var john = new Human('John');
var johnTheProgrammer = new Programmer(john, 'PHP');
var jeremy = new Programmer('Jeremy', 'JavaScript');
john.name = 'Johnson';
alert(
john + '\n' +
johnTheProgrammer + '\n' +
jeremy + '\n\n' +
(jeremy instanceof Programmer) + ' ' +
(jeremy instanceof Human) + ' ' +
(john.isPrototypeOf(johnTheProgrammer))
);
Érdekesség, hogy a megszokottól eltérően JavaScriptben a new
operátorral meghívott kifejezés visszatérési értéke nem feltétlenül a new
operátor által létrehozott objektum: ha a visszatérési érték undefined
, a this
-t kapjuk meg, de lehetőségünk van explicit akár teljesen mást megadni.
Ha nincs szükségünk arra, hogy John fejlődését (john.isPrototypeOf(johnTheProgrammer)
) nyomonkövessük, a klasszikus kód is írható:
var Human = function (name) {
this.name = name;
};
Human.prototype.toString = function () {
return 'My name is ' + this.name + '.';
};
var Programmer = function (name, hacks) {
Human.call(this, name);
this.hacks = hacks;
};
Programmer.prototype = new Human('__The Prototype of Programmers__');
Programmer.prototype.toString = function () {
return this.name + ' hacks ' + this.hacks + '.';
};
var john = new Human('John');
var johnTheProgrammer = new Programmer('John', 'PHP');
var jeremy = new Programmer('Jeremy', 'JavaScript');
alert(
john + '\n' +
johnTheProgrammer + '\n' +
jeremy + '\n\n' +
(jeremy instanceof Programmer) + ' ' +
(jeremy instanceof Human)
);
Ez nem minden esetben használható, néha gondot okozhat, hogy létrehozunk egy objektumot, és utána még egyszer meg akarjuk hívni a konstruktort rajta. (Ebbe futottam bele, amikor a Worker képességeit szerettem volna kibővíteni.)
Feltaláltam a kereket?
■
Fel :-)
Valahogy így:
Van királyi út…
__proto__
a konstruktor függvényben. Két okból. Egyrészt, ha felülírnánk a prototípust, elveszne az eredetileg megadotttoString
metódus, másrészt a prototípus megváltoztatása törli a közvetlen konstruktor függvényre vonatkozó információt: