Ich habe durchaus die Funktion, Klassen mit Mehrfachvererbung definieren zu können. Es ermöglicht Code wie den folgenden. Insgesamt werden Sie eine vollständige Abweichung von den nativen Klassifizierungstechniken in Javascript feststellen (z. B. werden Sie das classSchlüsselwort nie sehen ):
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();
um eine Ausgabe wie diese zu erzeugen:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
So sehen die Klassendefinitionen aus:
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();
}
}));
Wir können sehen, dass jede Klassendefinition, die die makeClassFunktion verwendet, einen Objectder übergeordneten Klassennamen akzeptiert , die übergeordneten Klassen zugeordnet sind. Es akzeptiert auch eine Funktion, die Objectenthaltende Eigenschaften für die zu definierende Klasse zurückgibt . Diese Funktion hat einen Parameterprotos , der genügend Informationen enthält, um auf eine Eigenschaft zuzugreifen, die von einer der übergeordneten Klassen definiert wurde.
Das letzte Stück, das benötigt wird, ist die makeClassFunktion selbst, die ziemlich viel Arbeit leistet. Hier ist es, zusammen mit dem Rest des Codes. Ich habe makeClassziemlich stark kommentiert :
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();
Die makeClassFunktion unterstützt auch Klasseneigenschaften. Diese werden definiert, indem Eigenschaftsnamen mit dem $Symbol versehen werden (beachten Sie, dass der endgültige Eigenschaftsname, der sich ergibt, $entfernt wird). In diesem Sinne könnten wir eine spezielle DragonKlasse schreiben , die den "Typ" des Drachen modelliert, wobei die Liste der verfügbaren Drachentypen in der Klasse selbst gespeichert wird, im Gegensatz zu den Instanzen:
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 });
Die Herausforderungen der Mehrfachvererbung
Jeder, der den Code makeClassgenau befolgt hat, wird ein ziemlich bedeutendes unerwünschtes Phänomen bemerken, das stillschweigend auftritt, wenn der obige Code ausgeführt wird: Das Instanziieren von a RunningFlyingführt zu ZWEI Aufrufen des NamedKonstruktors!
Dies liegt daran, dass das Vererbungsdiagramm folgendermaßen aussieht:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
Wenn in einem Vererbungsdiagramm einer Unterklasse mehrere Pfade zu derselben übergeordneten Klasse vorhanden sind Klasse vorhanden sind, rufen Instanziierungen der Unterklasse den Konstruktor dieser übergeordneten Klasse mehrmals auf.
Dies zu bekämpfen ist nicht trivial. Schauen wir uns einige Beispiele mit vereinfachten Klassennamen an. Wir betrachten die Klasse A, die abstrakteste Elternklasse, die Klassen Bund C, die beide erben A, und die Klasse, BCdie von Bund erbt C(und daher konzeptionell "doppelt erbt" von 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');
}
}));
Wenn wir verhindern möchten, dass BCdoppelt aufgerufen wird, müssen A.prototype.initwir möglicherweise den Stil des direkten Aufrufs geerbter Konstruktoren aufgeben. Wir benötigen ein gewisses Maß an Indirektion, um zu überprüfen, ob doppelte Anrufe auftreten, und um sie kurzzuschließen, bevor sie auftreten.
Wir konnten betrachten die Parameter an die Funktion Eigenschaften geliefert zu ändern: neben protos, eine ObjectRohdaten beschreiben vererbten Eigenschaften enthält, könnten wir auch eine Nutzenfunktion eine Instanz Methode so für den Aufruf , dass Eltern Methoden auch genannt werden, aber doppelte Anrufe werden erkannt und verhindert. Werfen wir einen Blick darauf, wo wir die Parameter für Folgendes festlegen 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 ... */
};
Der gesamte Zweck der obigen Änderung makeClassbesteht darin, dass wir ein zusätzliches Argument erhalten, propertiesFnwenn wir uns aufrufen makeClass. Wir sollten auch bewusst sein , dass jede Funktion in jeder Klasse definiert nun einen Parameter , nachdem alle seine anderen empfangen kann, mit dem Namen dup, der eine ist , Setdie alle Funktionen enthält , die bereits als Folge genannt wurden , von der geerbten Methode aufrufen:
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');
}
}));
Mit diesem neuen Stil kann tatsächlich sichergestellt "Construct A"werden, dass nur einmal protokolliert wird, wenn eine Instanz von BCinitialisiert wird. Es gibt jedoch drei Nachteile, von denen der dritte sehr kritisch ist :
- Dieser Code ist weniger lesbar und wartbar geworden. Hinter der
util.invokeNoDuplicatesFunktion verbirgt sich eine Menge Komplexität , und darüber nachzudenken, wie dieser Stil Mehrfachaufrufe vermeidet, ist nicht intuitiv und verursacht Kopfschmerzen. Wir haben auch diesen lästigen dupsParameter, der wirklich für jede einzelne Funktion in der Klasse definiert werden muss . Autsch.
- Dieser Code ist langsamer - viel mehr Indirektion und Berechnung sind erforderlich, um wünschenswerte Ergebnisse bei Mehrfachvererbung zu erzielen. Leider ist dies wahrscheinlich bei jeder Lösung unseres Problems mit mehreren Aufrufen der Fall .
- Am wichtigsten ist, dass die Struktur von Funktionen, die auf Vererbung beruhen, sehr starr geworden ist . Wenn eine Unterklasse
NiftyClasseine Funktion überschreibt niftyFunctionund util.invokeNoDuplicates(this, 'niftyFunction', ...)sie ohne doppelten Aufruf ausführt, NiftyClass.prototype.niftyFunctionruft sie die Funktion niftyFunctionjeder übergeordneten Klasse auf, die sie definiert, ignoriert alle Rückgabewerte dieser Klassen und führt schließlich die spezielle Logik von aus NiftyClass.prototype.niftyFunction. Dies ist die einzig mögliche Struktur . Wenn NiftyClasserbt CoolClassund GoodClassund beide übergeordneten Klassen niftyFunctioneigene Definitionen bereitstellen , NiftyClass.prototype.niftyFunctionwird es niemals (ohne das Risiko eines Mehrfachaufrufs) möglich sein:
- A. Führen Sie zuerst die spezialisierte Logik von
NiftyClassund dann die spezialisierte Logik von Elternklassen aus
- B. Führen Sie die spezialisierte Logik
NiftyClassan einem anderen Punkt aus, als nachdem alle spezialisierten übergeordneten Logik abgeschlossen wurde
- C. Verhalten Sie sich bedingt abhängig von den Rückgabewerten der speziellen Logik des übergeordneten Elements
- D. Vermeiden Sie eine bestimmte Mutter laufen ist spezialisiert
niftyFunctioninsgesamt
Natürlich könnten wir jedes der oben genannten Probleme lösen, indem wir spezielle Funktionen definieren unter util:
- A. definieren
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B. define
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(Wo parentNameist der Name des Elternteils, auf dessen spezialisierte Logik unmittelbar die spezialisierte Logik der Kinderklassen folgt?)
- C. define
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(In diesem Fall testFnwürde das Ergebnis der speziellen Logik für das genannte übergeordnete Element empfangen parentNameund ein true/falseWert zurückgegeben, der angibt, ob der Kurzschluss auftreten sollte.)
- D. define
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(In diesem Fall handelt blackListes sich um einen Arrayübergeordneten Namen, dessen spezielle Logik insgesamt übersprungen werden sollte.)
Diese Lösungen sind alle verfügbar, aber das ist totales Chaos ! Für jede eindeutige Struktur, die ein geerbter Funktionsaufruf annehmen kann, benötigen wir eine spezielle Methode, die unter definiert ist util. Was für eine absolute Katastrophe.
Vor diesem Hintergrund können wir die Herausforderungen der Implementierung einer guten Mehrfachvererbung erkennen. Die vollständige Implementierung von makeClassI in dieser Antwort berücksichtigt nicht einmal das Problem des Mehrfachaufrufs oder viele andere Probleme, die im Zusammenhang mit der Mehrfachvererbung auftreten.
Diese Antwort wird sehr lang. Ich hoffe, dass die makeClassImplementierung, die ich aufgenommen habe, immer noch nützlich ist, auch wenn sie nicht perfekt ist. Ich hoffe auch, dass jeder, der sich für dieses Thema interessiert, mehr Kontext gewonnen hat, um sich beim weiteren Lesen daran zu erinnern!