मेरे पास वर्गों को एकाधिक वंशानुक्रम के साथ परिभाषित करने की अनुमति देने के लिए काफी कार्य है। यह निम्नलिखित की तरह कोड के लिए अनुमति देता है। कुल मिलाकर आप जावास्क्रिप्ट में देशी क्लासिंग तकनीकों से पूर्ण प्रस्थान पर ध्यान देंगे (जैसे कि आप class
कीवर्ड कभी नहीं देखेंगे ):
let human = new Running({ name: 'human', numLegs: 2 });
human.run();
let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();
let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();
इस तरह उत्पादन का उत्पादन करने के लिए:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
यहाँ कक्षा परिभाषाएँ कैसी दिखती हैं:
let Named = makeClass('Named', {}, () => ({
init: function({ name }) {
this.name = name;
}
}));
let Running = makeClass('Running', { Named }, protos => ({
init: function({ name, numLegs }) {
protos.Named.init.call(this, { name });
this.numLegs = numLegs;
},
run: function() {
console.log(`${this.name} runs with ${this.numLegs} legs.`);
}
}));
let Flying = makeClass('Flying', { Named }, protos => ({
init: function({ name, numWings }) {
protos.Named.init.call(this, { name });
this.numWings = numWings;
},
fly: function( ){
console.log(`${this.name} flies away with ${this.numWings} wings!`);
}
}));
let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
init: function({ name, numLegs, numWings }) {
protos.Running.init.call(this, { name, numLegs });
protos.Flying.init.call(this, { name, numWings });
},
takeFlight: function() {
this.run();
this.fly();
}
}));
हम देख सकते हैं कि makeClass
फ़ंक्शन का उपयोग करने वाला प्रत्येक वर्ग परिभाषा Object
माता-पिता-कक्षाओं के लिए मैप किए गए मूल-श्रेणी के नामों को स्वीकार करता है । यह एक फ़ंक्शन को भी स्वीकार करता है जो Object
परिभाषित की जा रही कक्षा के लिए एक गुण देता है। इस फ़ंक्शन का एक पैरामीटर हैprotos
, जिसमें किसी भी मूल-वर्ग द्वारा परिभाषित किसी भी संपत्ति तक पहुंचने के लिए पर्याप्त जानकारी है।
अंतिम टुकड़ा आवश्यक makeClass
कार्य ही है, जो काफी काम करता है। यहाँ यह बाकी कोड के साथ है। मैंने makeClass
काफी भारी टिप्पणी की है:
let makeClass = (name, parents={}, propertiesFn=()=>({})) => {
// The constructor just curries to a Function named "init"
let Class = function(...args) { this.init(...args); };
// This allows instances to be named properly in the terminal
Object.defineProperty(Class, 'name', { value: name });
// Tracking parents of `Class` allows for inheritance queries later
Class.parents = parents;
// Initialize prototype
Class.prototype = Object.create(null);
// Collect all parent-class prototypes. `Object.getOwnPropertyNames`
// will get us the best results. Finally, we'll be able to reference
// a property like "usefulMethod" of Class "ParentClass3" with:
// `parProtos.ParentClass3.usefulMethod`
let parProtos = {};
for (let parName in parents) {
let proto = parents[parName].prototype;
parProtos[parName] = {};
for (let k of Object.getOwnPropertyNames(proto)) {
parProtos[parName][k] = proto[k];
}
}
// Resolve `properties` as the result of calling `propertiesFn`. Pass
// `parProtos`, so a child-class can access parent-class methods, and
// pass `Class` so methods of the child-class have a reference to it
let properties = propertiesFn(parProtos, Class);
properties.constructor = Class; // Ensure "constructor" prop exists
// If two parent-classes define a property under the same name, we
// have a "collision". In cases of collisions, the child-class *must*
// define a method (and within that method it can decide how to call
// the parent-class methods of the same name). For every named
// property of every parent-class, we'll track a `Set` containing all
// the methods that fall under that name. Any `Set` of size greater
// than one indicates a collision.
let propsByName = {}; // Will map property names to `Set`s
for (let parName in parProtos) {
for (let propName in parProtos[parName]) {
// Now track the property `parProtos[parName][propName]` under the
// label of `propName`
if (!propsByName.hasOwnProperty(propName))
propsByName[propName] = new Set();
propsByName[propName].add(parProtos[parName][propName]);
}
}
// For all methods defined by the child-class, create or replace the
// entry in `propsByName` with a Set containing a single item; the
// child-class' property at that property name (this also guarantees
// there is no collision at this property name). Note property names
// prefixed with "$" will be considered class properties (and the "$"
// will be removed).
for (let propName in properties) {
if (propName[0] === '$') {
// The "$" indicates a class property; attach to `Class`:
Class[propName.slice(1)] = properties[propName];
} else {
// No "$" indicates an instance property; attach to `propsByName`:
propsByName[propName] = new Set([ properties[propName] ]);
}
}
// Ensure that "init" is defined by a parent-class or by the child:
if (!propsByName.hasOwnProperty('init'))
throw Error(`Class "${name}" is missing an "init" method`);
// For each property name in `propsByName`, ensure that there is no
// collision at that property name, and if there isn't, attach it to
// the prototype! `Object.defineProperty` can ensure that prototype
// properties won't appear during iteration with `in` keyword:
for (let propName in propsByName) {
let propsAtName = propsByName[propName];
if (propsAtName.size > 1)
throw new Error(`Class "${name}" has conflict at "${propName}"`);
Object.defineProperty(Class.prototype, propName, {
enumerable: false,
writable: true,
value: propsAtName.values().next().value // Get 1st item in Set
});
}
return Class;
};
let Named = makeClass('Named', {}, () => ({
init: function({ name }) {
this.name = name;
}
}));
let Running = makeClass('Running', { Named }, protos => ({
init: function({ name, numLegs }) {
protos.Named.init.call(this, { name });
this.numLegs = numLegs;
},
run: function() {
console.log(`${this.name} runs with ${this.numLegs} legs.`);
}
}));
let Flying = makeClass('Flying', { Named }, protos => ({
init: function({ name, numWings }) {
protos.Named.init.call(this, { name });
this.numWings = numWings;
},
fly: function( ){
console.log(`${this.name} flies away with ${this.numWings} wings!`);
}
}));
let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
init: function({ name, numLegs, numWings }) {
protos.Running.init.call(this, { name, numLegs });
protos.Flying.init.call(this, { name, numWings });
},
takeFlight: function() {
this.run();
this.fly();
}
}));
let human = new Running({ name: 'human', numLegs: 2 });
human.run();
let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();
let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();
makeClass
समारोह भी वर्ग गुण का समर्थन करता है; इनको $
प्रतीक के साथ संपत्ति के नामों को उपसर्ग द्वारा परिभाषित किया जाता है (ध्यान दें कि अंतिम संपत्ति का नाम जिसके परिणाम $
निकाल दिए जाएंगे )। इसे ध्यान में रखते हुए, हम एक विशेष Dragon
वर्ग लिख सकते हैं जो ड्रैगन के "प्रकार" को मॉडल करता है, जहां उपलब्ध ड्रैगन प्रकारों की सूची को कक्षा में ही संग्रहीत किया जाता है, उदाहरणों के विपरीत:
let Dragon = makeClass('Dragon', { RunningFlying }, protos => ({
$types: {
wyvern: 'wyvern',
drake: 'drake',
hydra: 'hydra'
},
init: function({ name, numLegs, numWings, type }) {
protos.RunningFlying.init.call(this, { name, numLegs, numWings });
this.type = type;
},
description: function() {
return `A ${this.type}-type dragon with ${this.numLegs} legs and ${this.numWings} wings`;
}
}));
let dragon1 = new Dragon({ name: 'dragon1', numLegs: 2, numWings: 4, type: Dragon.types.drake });
let dragon2 = new Dragon({ name: 'dragon2', numLegs: 4, numWings: 2, type: Dragon.types.hydra });
एकाधिक वंशानुक्रम की चुनौतियाँ
जो कोई भी कोड के लिए makeClass
निकटता से पालन करता है वह चुपचाप घटित होने वाली एक महत्वपूर्ण अवांछनीय घटना को नोट करेगा जब उपरोक्त कोड चलता है: इंस्टेंट करने RunningFlying
से परिणामकर्ता को दो कॉल आएंगे Named
!
ऐसा इसलिए है क्योंकि वंशानुक्रम ग्राफ़ इस तरह दिखता है:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
जब सब-क्लास 'इनहेरिटेंस ग्राफ में समान पैरेंट-क्लास के कई रास्ते होते हैं , तो सब-क्लास के इंस्टेंटिएशन कई बार उस पैरेंट-क्लास के कंस्ट्रक्टर को इन्वॉल्व करेंगे।
यह मुकाबला गैर-तुच्छ है। आइए सरलीकृत वर्गनाम के साथ कुछ उदाहरण देखें। हम वर्ग पर विचार करेंगे A
, सबसे अमूर्त अभिभावक-वर्ग, वर्ग B
और C
, जो दोनों से विरासत में मिला है A
, और वर्ग BC
जो विरासत में मिला है B
और C
(और इसलिए वैचारिक रूप से "दोहरे-विरासत" A
):
let A = makeClass('A', {}, () => ({
init: function() {
console.log('Construct A');
}
}));
let B = makeClass('B', { A }, protos => ({
init: function() {
protos.A.init.call(this);
console.log('Construct B');
}
}));
let C = makeClass('C', { A }, protos => ({
init: function() {
protos.A.init.call(this);
console.log('Construct C');
}
}));
let BC = makeClass('BC', { B, C }, protos => ({
init: function() {
// Overall "Construct A" is logged twice:
protos.B.init.call(this); // -> console.log('Construct A'); console.log('Construct B');
protos.C.init.call(this); // -> console.log('Construct A'); console.log('Construct C');
console.log('Construct BC');
}
}));
यदि हम BC
दोहरे आक्रमण से बचना चाहते हैं तो हमें A.prototype.init
विरासत में मिले निर्माणों को सीधे कॉल करने की शैली को छोड़ना पड़ सकता है। हमें यह जांचने के लिए अप्रत्यक्ष स्तर की आवश्यकता होगी कि क्या होने से पहले डुप्लिकेट कॉल हो रही हैं, और शॉर्ट-सर्किट।
हम गुण फ़ंक्शन में दिए गए मापदंडों को बदलने पर विचार कर सकते हैं: बगल में protos
, Object
जिसमें निहित गुणों का वर्णन करने वाला एक कच्चा डेटा है, हम एक उदाहरण विधि को कॉल करने के लिए एक उपयोगिता फ़ंक्शन को इस तरह से भी शामिल कर सकते हैं कि मूल तरीकों को भी कहा जाता है, लेकिन डुप्लिकेट कॉल का पता लगाया जाता है और रोका गया। आइए एक नजर डालते हैं कि हम कहां के लिए पैरामीटर स्थापित करते हैं propertiesFn
Function
:
let makeClass = (name, parents, propertiesFn) => {
/* ... a bunch of makeClass logic ... */
// Allows referencing inherited functions; e.g. `parProtos.ParentClass3.usefulMethod`
let parProtos = {};
/* ... collect all parent methods in `parProtos` ... */
// Utility functions for calling inherited methods:
let util = {};
util.invokeNoDuplicates = (instance, fnName, args, dups=new Set()) => {
// Invoke every parent method of name `fnName` first...
for (let parName of parProtos) {
if (parProtos[parName].hasOwnProperty(fnName)) {
// Our parent named `parName` defines the function named `fnName`
let fn = parProtos[parName][fnName];
// Check if this function has already been encountered.
// This solves our duplicate-invocation problem!!
if (dups.has(fn)) continue;
dups.add(fn);
// This is the first time this Function has been encountered.
// Call it on `instance`, with the desired args. Make sure we
// include `dups`, so that if the parent method invokes further
// inherited methods we don't lose track of what functions have
// have already been called.
fn.call(instance, ...args, dups);
}
}
};
// Now we can call `propertiesFn` with an additional `util` param:
// Resolve `properties` as the result of calling `propertiesFn`:
let properties = propertiesFn(parProtos, util, Class);
/* ... a bunch more makeClass logic ... */
};
उपरोक्त परिवर्तन का पूरा उद्देश्य makeClass
यह है कि propertiesFn
जब हम आह्वान करते हैं तो हमारे पास एक अतिरिक्त तर्क होता है makeClass
। हमें यह भी पता होना चाहिए कि किसी भी वर्ग में परिभाषित प्रत्येक फ़ंक्शन को अब अपने सभी अन्य, नाम के बाद एक पैरामीटर प्राप्त हो सकता है dup
, जो कि एक Set
ऐसा कार्य है जो विरासत में मिली विधि को कॉल करने के परिणामस्वरूप सभी कार्यों को पहले से ही कहा गया है:
let A = makeClass('A', {}, () => ({
init: function() {
console.log('Construct A');
}
}));
let B = makeClass('B', { A }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
console.log('Construct B');
}
}));
let C = makeClass('C', { A }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
console.log('Construct C');
}
}));
let BC = makeClass('BC', { B, C }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
console.log('Construct BC');
}
}));
यह नई शैली वास्तव में यह सुनिश्चित करने में सफल होती है "Construct A"
कि केवल एक बार लॉग इन किया जाता है जब इसका उदाहरण BC
इनिशियलाइज़ किया जाता है। लेकिन तीन डाउनसाइड हैं, जिनमें से तीसरा बहुत महत्वपूर्ण है :
- यह कोड कम पठनीय और रखरखाव योग्य बन गया है।
util.invokeNoDuplicates
फ़ंक्शन के पीछे बहुत सारी जटिलता छिप जाती है, और यह सोचकर कि यह शैली बहु-आह्वान से कैसे बचती है, गैर-सहज और सिरदर्द उत्प्रेरण है। हमारे पास वह पेसकी dups
पैरामीटर भी है , जिसे वास्तव में कक्षा में प्रत्येक एकल फ़ंक्शन पर परिभाषित करने की आवश्यकता है । आउच।
- यह कोड धीमा है - कई विरासतों के साथ वांछनीय परिणाम प्राप्त करने के लिए काफी अधिक अप्रत्यक्ष और संगणना आवश्यक है। दुर्भाग्य से यह हमारी बहु-मंगलाचरण समस्या के किसी भी समाधान के साथ होने की संभावना है।
- सबसे महत्वपूर्ण बात, कार्यों की संरचना जो विरासत पर निर्भर करती है, बहुत कठोर हो गई है । यदि कोई उप-वर्ग
NiftyClass
किसी फ़ंक्शन को ओवरराइड करता है niftyFunction
, और util.invokeNoDuplicates(this, 'niftyFunction', ...)
इसे डुप्लिकेट-इनवोकेशन के बिना चलाने के लिए उपयोग करता है, तो इसे परिभाषित करने वाले प्रत्येक माता-पिता वर्ग NiftyClass.prototype.niftyFunction
के फ़ंक्शन को कॉल करेगा , niftyFunction
उन कक्षाओं से किसी भी रिटर्न मान को अनदेखा करेगा, और अंत में विशेष तर्क का प्रदर्शन करेगा NiftyClass.prototype.niftyFunction
। यह एकमात्र संभव संरचना है । यदि NiftyClass
विरासत CoolClass
और GoodClass
, और ये दोनों मूल-वर्ग niftyFunction
अपनी स्वयं की परिभाषा प्रदान करते हैं, NiftyClass.prototype.niftyFunction
तो कभी भी (एकाधिक-आह्वान के जोखिम के बिना) नहीं कर पाएंगे:
- ए
NiftyClass
पहले के विशेष तर्क को चलाते हैं , फिर अभिभावक-वर्गों के विशेष तर्क को
- B. सभी विशेष अभिभावक तर्क पूर्ण होने के बाद
NiftyClass
किसी भी बिंदु पर विशेष तर्क को चलाएं
- C. अपने माता-पिता के विशेष तर्क के वापसी मूल्यों के आधार पर सशर्त व्यवहार करें
- डी से बचें एक विशेष माता-पिता चल विशेष है
niftyFunction
पूरी तरह
निश्चित रूप से, हम विशेष प्रकार के कार्यों को परिभाषित करके उपरोक्त प्रत्येक अक्षरित समस्या को हल कर सकते हैं util
:
- ए परिभाषित करते हैं
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- बी डिफाइन
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)
( parentName
उस अभिभावक का नाम कहां है जिसका विशेष तर्क बच्चे के वर्ग विशेष के तर्क के तुरंत बाद होगा)
- C. परिभाषित
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)
(इस मामले में testFn
नामांकित माता-पिता के लिए विशेष तर्क का परिणाम प्राप्त होगा parentName
, और यह true/false
दर्शाता है कि शॉर्ट-सर्किट होगा या नहीं यह मान लौटाएगा )
- डी। डिफाइन
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)
(इस मामले में blackList
उन Array
अभिभावकों के नाम होंगे जिनके विशेष तर्क को पूरी तरह छोड़ दिया जाना चाहिए)
ये समाधान सभी उपलब्ध हैं, लेकिन यह कुल तबाही है ! प्रत्येक अद्वितीय संरचना के लिए जो विरासत में मिली फ़ंक्शन कॉल ले सकती है, हमें इसके तहत परिभाषित एक विशेष विधि की आवश्यकता होगी util
। क्या एक पूर्ण आपदा।
इसे ध्यान में रखते हुए हम अच्छी बहु विरासत को लागू करने की चुनौतियों को देखना शुरू कर सकते हैं। makeClass
इस उत्तर में मैंने जो पूर्ण कार्यान्वयन प्रदान किया है, वह बहु-इनवोकेशन समस्या, या कई अन्य समस्याओं पर विचार नहीं करता है जो कई उत्तराधिकार के संबंध में उत्पन्न होती हैं।
यह उत्तर बहुत लंबा हो रहा है। मुझे आशा है कि मैंने जो makeClass
कार्यान्वयन शामिल किया है वह अभी भी उपयोगी है, भले ही यह सही न हो। मुझे आशा है कि इस विषय में रुचि रखने वाले किसी भी व्यक्ति ने आगे पढ़ने के लिए ध्यान रखने के लिए अधिक संदर्भ प्राप्त किया है!