ugrás a tartalomhoz

JavaScript öröklődés

presidento · 2010. Május. 20. (Cs), 08.11

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?

 
1

Fel :-)

inf3rno · 2010. Május. 20. (Cs), 09.10
A konstruktort ki szokták emelni, hogy az öröklődésnél másolható legyen..

Valahogy így:

(function ()
{
	var Map=function ()
	{
		//...
	};
	//...
	
	var ClassMap=new Map();
	
	var Class=function ()
	{
		this.construct=function ()
		{
			if (this.initialize instanceof Function)
			{
				return this.initialize.apply(this,arguments);
			}
		};
		ClassMap.put(this.construct,this);
		//...
		return this.construct;
	};
	
	window.Class=Class;
})();
A Class is bővíthető függvényekkel és a konstruktor felületére is ki lehet tenni függvényeket. A konstruktor osztályának beazonosítását a Map oldja meg, így a Class object private marad.
2

Van királyi út…

presidento · 2010. Május. 20. (Cs), 10.29
…de csak Mozilla alapú böngészőkben:
var Human = function( name ) {
	this.name = name;
};
Human.prototype.toString = function() { return 'My name is ' + this.name + '.'; };

var Programmer = function(name, hacks) {
	this.__proto__.__proto__ =  name instanceof Human ? name : new Human( name ); 
	this.hacks = hacks;
};
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');
john.name = 'Johnson';

console.log(john + '\n' + johnTheProgrammer + '\n' + jeremy + '\n\n' + 
	( jeremy instanceof Programmer ) + ' ' + ( jeremy instanceof Human ) 
);
Csodálatos. :) Kérdezheted, miért kell két __proto__ a konstruktor függvényben. Két okból. Egyrészt, ha felülírnánk a prototípust, elveszne az eredetileg megadott toString metódus, másrészt a prototípus megváltoztatása törli a közvetlen konstruktor függvényre vonatkozó információt:
function X(){};
function Y(){};
var x = new X();
var y = new Y();

y.__proto__ = x;

console.log( x.isPrototypeOf( y ) ); // true
console.log( y instanceof X );       // true
console.log( y instanceof Y );       // false (!)
Ezzel a módszerrel megvalósítható a többszörös öröklődés is:
Object.prototype.setPrototypes = function() {
	var o = this;
	var i = arguments.length;
	while ( i-- ) {
		while ( o.__proto__.__proto__ ) o = o.__proto__;
		o.__proto__ = arguments[i];
	};
};

var Man = function( name ) { this.name = name; };
Man.prototype.sayName = function() { return 'My name is ' + this.name + '.'; };

var Horse = function( speed ) { this.speed = speed; };
Horse.prototype.saySpeed = function() { return 'My speed is ' + this.speed + '.'; };

var Kentaur = function( name, speed ) {
	this.setPrototypes( new Man( name ), new Horse( speed ) );
};

var kentaur = new Kentaur( 'Kentaur Karoly', 100);

console.log( '(' + kentaur.name + ', ' + kentaur.speed + ')');
console.log( kentaur.sayName() + ' ' + kentaur.saySpeed());
console.log( (kentaur instanceof Man) + ' ' + (kentaur instanceof Horse)
		+ ' ' + (kentaur instanceof Kentaur) ); // true, true, true