ugrás a tartalomhoz

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

inf · 2012. Május. 19. (Szo), 18.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:

  1. var Ancestor = function () {  
  2.     alert("Ancestor");  
  3. };  
  4.   
  5. Ancestor.prototype.x = function () {  
  6.     alert("Ancestor.x");  
  7. };  
  8.   
  9. Ancestor.prototype.y = function () {  
  10.     alert("Ancestor.y");  
  11. };  
  12.   
  13. var Descendant = function () {  
  14.     alert("Descendant");  
  15. };  
  16.   
  17. for (var property in Ancestor.prototype) {  
  18.     Descendant.prototype[property] = Ancestor.prototype[property];  
  19. }  
  20.   
  21. Descendant.prototype.x = function () {  
  22.     alert("Descendant.x");  
  23. };  

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:

  1. var Ancestor = function () {  
  2.     alert("Ancestor");  
  3. };  
  4.   
  5. Ancestor.prototype.x = function () {  
  6.     alert("Ancestor.x");  
  7. };  
  8.   
  9. Ancestor.prototype.y = function () {  
  10.     alert("Ancestor.y");  
  11. };  
  12.   
  13. var Descendant = function () {  
  14.     alert("Descendant");  
  15. };  
  16.   
  17. Descendant.prototype = new Ancestor();  
  18. Descendant.prototype.constructor = Descendant;  
  19.   
  20. Descendant.prototype.x = function () {  
  21.     alert("Descendant.x");  
  22. };  

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:

  1. var createClass = function (properties) {  
  2.     var Class = function () {  
  3.         if (!this.breakInit && this.init) {  
  4.             this.init.apply(this, arguments);  
  5.         }  
  6.     };  
  7.       
  8.     for (var property in properties) {  
  9.         Class.prototype[property] = properties[property];  
  10.     }  
  11.       
  12.     return Class;  
  13. };  
  14.   
  15. var extendClass = function (Ancestor, properties) {  
  16.     var Descendant = function () {  
  17.         if (!this.breakInit && this.init) {  
  18.             this.init.apply(this, arguments);  
  19.         }  
  20.     };  
  21.       
  22.     Ancestor.prototype.breakInit = true;  
  23.     Descendant.prototype = new Ancestor();  
  24.     Ancestor.prototype.breakInit = false;  
  25.       
  26.     Descendant.prototype.constructor = Descendant;  
  27.       
  28.     for (var property in properties) {  
  29.         Descendant.prototype[property] = properties[property];  
  30.     }  
  31.       
  32.     return Descendant;  
  33. };  
  34.   
  35. var Ancestor = createClass({  
  36.     init: function (param) {  
  37.         this.display(param);  
  38.     },  
  39.       
  40.     display: function (param) {  
  41.         alert(param);  
  42.     }  
  43. });  
  44.   
  45. var Descendant = extendClass(Ancestor, {  
  46.     init: function (param) {  
  47.         this.display(param + "1");  
  48.     }  
  49. });  
  50.   
  51. 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:

  1. var createClass = function (properties) {  
  2.     var Class = function () {  
  3.         if (!this.breakInit && this.init) {  
  4.             this.init.apply(this, arguments);  
  5.         }  
  6.     };  
  7.       
  8.     for (var property in properties) {  
  9.         Class.prototype[property] = properties[property];  
  10.     }  
  11.       
  12.     return Class;  
  13. };  
  14.   
  15. var extendClass = function (Ancestor, properties) {  
  16.     var Descendant = function () {  
  17.         if (!this.breakInit && this.init) {  
  18.             this.init.apply(this, arguments);  
  19.     };  
  20.       
  21.     Ancestor.prototype.breakInit = true;  
  22.     Descendant.prototype = new Ancestor();  
  23.     Ancestor.prototype.breakInit = false;  
  24.       
  25.     Descendant.prototype.constructor = Descendant;  
  26.     Descendant.SuperClass = Ancestor;  
  27.       
  28.     for (var property in properties) {  
  29.         Descendant.prototype[property] = properties[property];  
  30.     }  
  31.       
  32.     return Descendant;  
  33. };  
  34.   
  35. var AncestorAncestor = createClass({  
  36.     init: function (param) {  
  37.         alert(param);  
  38.     }  
  39. });  
  40.   
  41. var Ancestor = extendClass(AncestorAncestor, {  
  42.     init: function (param) {  
  43.         this.constructor.SuperClass.prototype.init.call(this, param + "2");  
  44.     }  
  45. });  
  46.   
  47. var Descendant = extendClass(Ancestor, {  
  48.     init: function (param) {  
  49.         this.constructor.SuperClass.prototype.init.call(this, param + "1");  
  50.     }  
  51. });  
  52.   
  53. var descendant = new Descendant("0"); // error: too much recursion  

A helyes megoldás:

  1. var createClass = function (properties) {  
  2.     var Class = function () {  
  3.         if (!this.breakInit && this.init) {  
  4.             this.init.apply(this, arguments);  
  5.         }  
  6.     };  
  7.       
  8.     for (var property in properties) {  
  9.         Class.prototype[property] = properties[property];  
  10.     }  
  11.       
  12.     return Class;  
  13. };  
  14.   
  15. var extendClass = function (Ancestor, properties) {  
  16.     var Descendant = function () {  
  17.         if (!this.breakInit && this.init) {  
  18.             this.init.apply(this, arguments);  
  19.         }  
  20.     };  
  21.       
  22.     Ancestor.prototype.breakInit = true;  
  23.     Descendant.prototype = new Ancestor();  
  24.     Ancestor.prototype.breakInit = false;  
  25.       
  26.     Descendant.prototype.constructor = Descendant;  
  27.     Descendant.SuperClass = Ancestor;  
  28.       
  29.     for (var property in properties) {  
  30.         Descendant.prototype[property] = properties[property];  
  31.     }  
  32.       
  33.     Descendant.prototype.init.Class = Descendant;  
  34.     return Descendant;  
  35. };  
  36.   
  37. var AncestorAncestor = createClass({  
  38.     init: function (param) {  
  39.         alert(param);  
  40.     }  
  41. });  
  42.   
  43. var Ancestor = extendClass(AncestorAncestor, {  
  44.     init: function (param) {  
  45.         arguments.callee.Class.SuperClass.prototype.init.call(this, param + "2");  
  46.     }  
  47. });  
  48.   
  49. var Descendant = extendClass(Ancestor, {  
  50.     init: function (param) {  
  51.         arguments.callee.Class.SuperClass.prototype.init.call(this, param + "1");  
  52.     }  
  53. });  
  54.   
  55. 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:

  1. var A = function () {  
  2.     this.display("A");  
  3. };  
  4.   
  5. var B = function () {  
  6.     this.display("B");  
  7. };  
  8.   
  9. A.prototype.display = function (param) {  
  10.     alert(param);  
  11. };  
  12.   
  13. B.prototype = A.prototype;  
  14.   
  15. var a = new A(); // "A"  
  16. 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:

  1. var createClass = function (properties) {  
  2.     var Class = properties.constructor;  
  3.       
  4.     Class.repository = function () {};  
  5.     Class.repository.prototype = Class.prototype;  
  6.       
  7.     for (var property in properties) {  
  8.         Class.prototype[property] = properties[property];  
  9.     }  
  10.       
  11.     return Class;  
  12. };  
  13.   
  14. var extendClass = function (Ancestor, properties) {  
  15.     var Descendant = properties.constructor;  
  16.       
  17.     Descendant.prototype = new Ancestor.repository();  
  18.     Descendant.prototype.constructor = Descendant;  
  19.     Descendant.SuperClass = Ancestor;  
  20.       
  21.     Descendant.repository = function () {};  
  22.     Descendant.repository.prototype = Descendant.prototype;  
  23.       
  24.     for (var property in properties) {  
  25.         Descendant.prototype[property] = properties[property];  
  26.     }  
  27.       
  28.     return Descendant;  
  29. };  
  30.   
  31. var AncestorAncestor = createClass({  
  32.     constructor: function (param) {  
  33.         alert(param);  
  34.     }  
  35. });  
  36.   
  37. var Ancestor = extendClass(AncestorAncestor, {  
  38.     constructor: function (param) {  
  39.         arguments.callee.SuperClass.call(this, param + "2");  
  40.     }  
  41. });  
  42.   
  43. var Descendant = extendClass(Ancestor, {  
  44.     constructor: function (param) {  
  45.         arguments.callee.SuperClass.call(this, param + "1");  
  46.     }  
  47. });  
  48.   
  49. 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), 15.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:
  1. Ext.define('Descendant', {  
  2.     extend: 'Ancestor',  
  3.     init: function (param) {  
  4.         this.callParent(param + "1");  
  5.     }  
  6. });  
2

Persze, a for-in-nel

inf · 2012. Május. 20. (V), 15.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), 20.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), 11.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:
  1. Function.prototype.extend = function(proto) {  
  2.     if (!proto) {  
  3.         proto = {};  
  4.     }  
  5.     var subclass = proto.hasOwnProperty("constructor") ? proto.constructor : proto.constructor = function() {};  
  6.     subclass.prototype = Object.create(this.prototype); // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create  
  7.     for (var k in proto) {  
  8.         if (proto.hasOwnProperty(k)) {  
  9.             subclass.prototype[k] = proto[k];  
  10.         }  
  11.     }  
  12.     return subclass;  
  13. };  
szerintem, és tök jól működik.
5

Azért nem mindent, de nagyon

inf · 2012. Május. 21. (H), 12.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), 19.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

inf · 2012. Május. 21. (H), 20.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), 19.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), 22.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

inf · 2012. Május. 22. (K), 00.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.
  1. if (!Object.create) {    
  2.     Object.create = function (o) {    
  3.         if (arguments.length > 1) {    
  4.             throw new Error('Object.create implementation only accepts the first parameter.');    
  5.         }    
  6.         function F() {}    
  7.         F.prototype = o;    
  8.         return new F();    
  9.     };    
  10. }  
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), 05.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

inf · 2012. Május. 22. (K), 11.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), 14.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), 22.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