JavaScript öröklődés – konstruktor hívás elkerülésével
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:
- egy osztályra, aminek van prototípusa és konstruktora;
- hogy a prototípus módosítása kihasson a leszármazottra;
- 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. :-)
■
ExtJS
ExtJS 4-ben vezették be a callParent függvényt, ami szintén nagy találmány:
Persze, a for-in-nel
backbone
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
Én régebben mikor elkezdtem
Mindent el lehet intézni egy ilyennel:
Azért nem mindent, de nagyon
Nem is olyan ritka...
Persze, de optimális
Egy másik Function.prototype.extend
Bár én igyekszem nem kiegészíteni az Object, Function … beépített osztályokat.
Az, hogy beleépítsük a
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.
Yepp, igaz, itt is prototípus
Ahogy nézem csak a legfrisebb
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 :)
Na köszi, legalább már ez is
már volt is talán?
:-o
egyszerűen
link