Mehrfachvererbung / Prototypen in JavaScript

132

Ich bin an einem Punkt angelangt, an dem eine rudimentäre Mehrfachvererbung in JavaScript stattfinden muss. (Ich bin nicht hier, um zu diskutieren, ob dies eine gute Idee ist oder nicht. Bitte behalten Sie diese Kommentare für sich.)

Ich möchte nur wissen, ob jemand dies mit (oder nicht) Erfolg versucht hat und wie er es gemacht hat.

Kochen Sie es auf, was ich wirklich brauchen , um ein Objekt zu können , ist haben fähig ist , eine Eigenschaft von mehr als ein Prototyp Vererbungskette (dh jeder Prototyp könnte seine eigene richtige Kette), aber in einer bestimmten Reihenfolge des Vorrangs (es wird Durchsuchen Sie die Ketten nach der ersten Definition.

Um zu demonstrieren, wie dies theoretisch möglich ist, könnte dies durch Anbringen der Sekundärkette am Ende der Primärkette erreicht werden. Dies würde jedoch alle Instanzen eines dieser vorherigen Prototypen betreffen, und das ist nicht das, was ich möchte.

Gedanken?

devios1
quelle
1
Ich denke , Dojo declare Griffe Mehrfachvererbung src auch ich ein Gefühl Mootools auch nicht, viel davon ist mir schleierhaft , aber ich werde eine schnelle Lesen von haben dies als Dojo schon sagt
TI
Werfen Sie einen Blick auf TraitsJS ( Link 1 , Link 2 ), es ist eine wirklich gute Alternative zu Mehrfachvererbung und Mixins ...
CMS
1
@Pointy weil das nicht sehr dynamisch ist. Ich möchte in der Lage sein, Änderungen an beiden übergeordneten Ketten zu erfassen, sobald sie auftreten. Allerdings muss ich möglicherweise darauf zurückgreifen, wenn es einfach nicht möglich ist.
Devios1
Mögliches Duplikat von Javascript unterstützt Mehrfachvererbung wie C ++
Daniel Earwicker
1
Eine interessante Lektüre dazu: webreflection.blogspot.co.uk/2009/06/…
Nobita

Antworten:

49

Die Mehrfachvererbung kann in ECMAScript 6 mithilfe von Proxy-Objekten erreicht werden .

Implementierung

function getDesc (obj, prop) {
  var desc = Object.getOwnPropertyDescriptor(obj, prop);
  return desc || (obj=Object.getPrototypeOf(obj) ? getDesc(obj, prop) : void 0);
}
function multiInherit (...protos) {
  return Object.create(new Proxy(Object.create(null), {
    has: (target, prop) => protos.some(obj => prop in obj),
    get (target, prop, receiver) {
      var obj = protos.find(obj => prop in obj);
      return obj ? Reflect.get(obj, prop, receiver) : void 0;
    },
    set (target, prop, value, receiver) {
      var obj = protos.find(obj => prop in obj);
      return Reflect.set(obj || Object.create(null), prop, value, receiver);
    },
    *enumerate (target) { yield* this.ownKeys(target); },
    ownKeys(target) {
      var hash = Object.create(null);
      for(var obj of protos) for(var p in obj) if(!hash[p]) hash[p] = true;
      return Object.getOwnPropertyNames(hash);
    },
    getOwnPropertyDescriptor(target, prop) {
      var obj = protos.find(obj => prop in obj);
      var desc = obj ? getDesc(obj, prop) : void 0;
      if(desc) desc.configurable = true;
      return desc;
    },
    preventExtensions: (target) => false,
    defineProperty: (target, prop, desc) => false,
  }));
}

Erläuterung

Ein Proxy-Objekt besteht aus einem Zielobjekt und einigen Traps, die das benutzerdefinierte Verhalten für grundlegende Operationen definieren.

Wenn wir ein Objekt erstellen, das von einem anderen erbt, verwenden wir Object.create(obj) . In diesem Fall möchten wir jedoch eine Mehrfachvererbung. Stattdessen verwende objich einen Proxy, der grundlegende Operationen auf das entsprechende Objekt umleitet.

Ich benutze diese Fallen:

  • Die hasFalle ist eine Falle für den inBediener . ich benutzesome überprüfe, ob mindestens ein Prototyp die Eigenschaft enthält.
  • Die getFalle ist eine Falle zum Abrufen von Eigenschaftswerten. Ich verwende find, um den ersten Prototyp zu finden, der diese Eigenschaft enthält, und gebe den Wert zurück oder rufe den Getter auf dem entsprechenden Empfänger auf. Dies wird von erledigtReflect.get . Wenn kein Prototyp die Eigenschaft enthält, kehre ich zurück undefined.
  • Die setFalle ist eine Falle zum Festlegen von Eigenschaftswerten. Ich benutze find, um den ersten Prototyp zu finden, der diese Eigenschaft enthält, und rufe seinen Setter auf dem entsprechenden Empfänger auf. Wenn kein Setter vorhanden ist oder kein Prototyp die Eigenschaft enthält, wird der Wert auf dem entsprechenden Empfänger definiert. Dies wird von erledigt Reflect.set.
  • Die enumerateFalle ist eine Falle für for...inSchleifen . Ich iteriere die aufzählbaren Eigenschaften vom ersten Prototyp, dann vom zweiten und so weiter. Sobald eine Eigenschaft iteriert wurde, speichere ich sie in einer Hash-Tabelle, um eine erneute Iteration zu vermeiden.
    Warnung : Diese Falle wurde im ES7-Entwurf entfernt und ist in Browsern veraltet.
  • Die ownKeysFalle ist eine Falle fürObject.getOwnPropertyNames() . Seit ES7 rufen for...inSchleifen immer wieder GetPrototypeOf auf und erhalten die eigenen Eigenschaften von jedem. Um die Eigenschaften aller Prototypen zu iterieren, verwende ich diese Falle, um alle aufzählbaren geerbten Eigenschaften als eigene Eigenschaften erscheinen zu lassen.
  • Die getOwnPropertyDescriptorFalle ist eine Falle für Object.getOwnPropertyDescriptor(). Es ownKeysreicht nicht aus, alle aufzählbaren Eigenschaften als eigene Eigenschaften in der Trap anzuzeigen. for...inSchleifen erhalten den Deskriptor, um zu überprüfen, ob sie aufzählbar sind. Ich verwende also find, um den ersten Prototyp zu finden, der diese Eigenschaft enthält, und iteriere seine prototypische Kette, bis ich den Eigentümer der Eigenschaft finde, und gebe seinen Deskriptor zurück. Wenn kein Prototyp die Eigenschaft enthält, kehre ich zurückundefined . Der Deskriptor wurde geändert, um ihn konfigurierbar zu machen, da wir sonst einige Proxy-Invarianten auflösen könnten.
  • Die preventExtensionsund definePropertyTraps sind nur enthalten, um zu verhindern, dass diese Vorgänge das Proxy-Ziel ändern. Andernfalls könnten wir einige Proxy-Invarianten brechen.

Es sind weitere Fallen verfügbar, die ich nicht benutze

  • Die getPrototypeOfFalle könnte hinzugefügt werden, aber es gibt keine geeignete Möglichkeit, die mehreren Prototypen zurückzugeben. Dies impliziert, dass instanceofes auch nicht funktioniert. Daher lasse ich es den Prototyp des Ziels erhalten, der anfänglich null ist.
  • Die setPrototypeOfFalle könnte hinzugefügt werden und eine Reihe von Objekten akzeptieren, die die Prototypen ersetzen würden. Dies bleibt dem Leser als Übung. Hier lasse ich einfach den Prototyp des Ziels modifizieren, was nicht sehr nützlich ist, da keine Falle das Ziel verwendet.
  • Die deletePropertyFalle ist eine Falle zum Löschen eigener Eigenschaften. Der Proxy stellt die Vererbung dar, daher wäre dies nicht sehr sinnvoll. Ich lasse es versuchen, das Ziel zu löschen, das sowieso keine Eigenschaft haben sollte.
  • Die isExtensibleFalle ist eine Falle, um die Erweiterbarkeit zu erhalten. Nicht sehr nützlich, da eine Invariante sie zwingt, dieselbe Erweiterbarkeit wie das Ziel zurückzugeben. Also lasse ich es einfach den Vorgang zum Ziel umleiten, das erweiterbar sein wird.
  • Die applyund constructTraps sind Traps zum Aufrufen oder Instanziieren. Sie sind nur nützlich, wenn das Ziel eine Funktion oder ein Konstruktor ist.

Beispiel

// Creating objects
var o1, o2, o3,
    obj = multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3});

// Checking property existences
'a' in obj; // true   (inherited from o1)
'b' in obj; // true   (inherited from o2)
'c' in obj; // false  (not found)

// Setting properties
obj.c = 3;

// Reading properties
obj.a; // 1           (inherited from o1)
obj.b; // 2           (inherited from o2)
obj.c; // 3           (own property)
obj.d; // undefined   (not found)

// The inheritance is "live"
obj.a; // 1           (inherited from o1)
delete o1.a;
obj.a; // 3           (inherited from o3)

// Property enumeration
for(var p in obj) p; // "c", "b", "a"
Oriol
quelle
1
Gibt es nicht einige Leistungsprobleme, die selbst bei normalen Anwendungen relevant werden könnten?
Tomáš Zato - Wiedereinsetzung Monica
1
@ TomášZato Es ist langsamer als Dateneigenschaften in einem normalen Objekt, aber ich denke nicht, dass es viel schlimmer sein wird als Accessor-Eigenschaften.
Oriol
Bis:multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
BloodyKnuckles
4
Ich würde in Betracht ziehen, "Mehrfachvererbung" durch "Mehrfachdelegation" zu ersetzen, um eine bessere Vorstellung davon zu bekommen, was los ist. Das Schlüsselkonzept in Ihrer Implementierung besteht darin, dass der Proxy tatsächlich das richtige Objekt zum Delegieren (oder Weiterleiten) der Nachricht auswählt. Die Stärke Ihrer Lösung besteht darin, dass Sie die Zielprototypen dynamisch erweitern können. Andere Antworten verwenden die Verkettung (ala Object.assign) oder ein ganz anderes Diagramm. Am Ende erhalten alle eine einzige Prototypkette zwischen Objekten. Die Proxy-Lösung bietet eine Laufzeitverzweigung, und das rockt!
Sminutoli
Wenn Sie ein Objekt erstellen, das von mehreren Objekten erbt, das von mehreren Objekten erbt usw., wird es exponentiell. Also ja, es wird langsamer sein. Aber im Normalfall denke ich nicht, dass es so schlimm sein wird.
Oriol
16

Update (2019): Der ursprüngliche Beitrag ist ziemlich veraltet. Dieser Artikel (jetzt Internet-Archiv-Link, da die Domain weg ist) und die dazugehörige GitHub-Bibliothek sind ein guter moderner Ansatz.

Ursprünglicher Beitrag: Mehrfachvererbung [Bearbeiten, keine ordnungsgemäße Vererbung des Typs, sondern der Eigenschaften; mixins] in Javascript ist ziemlich einfach, wenn Sie konstruierte Prototypen anstelle von generischen Objekten verwenden. Hier sind zwei übergeordnete Klassen, von denen geerbt werden soll:

function FoodPrototype() {
    this.eat = function () {
        console.log("Eating", this.name);
    };
}
function Food(name) {
    this.name = name;
}
Food.prototype = new FoodPrototype();


function PlantPrototype() {
    this.grow = function () {
        console.log("Growing", this.name);
    };
}
function Plant(name) {
    this.name = name;
}
Plant.prototype = new PlantPrototype();

Beachten Sie, dass ich in jedem Fall das gleiche "Name" -Mitglied verwendet habe. Dies könnte ein Problem sein, wenn die Eltern nicht einverstanden sind, wie mit "Name" umgegangen werden soll. Aber sie sind in diesem Fall kompatibel (wirklich redundant).

Jetzt brauchen wir nur noch eine Klasse, die von beiden erbt. Die Vererbung erfolgt durch Aufrufen der Konstruktorfunktion (ohne Verwendung des neuen Schlüsselworts) für die Prototypen und die Objektkonstruktoren. Zunächst muss der Prototyp von den übergeordneten Prototypen erben

function FoodPlantPrototype() {
    FoodPrototype.call(this);
    PlantPrototype.call(this);
    // plus a function of its own
    this.harvest = function () {
        console.log("harvest at", this.maturity);
    };
}

Und der Konstruktor muss von den übergeordneten Konstruktoren erben:

function FoodPlant(name, maturity) {
    Food.call(this, name);
    Plant.call(this, name);
    // plus a property of its own
    this.maturity = maturity;
}

FoodPlant.prototype = new FoodPlantPrototype();

Jetzt können Sie verschiedene Instanzen anbauen, essen und ernten:

var fp1 = new FoodPlant('Radish', 28);
var fp2 = new FoodPlant('Corn', 90);

fp1.grow();
fp2.grow();
fp1.harvest();
fp1.eat();
fp2.harvest();
fp2.eat();
Roy J.
quelle
Können Sie dies mit eingebauten Prototypen tun? (Array, String, Number)
Tomáš Zato - Wiedereinsetzung Monica
Ich glaube nicht, dass die eingebauten Prototypen Konstruktoren haben, die Sie aufrufen können.
Roy J
Nun, ich kann es tun, Array.call(...)aber es scheint keinen Einfluss darauf zu haben, was ich als passiere this.
Tomáš Zato - Wiedereinsetzung Monica
@ TomášZato Sie könnten tunArray.prototype.constructor.call()
Roy J
1
@AbhishekGupta Danke, dass du mich informiert hast. Ich habe den Link durch einen Link zur archivierten Webseite ersetzt.
Roy J
7

Dieser verwendet Object.create, um eine echte Prototypkette zu erstellen:

function makeChain(chains) {
  var c = Object.prototype;

  while(chains.length) {
    c = Object.create(c);
    $.extend(c, chains.pop()); // some function that does mixin
  }

  return c;
}

Beispielsweise:

var obj = makeChain([{a:1}, {a: 2, b: 3}, {c: 4}]);

wird zurückkehren:

a: 1
  a: 2
  b: 3
    c: 4
      <Object.prototype stuff>

so dass obj.a === 1, obj.b === 3usw.

pimvdb
quelle
Nur eine kurze hypothetische Frage: Ich wollte die Vector-Klasse durch Mischen von Number- und Array-Prototypen (zum Spaß) erstellen. Dies würde mir sowohl Array-Indizes als auch mathematische Operatoren geben. Aber würde es funktionieren?
Tomáš Zato - Wiedereinsetzung Monica
@ TomášZato, es lohnt sich, diesen Artikel zu lesen, wenn Sie sich mit Unterklassen-Arrays befassen . Es könnte Ihnen Kopfschmerzen ersparen. Viel Glück!
user3276552
5

Ich mag John Resigs Implementierung einer Klassenstruktur: http://ejohn.org/blog/simple-javascript-inheritance/

Dies kann einfach erweitert werden auf:

Class.extend = function(prop /*, prop, prop, prop */) {
    for( var i=1, l=arguments.length; i<l; i++ ){
        prop = $.extend( prop, arguments[i] );
    }

    // same code
}

Dadurch können Sie mehrere Objekte übergeben, von denen Sie erben möchten. Sie werden instanceOfhier die Fähigkeit verlieren , aber das ist eine Selbstverständlichkeit, wenn Sie mehrere Vererbungen wünschen.


Mein ziemlich kompliziertes Beispiel dafür finden Sie unter https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js

Beachten Sie, dass diese Datei einen toten Code enthält, der jedoch eine Mehrfachvererbung ermöglicht, wenn Sie einen Blick darauf werfen möchten.


Wenn Sie eine verkettete Vererbung wünschen (NICHT Mehrfachvererbung, aber für die meisten Menschen ist es dasselbe), kann dies mit einer Klasse wie der folgenden erreicht werden:

var newClass = Class.extend( cls1 ).extend( cls2 ).extend( cls3 )

Dadurch bleibt die ursprüngliche Prototypenkette erhalten, aber es wird auch viel sinnloser Code ausgeführt.

Mark Kahn
quelle
7
Dadurch wird ein zusammengeführter flacher Klon erstellt. Das Hinzufügen einer neuen Eigenschaft zu den "geerbten" Objekten führt nicht dazu, dass die neue Eigenschaft auf dem abgeleiteten Objekt angezeigt wird, wie dies bei einer echten Prototypvererbung der Fall wäre.
Daniel Earwicker
@DanielEarwicker - Stimmt, aber wenn Sie "Mehrfachvererbung" in dieser einen Klasse wünschen, die von zwei Klassen abgeleitet ist, gibt es keine wirkliche Alternative. Geänderte Antwort, um zu reflektieren, dass das einfache Verketten von Klassen in den meisten Fällen dasselbe ist.
Mark Kahn
Es scheint, dass dein GitHUb weg ist. Hast du noch github.com/cwolves/Fetch/blob/master/support/plugins/klass/… ? Es würde mir nichts ausmachen, es dir anzusehen , wenn du es teilen möchtest ?
JasonDavis
4

Verwechseln Sie sich nicht mit JavaScript-Framework-Implementierungen mit Mehrfachvererbung.

Alles, was Sie tun müssen, ist, mit Object.create () jedes Mal ein neues Objekt mit dem angegebenen Prototypobjekt und den angegebenen Eigenschaften zu erstellen. Ändern Sie dann den Object.prototype.constructor in jedem Schritt des Weges, wenn Sie die Instanziierung Bin planen möchten Zukunft.

Erben Eigenschaften der Instanz thisAund thisBverwenden wir Function.prototype.call () am Ende jeder Objektfunktion. Dies ist optional, wenn Sie nur den Prototyp erben möchten.

Führen Sie irgendwo den folgenden Code aus und beachten Sie objC:

function A() {
  this.thisA = 4; // objC will contain this property
}

A.prototype.a = 2; // objC will contain this property

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

function B() {
  this.thisB = 55; // objC will contain this property

  A.call(this);
}

B.prototype.b = 3; // objC will contain this property

C.prototype = Object.create(B.prototype);
C.prototype.constructor = C;

function C() {
  this.thisC = 123; // objC will contain this property

  B.call(this);
}

C.prototype.c = 2; // objC will contain this property

var objC = new C();
  • B erbt den Prototyp von A
  • C erbt den Prototyp von B
  • objC ist eine Instanz von C

Dies ist eine gute Erklärung für die obigen Schritte:

OOP In JavaScript: Was Sie wissen müssen

Dave
quelle
Kopiert dies jedoch nicht alle Eigenschaften in das neue Objekt? Wenn Sie also zwei Prototypen haben, A und B, und beide auf C neu erstellen, wirkt sich das Ändern einer Eigenschaft von A nicht auf diese Eigenschaft auf C aus und umgekehrt. Am Ende erhalten Sie eine Kopie aller Eigenschaften in A und B, die im Speicher gespeichert sind. Es wäre die gleiche Leistung, als hätten Sie alle Eigenschaften von A und B fest in C codiert. Dies ist für die Lesbarkeit gut geeignet, und die Suche nach Eigenschaften muss nicht zu übergeordneten Objekten erfolgen, ist jedoch keine echte Vererbung - eher wie Klonen. Das Ändern einer Eigenschaft auf A ändert nicht die geklonte Eigenschaft auf C.
Frank
2

Ich bin in keiner Weise ein Experte für Javascript OOP, aber wenn ich Sie richtig verstehe, möchten Sie so etwas wie (Pseudocode):

Earth.shape = 'round';
Animal.shape = 'random';

Cat inherit from (Earth, Animal);

Cat.shape = 'random' or 'round' depending on inheritance order;

In diesem Fall würde ich etwas versuchen wie:

var Earth = function(){};
Earth.prototype.shape = 'round';

var Animal = function(){};
Animal.prototype.shape = 'random';
Animal.prototype.head = true;

var Cat = function(){};

MultiInherit(Cat, Earth, Animal);

console.log(new Cat().shape); // yields "round", since I reversed the inheritance order
console.log(new Cat().head); // true

function MultiInherit() {
    var c = [].shift.call(arguments),
        len = arguments.length
    while(len--) {
        $.extend(c.prototype, new arguments[len]());
    }
}
David Hellsing
quelle
1
Wählt das nicht einfach den ersten Prototyp aus und ignoriert den Rest? c.prototypeMehrfaches Setzen führt nicht zu mehreren Prototypen. Wenn Sie zum Beispiel hätten Animal.isAlive = true, Cat.isAlivewäre immer noch undefiniert.
Devios1
Ja, ich wollte die Prototypen mischen, korrigiert ... (Ich habe jQuery's Extend hier verwendet, aber Sie bekommen das Bild)
David Hellsing
2

Es ist möglich, Mehrfachvererbung in JavaScript zu implementieren, obwohl dies nur sehr wenige Bibliotheken tun.

Ich könnte auf Ring.js verweisen , das einzige Beispiel, das ich kenne.

nicolas-van
quelle
2

Ich habe heute viel daran gearbeitet und versucht, dies selbst in ES6 zu erreichen. Ich habe es mit Browserify, Babel und dann mit Wallaby getestet und es schien zu funktionieren. Mein Ziel ist es, das aktuelle Array zu erweitern, ES6, ES7 einzuschließen und einige zusätzliche benutzerdefinierte Funktionen hinzuzufügen, die ich im Prototyp für den Umgang mit Audiodaten benötige.

Wallaby besteht 4 meiner Tests. Die Datei example.js kann in die Konsole eingefügt werden, und Sie können sehen, dass sich die Eigenschaft 'include' im Prototyp der Klasse befindet. Ich möchte das morgen noch mehr testen.

Hier ist meine Methode: (Ich werde höchstwahrscheinlich nach etwas Schlaf als Modul umgestalten und neu verpacken!)

var includes = require('./polyfills/includes');
var keys =  Object.getOwnPropertyNames(includes.prototype);
keys.shift();

class ArrayIncludesPollyfills extends Array {}

function inherit (...keys) {
  keys.map(function(key){
      ArrayIncludesPollyfills.prototype[key]= includes.prototype[key];
  });
}

inherit(keys);

module.exports = ArrayIncludesPollyfills

Github Repo: https://github.com/danieldram/array-includes-polyfill

Daniel Ram
quelle
2

Ich finde es lächerlich einfach. Das Problem hierbei ist, dass sich die untergeordnete Klasse nur auf instanceofdie erste Klasse bezieht, die Sie aufrufen

https://jsfiddle.net/1033xzyt/19/

function Foo() {
  this.bar = 'bar';
  return this;
}
Foo.prototype.test = function(){return 1;}

function Bar() {
  this.bro = 'bro';
  return this;
}
Bar.prototype.test2 = function(){return 2;}

function Cool() {
  Foo.call(this);
  Bar.call(this);

  return this;
}

var combine = Object.create(Foo.prototype);
$.extend(combine, Object.create(Bar.prototype));

Cool.prototype = Object.create(combine);
Cool.prototype.constructor = Cool;

var cool = new Cool();

console.log(cool.test()); // 1
console.log(cool.test2()); //2
console.log(cool.bro) //bro
console.log(cool.bar) //bar
console.log(cool instanceof Foo); //true
console.log(cool instanceof Bar); //false
BarryBones41
quelle
1

Überprüfen Sie den Code, unter dem IS die Unterstützung für Mehrfachvererbung anzeigt. Geschehen mit PROTOTYPAL INHERITANCE

function A(name) {
    this.name = name;
}
A.prototype.setName = function (name) {

    this.name = name;
}

function B(age) {
    this.age = age;
}
B.prototype.setAge = function (age) {
    this.age = age;
}

function AB(name, age) {
    A.prototype.setName.call(this, name);
    B.prototype.setAge.call(this, age);
}

AB.prototype = Object.assign({}, Object.create(A.prototype), Object.create(B.prototype));

AB.prototype.toString = function () {
    return `Name: ${this.name} has age: ${this.age}`
}

const a = new A("shivang");
const b = new B(32);
console.log(a.name);
console.log(b.age);
const ab = new AB("indu", 27);
console.log(ab.toString());
Shivang Gupta
quelle
1

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 :

  1. 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.
  2. 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 .
  3. 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. definierenutil.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!

Gershom
quelle
0

Schauen Sie sich das Paket IeUnit an .

Die in IeUnit implementierte Konzeptassimilation scheint auf sehr dynamische Weise das zu bieten, wonach Sie suchen.

James
quelle
0

Hier ist ein Beispiel für die Verkettung von Prototypen mithilfe von Konstruktorfunktionen :

function Lifeform () {             // 1st Constructor function
    this.isLifeform = true;
}

function Animal () {               // 2nd Constructor function
    this.isAnimal = true;
}
Animal.prototype = new Lifeform(); // Animal is a lifeform

function Mammal () {               // 3rd Constructor function
    this.isMammal = true;
}
Mammal.prototype = new Animal();   // Mammal is an animal

function Cat (species) {           // 4th Constructor function
    this.isCat = true;
    this.species = species
}
Cat.prototype = new Mammal();     // Cat is a mammal

Dieses Konzept verwendet Yehuda Katz 'Definition einer "Klasse" für JavaScript:

... eine JavaScript- "Klasse" ist nur ein Funktionsobjekt, das als Konstruktor und ein angefügtes Prototypobjekt dient. ( Quelle: Guru Katz )

Im Gegensatz zum Object.create-Ansatz müssen wir nicht wissen, von was jede "Klasse" erbt, wenn die Klassen auf diese Weise erstellt werden und wir Instanzen einer "Klasse" erstellen möchten. Wir benutzen nur new.

// Make an instance object of the Cat "Class"
var tiger = new Cat("tiger");

console.log(tiger.isCat, tiger.isMammal, tiger.isAnimal, tiger.isLifeform);
// Outputs: true true true true

Die Reihenfolge der Präzision sollte sinnvoll sein. Zuerst sieht es im Instanzobjekt aus, dann im Prototyp, dann im nächsten Prototyp usw.

// Let's say we have another instance, a special alien cat
var alienCat = new Cat("alien");
// We can define a property for the instance object and that will take 
// precendence over the value in the Mammal class (down the chain)
alienCat.isMammal = false;
// OR maybe all cats are mutated to be non-mammals
Cat.prototype.isMammal = false;
console.log(alienCat);

Wir können auch die Prototypen ändern, die sich auf alle auf der Klasse erstellten Objekte auswirken.

// All cats are mutated to be non-mammals
Cat.prototype.isMammal = false;
console.log(tiger, alienCat);

Ich habe ursprünglich einiges davon mit dieser Antwort geschrieben .

Luke
quelle
2
Das OP fordert mehrere Prototypketten an (z. B. childerbt von parent1und parent2). Ihr Beispiel spricht nur von einer Kette.
vornehmster
0

Ein Neuling in der Szene ist SimpleDeclare . Wenn Sie sich jedoch mit Mehrfachvererbung befassen, erhalten Sie immer noch Kopien der ursprünglichen Konstruktoren. Das ist eine Notwendigkeit in Javascript ...

Merc.

Merc
quelle
Das ist eine Notwendigkeit in Javascript ... bis zu ES6 Proxies.
Jonathon
Proxies sind interessant! Ich werde auf jeden Fall versuchen, SimpleDeclare so zu ändern, dass Methoden nicht mehr über Proxys kopiert werden müssen, sobald sie Teil des Standards sind. SimpleDeclare Code ist wirklich sehr, sehr einfach zu lesen und zu ändern ...
Merc
0

Ich würde ds.oop verwenden . Es ähnelt prototype.js und anderen. macht Mehrfachvererbung sehr einfach und minimalistisch. (nur 2 oder 3 kb) Unterstützt auch einige andere nette Funktionen wie Schnittstellen und Abhängigkeitsinjektion

/*** multiple inheritance example ***********************************/

var Runner = ds.class({
    run: function() { console.log('I am running...'); }
});

var Walker = ds.class({
    walk: function() { console.log('I am walking...'); }
});

var Person = ds.class({
    inherits: [Runner, Walker],
    eat: function() { console.log('I am eating...'); }
});

var person = new Person();

person.run();
person.walk();
person.eat();
dss
quelle
0

Wie wäre es damit, es implementiert Mehrfachvererbung in JavaScript:

    class Car {
        constructor(brand) {
            this.carname = brand;
        }
        show() {
            return 'I have a ' + this.carname;
        }
    }

    class Asset {
        constructor(price) {
            this.price = price;
        }
        show() {
            return 'its estimated price is ' + this.price;
        }
    }

    class Model_i1 {        // extends Car and Asset (just a comment for ourselves)
        //
        constructor(brand, price, usefulness) {
            specialize_with(this, new Car(brand));
            specialize_with(this, new Asset(price));
            this.usefulness = usefulness;
        }
        show() {
            return Car.prototype.show.call(this) + ", " + Asset.prototype.show.call(this) + ", Model_i1";
        }
    }

    mycar = new Model_i1("Ford Mustang", "$100K", 16);
    document.getElementById("demo").innerHTML = mycar.show();

Und hier ist der Code für die Dienstprogrammfunktion specialize_with ():

function specialize_with(o, S) { for (var prop in S) { o[prop] = S[prop]; } }

Dies ist echter Code, der ausgeführt wird. Sie können es kopieren, in eine HTML-Datei einfügen und selbst ausprobieren. Es funktioniert.

Das ist der Aufwand, MI in JavaScript zu implementieren. Nicht viel Code, eher Know-how.

Bitte schauen Sie sich meinen vollständigen Artikel dazu an, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS

Leonid Titov
quelle
0

Ich habe nur zugewiesen, welche Klassen ich in den Eigenschaften anderer benötige, und einen Proxy hinzugefügt, um ihnen automatisch zu zeigen, was mir gefällt:

class A {
    constructor()
    {
        this.test = "a test";
    }

    method()
    {
        console.log("in the method");
    }
}

class B {
    constructor()
    {
        this.extends = [new A()];

        return new Proxy(this, {
            get: function(obj, prop) {

                if(prop in obj)
                    return obj[prop];

                let response = obj.extends.find(function (extended) {
                if(prop in extended)
                    return extended[prop];
            });

            return response ? response[prop] : Reflect.get(...arguments);
            },

        })
    }
}

let b = new B();
b.test ;// "a test";
b.method(); // in the method
Schamaseen
quelle