ugrás a tartalomhoz

JavaScript öröklődés – konstruktor hívás elkerülésével

inf3rno · 2012. Május. 19. (Szo), 17.26

Ezt a bejegyzést a JavaScript öröklődésről szóló folytatásának szánom. Azért döntöttem úgy, hogy bejegyzést írok erről a témáról, mert végre találtam egy olyan megoldást, ami a lehető legkevesebb mellékhatással valósítja meg mindezt.

Nézzük, mi az alap probléma: van egy ősosztályunk (ezentúl ezt a szót fogom használni rá, bár a JavaScriptben nincsenek valódi osztályok), annak van egy leszármazottja, ami szeretnénk ha örökölné az őse tulajdonságait.

Ez kétféleképpen oldható meg. Az első módszernél egyesével másoljuk a tulajdonságokat:

var Ancestor = function () {
    alert("Ancestor");
};

Ancestor.prototype.x = function () {
    alert("Ancestor.x");
};

Ancestor.prototype.y = function () {
    alert("Ancestor.y");
};

var Descendant = function () {
    alert("Descendant");
};

for (var property in Ancestor.prototype) {
    Descendant.prototype[property] = Ancestor.prototype[property];
}

Descendant.prototype.x = function () {
    alert("Descendant.x");
};

Ennek az a hátránya, hogy az ős későbbi módosítása nem hat ki a leszármazottra. A második módszernél a leszármazott prototípusa az ős egy példánya lesz:

var Ancestor = function () {
    alert("Ancestor");
};

Ancestor.prototype.x = function () {
    alert("Ancestor.x");
};

Ancestor.prototype.y = function () {
    alert("Ancestor.y");
};

var Descendant = function () {
    alert("Descendant");
};

Descendant.prototype = new Ancestor();
Descendant.prototype.constructor = Descendant;

Descendant.prototype.x = function () {
    alert("Descendant.x");
};

Ennek viszont az a hátránya, hogy az ős konstruktorát is meghívjuk a leszármazott készítése közben. A legtöbb keretrendszer (például a MooTools vagy a Prototype) ezt használja inkább, némi módosítással, mert az előzőnél nehéz megvalósítani, hogy az ős módosítása kihasson az összes leszármazottra.

Az általános megoldás a konstruktor hívás megkerülésére egy burkoló függvény, ami a leszármazott prototípusának készítésénél megakadályozza az ős konstruktorának meghívását:

var createClass = function (properties) {
    var Class = function () {
        if (!this.breakInit && this.init) {
            this.init.apply(this, arguments);
        }
    };
    
    for (var property in properties) {
        Class.prototype[property] = properties[property];
    }
    
    return Class;
};

var extendClass = function (Ancestor, properties) {
    var Descendant = function () {
        if (!this.breakInit && this.init) {
            this.init.apply(this, arguments);
        }
    };
    
    Ancestor.prototype.breakInit = true;
    Descendant.prototype = new Ancestor();
    Ancestor.prototype.breakInit = false;
    
    Descendant.prototype.constructor = Descendant;
    
    for (var property in properties) {
        Descendant.prototype[property] = properties[property];
    }
    
    return Descendant;
};

var Ancestor = createClass({
    init: function (param) {
        this.display(param);
    },
    
    display: function (param) {
        alert(param);
    }
});

var Descendant = extendClass(Ancestor, {
    init: function (param) {
        this.display(param + "1");
    }
});

var descendant = new Descendant("0"); // "01"

Ennek az a hátránya, hogy a konstruktor átkerül az init()-be, az osztályok konstruktora pedig egy generált függvény lesz, amiről elég nehézkes kideríteni, hogy pontosan melyik osztályt jelöli.

Itt még azt is észre kell venni, hogy az őshöz nem a this-en keresztül jutunk el, hanem az arguments.callee-n, hiszen ha többlépcsős öröklésről van szó, akkor könnyen rekurzióba kerülhetünk emiatt. A hibás megoldás:

var createClass = function (properties) {
    var Class = function () {
        if (!this.breakInit && this.init) {
            this.init.apply(this, arguments);
        }
    };
    
    for (var property in properties) {
        Class.prototype[property] = properties[property];
    }
    
    return Class;
};

var extendClass = function (Ancestor, properties) {
    var Descendant = function () {
        if (!this.breakInit && this.init) {
            this.init.apply(this, arguments);
    };
    
    Ancestor.prototype.breakInit = true;
    Descendant.prototype = new Ancestor();
    Ancestor.prototype.breakInit = false;
    
    Descendant.prototype.constructor = Descendant;
    Descendant.SuperClass = Ancestor;
    
    for (var property in properties) {
        Descendant.prototype[property] = properties[property];
    }
    
    return Descendant;
};

var AncestorAncestor = createClass({
    init: function (param) {
        alert(param);
    }
});

var Ancestor = extendClass(AncestorAncestor, {
    init: function (param) {
        this.constructor.SuperClass.prototype.init.call(this, param + "2");
    }
});

var Descendant = extendClass(Ancestor, {
    init: function (param) {
        this.constructor.SuperClass.prototype.init.call(this, param + "1");
    }
});

var descendant = new Descendant("0"); // error: too much recursion

A helyes megoldás:

var createClass = function (properties) {
    var Class = function () {
        if (!this.breakInit && this.init) {
            this.init.apply(this, arguments);
        }
    };
    
    for (var property in properties) {
        Class.prototype[property] = properties[property];
    }
    
    return Class;
};

var extendClass = function (Ancestor, properties) {
    var Descendant = function () {
        if (!this.breakInit && this.init) {
            this.init.apply(this, arguments);
        }
    };
    
    Ancestor.prototype.breakInit = true;
    Descendant.prototype = new Ancestor();
    Ancestor.prototype.breakInit = false;
    
    Descendant.prototype.constructor = Descendant;
    Descendant.SuperClass = Ancestor;
    
    for (var property in properties) {
        Descendant.prototype[property] = properties[property];
    }
    
    Descendant.prototype.init.Class = Descendant;
    return Descendant;
};

var AncestorAncestor = createClass({
    init: function (param) {
        alert(param);
    }
});

var Ancestor = extendClass(AncestorAncestor, {
    init: function (param) {
        arguments.callee.Class.SuperClass.prototype.init.call(this, param + "2");
    }
});

var Descendant = extendClass(Ancestor, {
    init: function (param) {
        arguments.callee.Class.SuperClass.prototype.init.call(this, param + "1");
    }
});

var descendant = new Descendant("0"); // 012

Persze hívhatjuk a saját nevén is az aktuális őst, mivel az arguments.callee strict módban már nem elérhető. Ami itt nem túl szép, hogy az init()-ben az arguments.callee nem a konstruktorra hivatkozik. Ahhoz, hogy tudjunk az aktuális ősre hivatkozni elég jól kell ismerni, hogy pontosan hogyan működik a megvalósításunk.

Ahhoz, hogy jobb megoldást találjunk, nézzük meg, hogy pontosan mire van szükségünk:

  1. egy osztályra, aminek van prototípusa és konstruktora;
  2. hogy a prototípus módosítása kihasson a leszármazottra;
  3. anélkül, hogy a konstruktort elfednénk egy generált függvénnyel.

Hogyan valósítható meg mindez? Ehhez azt kell észrevenni, hogy a JavaScript megengedi, hogy két konstruktorhoz ugyanaz a prototípus tartozzon:

var A = function () {
    this.display("A");
};

var B = function () {
    this.display("B");
};

A.prototype.display = function (param) {
    alert(param);
};

B.prototype = A.prototype;

var a = new A(); // "A"
var b = new B(); // "B"

Az egyik konstruktor lehet magának az osztálynak a konstruktora, a másik pedig egy üres függvény, ami az örökítéshez kell. A feni példa, a többlépcsős öröklés valahogy így néz ki ezzel a megoldással:

var createClass = function (properties) {
    var Class = properties.constructor;
    
    Class.repository = function () {};
    Class.repository.prototype = Class.prototype;
    
    for (var property in properties) {
        Class.prototype[property] = properties[property];
    }
    
    return Class;
};

var extendClass = function (Ancestor, properties) {
    var Descendant = properties.constructor;
    
    Descendant.prototype = new Ancestor.repository();
    Descendant.prototype.constructor = Descendant;
    Descendant.SuperClass = Ancestor;
    
    Descendant.repository = function () {};
    Descendant.repository.prototype = Descendant.prototype;
    
    for (var property in properties) {
        Descendant.prototype[property] = properties[property];
    }
    
    return Descendant;
};

var AncestorAncestor = createClass({
    constructor: function (param) {
        alert(param);
    }
});

var Ancestor = extendClass(AncestorAncestor, {
    constructor: function (param) {
        arguments.callee.SuperClass.call(this, param + "2");
    }
});

var Descendant = extendClass(Ancestor, {
    constructor: function (param) {
        arguments.callee.SuperClass.call(this, param + "1");
    }
});

var descendant = new Descendant("0");

Ha már találkoztatok máshol is ezzel a megoldással, akkor kérem jelezzétek. Egyelőre én még nem futottam össze vele, úgyhogy azt hiszem, valami újat alkottam. :-)

 
1

ExtJS

T.G · 2012. Május. 20. (V), 14.07
Érdemes lenne megnézned az ExtJS által készített megoldást is. 1-es, 2-es verzióban használt megoldás erősen hasonlít ehhez, amit azóta fokozatosan egészítettek ki további kényelmi funkcióval, illetve IE alatt a for (... in ...) nem minden elemet vesz figyelembe, pl. toString. Ezeket külön le kell kezelni.

ExtJS 4-ben vezették be a callParent függvényt, ami szintén nagy találmány:

Ext.define('Descendant', {
	extend: 'Ancestor',
	init: function (param) {
		this.callParent(param + "1");
	}
});
2

Persze, a for-in-nel

inf3rno · 2012. Május. 20. (V), 14.39
Persze, a for-in-nel tisztában vagyok, csak nem akartam annyira részletesre írni. Oks, megnézem az ExtJs-t, egyébként használtam már, csak még nem túrtam bele a kódjába.
3

backbone

marfalkov · 2012. Május. 20. (V), 19.00
esetleg érdekes lehet ez a backbone és underscore alapokon nyugvó megoldás:
https://github.com/jimmydo/js-toolbox/blob/master/toolbox.js
illetve az extrák hozzá:
https://github.com/jimmydo/js-toolbox/blob/master/toolbox.extras.js
4

Én régebben mikor elkezdtem

Karvaly84 · 2012. Május. 21. (H), 10.18
Én régebben mikor elkezdtem magam JavaScript-ben ki kupálni, egy rakás keretrendszert megnéztem az öröklődéssel kapcsolatban. Lehet csak azért mert nem írok olyan bonyolultságú dolgokat js-ben, de nekem az volt az érzésem, hogy túl van bonyolítva.

Mindent el lehet intézni egy ilyennel:

Function.prototype.extend = function(proto) {
	if (!proto) {
		proto = {};
	}
	var subclass = proto.hasOwnProperty("constructor") ? proto.constructor : proto.constructor = function() {};
	subclass.prototype = Object.create(this.prototype); // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create
	for (var k in proto) {
		if (proto.hasOwnProperty(k)) {
			subclass.prototype[k] = proto[k];
		}
	}
	return subclass;
};
szerintem, és tök jól működik.
5

Azért nem mindent, de nagyon

inf3rno · 2012. Május. 21. (H), 11.19
Azért nem mindent, de nagyon sok dolgot. Ha pl az ős osztályra a nevén hivatkozol a leszármazottban, akkor ugyanúgy el lehet érni benne a metódusokat... Annyi hátránya van, hogy az ős osztály változásait nem lehet egyből továbbörökíteni a leszármazottra, persze ez meg olyan dolog, ami szökőévente egyszer fordulhat elő...
6

Nem is olyan ritka...

T.G · 2012. Május. 21. (H), 18.15
Ha a keretrendszer forráskódját az ember nem akarja módosítani, ám valamely ősosztályban javítani akar valamit, akkor kifejezetten hasznos, ha az ősosztály javítása hat a származtatott osztályokra is.
8

Persze, de optimális

inf3rno · 2012. Május. 21. (H), 19.17
Persze, de optimális állapotban a keretrendszer maga a tökéletesség :D
7

Egy másik Function.prototype.extend

T.G · 2012. Május. 21. (H), 18.16
Szerintem ennek a verziónak komoly hiányossága, hogy nem hivatkozhatunk az ősosztályra, magam részéről azt nem tartom olyan ritka eseménynek. Ebből a függvényből számtalan verzió létezik, némileg szimpatikusabb ez a megoldás: https://gist.github.com/1367191
Bár én igyekszem nem kiegészíteni az Object, Function … beépített osztályokat.
9

Az, hogy beleépítsük a

Karvaly84 · 2012. Május. 21. (H), 21.50
Az, hogy beleépítsük a leszármazott példányba az ős osztályt már részlet kérdése, én speciel azért nem foglalkozom vele mert általában, ha kiterjesztek egy osztályt tudom, hogy melyiket terjesztem ki és így már szólíthatom a nevén is a dolgot ha pl. az ős osztály konstruktorát kéne meghívni az adott példányra.

Viszont amit inf3rno mond, miszerint az ős osztály változásai nem hatnak ki a leszármazottra azt nem értem miért ne hatna ki mikor prototípus alapú láncolás történik. Vagy félre értek valamit.

Egyébként az Object-et én sem szoktam kiegészíteni, de a Function-el már más a helyzet.
10

Yepp, igaz, itt is prototípus

inf3rno · 2012. Május. 21. (H), 23.31
Yepp, igaz, itt is prototípus alapú láncolás történik. Csak azért írtam, mert nem ismertem az Object.create függvényt. Ahogy nézem csak a legfrisebb böngészőkben támogatott, és végülis gyakorlatilag ugyanazt csinálja, mint az én megoldásom.

if (!Object.create) {  
    Object.create = function (o) {  
        if (arguments.length > 1) {  
            throw new Error('Object.create implementation only accepts the first parameter.');  
        }  
        function F() {}  
        F.prototype = o;  
        return new F();  
    };  
}
Akkor gyakorlatilag feltaláltam a spanyol viaszt... Amikor átadod a prototípust az Object.create-nek, akkor teljesen ugyanaz történik, mint az én megoldásomnál...
11

Ahogy nézem csak a legfrisebb

Karvaly84 · 2012. Május. 22. (K), 04.16
Ahogy nézem csak a legfrisebb böngészőkben támogatott

ezért írtam oda a linket, mert az Object.create függvényt le lehet gyártani bár nem teljes funkcionalitással, de az első paraméterig cross-browser :)
12

Na köszi, legalább már ez is

inf3rno · 2012. Május. 22. (K), 10.09
Na köszi, legalább már ez is felkerült weblaborra, ha nem is akkora hatalmas újítás :D
13

már volt is talán?

zzrek · 2012. Május. 22. (K), 13.58
Ha jól emlékszek, talán egy csiripben van blogmarkban már szerepelt ez már itt, a weblaboron is.
:-o
14

egyszerűen

blacksonic · 2012. Júl. 5. (Cs), 21.19
én a hetekben találkoztam egy önálló megvalósítással (nem kell hozzá semmilyen libet betölteni) ami úgy néz ki minden alap dolgot tud
link