Eredità / prototipi multipli in JavaScript


132

Sono arrivato al punto in cui devo avere una sorta di eredità multipla rudimentale in JavaScript. (Non sono qui per discutere se questa è una buona idea o no, quindi ti preghiamo gentilmente di tenere quei commenti per te.)

Voglio solo sapere se qualcuno ha tentato questo con qualche successo (o meno) e come hanno fatto.

Per ridurlo, ciò di cui ho veramente bisogno è poter avere un oggetto in grado di ereditare una proprietà da più di una catena di prototipi (ovvero ogni prototipo potrebbe avere la propria catena), ma in un dato ordine di precedenza (lo farà cerca le catene in ordine per la prima definizione).

Per dimostrare come ciò sia teoricamente possibile, si potrebbe ottenere attaccando la catena secondaria all'estremità della catena primaria, ma ciò influenzerebbe tutte le istanze di uno di quei prototipi precedenti e non è quello che voglio.

Pensieri?


1
Penso dojo di dichiarazione delle maniglie ereditarietà multipla src anche Ho la sensazione mootools fa anche, molto di questo è al di là di me, ma sto andando avere una rapida lettura di questo , come suggerisce dojo
TI

Dai un'occhiata a TraitsJS ( link 1 , link 2 ) è davvero un'ottima alternativa all'ereditarietà multipla e ai mixin ...
CMS

1
@Pointy perché non è molto dinamico. Mi piacerebbe essere in grado di raccogliere le modifiche apportate a entrambe le catene madri non appena si verificano. Tuttavia, detto questo, potrei dover ricorrere a questo se non fosse possibile.
devios1,


1
Una lettura interessante a riguardo: webreflection.blogspot.co.uk/2009/06/…
Nobita

Risposte:


49

L'ereditarietà multipla può essere ottenuta in ECMAScript 6 usando oggetti Proxy .

Implementazione

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,
  }));
}

Spiegazione

Un oggetto proxy è costituito da un oggetto di destinazione e da alcune trappole, che definiscono un comportamento personalizzato per le operazioni fondamentali.

Quando creiamo un oggetto che eredita da un altro, usiamo Object.create(obj). Ma in questo caso vogliamo l'ereditarietà multipla, quindi invece di objusare un proxy che reindirizzerà le operazioni fondamentali sull'oggetto appropriato.

Uso queste trappole:

  • La hastrappola è una trappola per l' inoperatore . Uso someper verificare se almeno un prototipo contiene la proprietà.
  • La gettrap è una trap per ottenere valori di proprietà. Uso findper trovare il primo prototipo che contiene quella proprietà e restituisco il valore o chiamo il getter sul ricevitore appropriato. Questo è gestito da Reflect.get. Se nessun prototipo contiene la proprietà, ritorno undefined.
  • La settrap è una trap per l'impostazione dei valori delle proprietà. Io usofind per trovare il primo prototipo che contiene quella proprietà e chiamo il suo setter sul ricevitore appropriato. Se non è presente alcun setter o nessun prototipo contiene la proprietà, il valore viene definito sul ricevitore appropriato. Questo è gestito da Reflect.set.
  • La enumeratetrappola è una trappola per i for...inloop . Ripeto le enumerabili proprietà dal primo prototipo, quindi dal secondo e così via. Una volta che una proprietà è stata ripetuta, la memorizzo in una tabella hash per evitare di ripetere la ripetizione.
    Avvertenza : questa trappola è stata rimossa nella bozza di ES7 ed è obsoleta nei browser.
  • La ownKeystrappola è una trappola perObject.getOwnPropertyNames() . Da ES7, i for...inloop continuano a chiamare [[GetPrototypeOf]] e ottenere le proprie proprietà di ciascuno. Quindi, al fine di far iterare le proprietà di tutti i prototipi, uso questa trap per far apparire tutte le proprietà ereditate enumerabili come proprietà proprie.
  • La getOwnPropertyDescriptortrappola è una trappola per Object.getOwnPropertyDescriptor(). Far apparire tutte le proprietà enumerabili come proprietà proprie nella ownKeystrap non è sufficiente, i for...inloop faranno in modo che il descrittore controlli se sono enumerabili. Quindi io usofind trovo il primo prototipo che contiene quella proprietà, e ripeto la sua catena prototipica fino a quando non trovo il proprietario della proprietà, e restituisco il suo descrittore. Se nessun prototipo contiene la proprietà, ritorno undefined. Il descrittore viene modificato per renderlo configurabile, altrimenti potremmo rompere alcuni invarianti di proxy.
  • Le trap preventExtensionse definePropertysono incluse solo per impedire a queste operazioni di modificare la destinazione del proxy. Altrimenti potremmo finire per rompere alcuni invarianti per procura.

Ci sono più trappole disponibili, che non uso

  • La getPrototypeOftrappola possibile aggiungere la , ma non esiste un modo corretto per restituire i prototipi multipli. Ciò implica instanceofche non funzionerà neanche. Pertanto, ho lasciato che ottenga il prototipo del target, che inizialmente è nullo.
  • La setPrototypeOftrappola potrebbe essere aggiunta e accettare una matrice di oggetti, che rimpiazzerebbe i prototipi. Questo è lasciato come esercizio per il lettore. Qui ho appena lasciato modificare il prototipo del bersaglio, il che non è molto utile perché nessuna trappola utilizza il bersaglio.
  • La deletePropertytrappola è una trap per l'eliminazione delle proprie proprietà. Il proxy rappresenta l'eredità, quindi questo non avrebbe molto senso. Ho lasciato tentare la cancellazione sul bersaglio, che comunque non dovrebbe avere proprietà.
  • La isExtensibletrappola è una trappola per ottenere l'estensibilità. Non molto utile, dato che un invariante lo costringe a restituire la stessa estensibilità del bersaglio. Quindi ho appena lasciato che reindirizzi l'operazione verso il target, che sarà estensibile.
  • Le trappole applye constructsono trappole per chiamare o creare un'istanza. Sono utili solo quando l'obiettivo è una funzione o un costruttore.

Esempio

// 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"

1
Non ci sono alcuni problemi di prestazioni che potrebbero diventare rilevanti anche su normali applicazioni in scala?
Tomáš Zato - Ripristina Monica il

1
@ TomášZato Sarà più lento delle proprietà dei dati in un oggetto normale, ma non penso che sarà molto peggio delle proprietà degli accessori.
Oriol,

TIL:multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
bloodyKnuckles,

4
Vorrei prendere in considerazione la sostituzione di "Eredità multipla" con "Delega multipla" per avere un'idea migliore di ciò che sta accadendo. Il concetto chiave nella tua implementazione è che il proxy sta effettivamente scegliendo l'oggetto giusto per delegare (o inoltrare) il messaggio. Il potere della tua soluzione è che puoi estendere dinamicamente il / i prototipo / i target. Altre risposte stanno usando la concatenazione (ala Object.assign) o ottenendo un grafico abbastanza diverso, alla fine tutti stanno ottenendo una catena di prototipi unica tra gli oggetti. La soluzione proxy offre una ramificazione di runtime, e questo è incredibile!
sminutoli,

Per quanto riguarda le prestazioni, se si crea un oggetto che eredita da più oggetti, che ereditano da più oggetti e così via, diventerà esponenziale. Quindi sì, sarà più lento. Ma in casi normali non penso che sarà così male.
Oriol,

16

Aggiornamento (2019): il post originale sta diventando piuttosto obsoleto. Questo articolo (ora link all'archivio Internet, da quando il dominio è scomparso) e la sua libreria GitHub associata sono un buon approccio moderno.

Post originale: ereditarietà multipla [modifica, eredità non corretta del tipo, ma delle proprietà; mixins] in Javascript è piuttosto semplice se usi prototipi costruiti piuttosto che oggetti generici. Ecco due classi principali da cui ereditare:

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();

Nota che ho usato lo stesso membro "name" in ogni caso, il che potrebbe essere un problema se i genitori non fossero d'accordo su come gestire "name". Ma sono compatibili (ridondanti, davvero) in questo caso.

Ora abbiamo solo bisogno di una classe che eredita da entrambi. L'ereditarietà viene effettuata chiamando la funzione di costruzione (senza utilizzare la nuova parola chiave) per i prototipi e i costruttori di oggetti. Innanzitutto, il prototipo deve ereditare dai prototipi padre

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

E il costruttore deve ereditare dai costruttori genitori:

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();

Ora puoi coltivare, mangiare e raccogliere diversi casi:

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

fp1.grow();
fp2.grow();
fp1.harvest();
fp1.eat();
fp2.harvest();
fp2.eat();

Puoi farlo con prototipi integrati? (Array, String, Number)
Tomáš Zato - Reinstate Monica il

Non credo che i prototipi integrati abbiano costruttori che puoi chiamare.
Roy J,

Bene, posso fare Array.call(...)ma non sembra influenzare qualunque cosa io passi this.
Tomáš Zato - Ripristina Monica il

@ TomášZato Potresti farloArray.prototype.constructor.call()
Roy J,

1
@AbhishekGupta Grazie per avermelo fatto notare. Ho sostituito il collegamento con un collegamento alla pagina Web archiviata.
Roy J,

7

Questo utilizza Object.createper creare una vera catena di prototipi:

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;
}

Per esempio:

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

sarà di ritorno:

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

cosicché obj.a === 1, obj.b === 3ecc


Solo una rapida domanda ipotetica: volevo fare lezione Vector mescolando i prototipi Number e Array (per divertimento). Questo mi darebbe sia indici di array che operatori matematici. Ma funzionerebbe?
Tomáš Zato - Ripristina Monica il

@ TomášZato, vale la pena dare un'occhiata a questo articolo se stai cercando array di sottoclassi; potrebbe risparmiarti un po 'di mal di testa. in bocca al lupo!
user3276552

5

Mi piace l'implementazione di John Resig di una struttura di classe: http://ejohn.org/blog/simple-javascript-inheritance/

Questo può essere semplicemente esteso a qualcosa come:

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

    // same code
}

che ti permetterà di passare in più oggetti di cui ereditare. Perderai instanceOfcapacità qui, ma è un dato di fatto se vuoi ereditarietà multipla.


il mio esempio piuttosto contorto di quanto sopra è disponibile su https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js

Nota che c'è un codice morto in quel file, ma consente l'ereditarietà multipla se vuoi dare un'occhiata.


Se vuoi l'eredità concatenata (NON l'ereditarietà multipla, ma per la maggior parte delle persone è la stessa cosa), può essere realizzata con Class come:

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

che conserverà la catena prototipo originale, ma avrai anche molto codice inutile in esecuzione.


7
Questo crea un clone superficiale unito. L'aggiunta di una nuova proprietà agli oggetti "ereditati" non farà apparire la nuova proprietà sull'oggetto derivato, come nella vera eredità del prototipo.
Daniel Earwicker,

@DanielEarwicker - Vero, ma se vuoi "ereditarietà multipla" in quella classe deriva da due classi, non c'è davvero un'alternativa. Risposta modificata per riflettere che semplicemente concatenare le lezioni insieme è la stessa cosa nella maggior parte dei casi.
Mark Kahn,

Sembra che il tuo GitHUb sia sparito, hai ancora github.com/cwolves/Fetch/blob/master/support/plugins/klass/… Non mi dispiacerebbe guardarlo se ti interessa condividere?
JasonDavis,

4

Non confonderti con le implementazioni del framework JavaScript di ereditarietà multipla.

Tutto quello che devi fare è usare Object.create () per creare un nuovo oggetto ogni volta con l'oggetto prototipo e le proprietà specificati, quindi assicurati di cambiare Object.prototype.constructor ad ogni passo se prevedi di creare un'istanza Bnel futuro.

Per ereditare le proprietà dell'istanza thisAe thisBusiamo Function.prototype.call () alla fine di ogni funzione dell'oggetto. Questo è facoltativo se ti interessa solo ereditare il prototipo.

Esegui il seguente codice da qualche parte e osserva 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 eredita il prototipo da A
  • C eredita il prototipo da B
  • objC è un'istanza di C

Questa è una buona spiegazione dei passaggi precedenti:

OOP in JavaScript: cosa devi sapere


Questo però non copia tutte le proprietà nel nuovo oggetto? Quindi se hai due prototipi, A e B, e li crei entrambi su C, la modifica di una proprietà di A non influirà su quella proprietà su C e viceversa. Si finirà con una copia di tutte le proprietà in A e B memorizzate. Sarebbe la stessa prestazione di se avessi codificato a fondo tutte le proprietà di A e B in C. È bello per la leggibilità e la ricerca delle proprietà non deve viaggiare verso gli oggetti padre, ma non è realmente ereditarietà - più come la clonazione. La modifica di una proprietà su A non modifica la proprietà clonata su C.
Frank,

2

Non sono in alcun modo un esperto di javascript OOP, ma se ti capisco correttamente vuoi qualcosa di simile (pseudo-codice):

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

Cat inherit from (Earth, Animal);

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

In tal caso, proverei qualcosa di simile:

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]());
    }
}

1
Non è solo scegliere il primo prototipo e ignorare il resto? L'impostazione di c.prototypepiù volte non produce più prototipi. Ad esempio, se avessi Animal.isAlive = true, Cat.isAlivesarebbe ancora indefinito.
devios1,

Sì, intendevo mescolare i prototipi, corretto ... (Ho usato l'estensione di jQuery qui, ma ottieni l'immagine)
David Hellsing

2

È possibile implementare l'ereditarietà multipla in JavaScript, anche se poche librerie lo fanno.

Potrei indicare Ring.js , l'unico esempio che conosco.


2

Ci stavo lavorando molto oggi e stavo provando a farlo da solo in ES6. Il modo in cui l'ho fatto è stato usando Browserify, Babel e poi l'ho provato con Wallaby e sembrava funzionare. Il mio obiettivo è estendere l'attuale array, includere ES6, ES7 e aggiungere alcune funzionalità personalizzate aggiuntive di cui ho bisogno nel prototipo per gestire i dati audio.

Wallaby supera 4 dei miei test. Il file example.js può essere incollato nella console e puoi vedere che la proprietà 'Includes' è nel prototipo della classe. Voglio ancora testarlo ancora domani.

Ecco il mio metodo: (Molto probabilmente rifatterò e riconfezionerò come modulo dopo un po 'di sonno!)

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


2

Penso che sia ridicolmente semplice. Il problema qui è che la classe figlio farà riferimento solo alla instanceofprima classe che chiami

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

1

Controlla il codice sotto il quale IS mostra il supporto per l'ereditarietà multipla. Fatto utilizzando 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());

1

Ho abbastanza la funzione di consentire alle classi di essere definite con ereditarietà multipla. Permette codice come il seguente. Complessivamente noterai una completa deviazione dalle tecniche di classificazione native in javascript (ad esempio non vedrai mai la classparola chiave):

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();

per produrre output in questo modo:

human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!

Ecco come appaiono le definizioni delle classi:

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();
  }
}));

Possiamo vedere che ogni definizione di classe usando la makeClassfunzione accetta un nome Objectdi classe genitore associato a classi genitore. Accetta anche una funzione che restituisce una Objectproprietà contenente per la classe che viene definita. Questa funzione ha un parametro protos, che contiene informazioni sufficienti per accedere a qualsiasi proprietà definita da una delle classi principali.

L'ultimo pezzo richiesto è la makeClassfunzione stessa, che fa un bel po 'di lavoro. Eccolo, insieme al resto del codice. Ho commentato makeClassabbastanza pesantemente:

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();

La makeClassfunzione supporta anche le proprietà di classe; questi sono definiti prefissando i nomi delle proprietà con il $simbolo (si noti che il nome della proprietà finale risultante verrà $rimosso). Con questo in mente, potremmo scrivere una Dragonclasse specializzata che modella il "tipo" del Drago, dove l'elenco dei tipi di Drago disponibili è memorizzato nella Classe stessa, al contrario delle istanze:

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 });

Le sfide dell'ereditarietà multipla

Chiunque abbia seguito da makeClassvicino il codice noterà un fenomeno indesiderabile piuttosto significativo che si verifica silenziosamente quando viene eseguito il codice sopra: la creazione di un'istanza RunningFlyingcomporterà DUE chiamate al Namedcostruttore!

Questo perché il grafico dell'ereditarietà è simile al seguente:

 (^^ More Specialized ^^)

      RunningFlying
         /     \
        /       \
    Running   Flying
         \     /
          \   /
          Named

  (vv More Abstract vv)

Quando ci sono più percorsi per la stessa classe genitore in un grafico dell'ereditarietà della sottoclasse, le istanze della sottoclasse richiameranno più volte il costruttore di quella classe genitore.

Combattere questo non è banale. Diamo un'occhiata ad alcuni esempi con nomi di classe semplificati. Considereremo la classe A, la classe genitore più astratta, le classi Be C, entrambe ereditate da A, e la classe BCche eredita da Be C(e quindi concettualmente "doppia eredità" da 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');
  }
}));

Se vogliamo impedire il BCdoppio A.prototype.initrichiamo, potremmo aver bisogno di abbandonare lo stile di chiamare direttamente i costruttori ereditati. Avremo bisogno di un certo livello di riferimento indiretto per verificare se si verificano chiamate duplicate e cortocircuitare prima che si verifichino.

Potremmo prendere in considerazione la modifica dei parametri forniti alla funzione proprietà: a fianco protos, Objectcontenente un dato non elaborato che descrive le proprietà ereditate, potremmo anche includere una funzione di utilità per chiamare un metodo di istanza in modo tale che vengano chiamati anche metodi parent, ma vengono rilevate chiamate duplicate e prevenuto. Diamo un'occhiata a dove stabiliamo i parametri per 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 ... */

};

L'intero scopo della modifica di cui sopra makeClassè in modo che abbiamo un argomento aggiuntivo fornito al nostro propertiesFnquando invochiamo makeClass. Dobbiamo anche essere consapevoli del fatto che ogni funzione definita in qualsiasi classe ora può ricevere un parametro dopo tutti gli altri, denominati dup, che è uno Setche contiene tutte le funzioni che sono già state chiamate come risultato della chiamata al metodo ereditato:

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');
  }
}));

Questo nuovo stile riesce effettivamente a garantire che "Construct A"venga registrato solo una volta quando BCviene inizializzata un'istanza di . Ma ci sono tre aspetti negativi, il terzo dei quali è molto critico :

  1. Questo codice è diventato meno leggibile e gestibile. Molta complessità si nasconde dietro la util.invokeNoDuplicatesfunzione e pensare a come questo stile eviti la multi-invocazione non è intuitivo e induce mal di testa. Abbiamo anche quel dupsparametro fastidioso , che deve davvero essere definito su ogni singola funzione della classe . Ahia.
  2. Questo codice è più lento - è necessario un po 'più di indiretta e di calcolo per ottenere risultati desiderabili con eredità multipla. Sfortunatamente, questo è probabilmente il caso di qualsiasi soluzione al nostro problema di invocazione multipla.
  3. Più significativamente, la struttura delle funzioni che si basano sull'eredità è diventata molto rigida . Se una sottoclasse NiftyClasssovrascrive una funzione niftyFunctione la utilizza util.invokeNoDuplicates(this, 'niftyFunction', ...)per eseguirla senza invocazione duplicata, NiftyClass.prototype.niftyFunctionchiamerà la funzione denominata niftyFunctiondi ogni classe genitrice che la definisce, ignora qualsiasi valore restituito da tali classi e infine esegue la logica specializzata di NiftyClass.prototype.niftyFunction. Questa è l' unica struttura possibile . Se NiftyClasseredita CoolClasse GoodClass, ed entrambe queste classi principali forniscono niftyFunctiondefinizioni proprie, NiftyClass.prototype.niftyFunctionnon potranno mai (senza rischiare l'invocazione multipla):
    • A. Esegui prima la logica specializzata NiftyClass, quindi la logica specializzata delle classi genitore
    • B. Esegui la logica specializzata NiftyClassin qualsiasi momento diverso dal completamento di tutta la logica padre specializzata
    • C. Comportarsi condizionatamente in base ai valori di ritorno della logica specializzata dei suoi genitori
    • D. Evitare l'esecuzione di un genitore particolare è specializzata niftyFunctiondel tutto

Ovviamente, potremmo risolvere ogni problema segnalato sopra definendo funzioni specializzate sotto util:

  • A. definireutil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. define util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(Dov'è parentNameil nome del genitore la cui logica specializzata sarà immediatamente seguita dalla logica specializzata delle classi figlio)
  • C. define util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(In questo caso testFnriceverebbe il risultato della logica specializzata per il genitore nominato parentNamee restituirebbe un true/falsevalore che indica se il cortocircuito dovrebbe avvenire)
  • D. define util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(In questo caso blackListsarebbe uno Arraydei nomi parent la cui logica specializzata dovrebbe essere saltata del tutto)

Queste soluzioni sono tutte disponibili, ma questo è un caos totale ! Per ogni struttura unica che può assumere una chiamata di funzione ereditata, avremmo bisogno di un metodo specializzato definito in util. Che disastro assoluto.

Con questo in mente possiamo iniziare a vedere le sfide legate all'implementazione di una buona eredità multipla. La piena implementazione di makeClassI fornita in questa risposta non considera nemmeno il problema di invocazione multipla o molti altri problemi che sorgono riguardo l'ereditarietà multipla.

Questa risposta sta diventando molto lunga. Spero che l' makeClassimplementazione che ho incluso sia ancora utile, anche se non è perfetta. Spero anche che chiunque sia interessato a questo argomento abbia guadagnato più contesto da tenere a mente mentre leggono ulteriormente!


0

Dai un'occhiata al pacchetto IeUnit .

L'assimilazione concettuale implementata in IeUnit sembra offrire ciò che stai cercando in un modo abbastanza dinamico.


0

Ecco un esempio di concatenamento di prototipi utilizzando le funzioni di costruzione :

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

Questo concetto utilizza la definizione di Yehuda Katz di una "classe" per JavaScript:

... una "classe" JavaScript è solo un oggetto Function che funge da costruttore oltre a un oggetto prototipo collegato. ( Fonte: Guru Katz )

A differenza dell'approccio Object.create , quando le classi sono costruite in questo modo e vogliamo creare istanze di una "classe", non abbiamo bisogno di sapere da cosa eredita ogni "classe". Usiamo solo 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

L'ordine di precendenza dovrebbe avere senso. Prima guarda nell'oggetto istanza, poi è il prototipo, quindi il prototipo successivo, ecc.

// 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);

Possiamo anche modificare i prototipi che avranno effetto su tutti gli oggetti costruiti sulla classe.

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

Inizialmente ho scritto un po 'di questo con questa risposta .


2
L'OP richiede catene multiple di prototipi (ad esempio childeredita da parent1e parent2). Il tuo esempio parla solo di una catena.
più elegante

0

Un ritardatario nella scena è SimpleDeclare . Tuttavia, quando si ha a che fare con l'ereditarietà multipla, si finiranno comunque con copie dei costruttori originali. Questa è una necessità in Javascript ...

Merc.


Questa è una necessità in Javascript ... fino ai proxy ES6.
Jonathon,

I proxy sono interessanti! Esaminerò sicuramente la modifica di SimpleDeclare in modo che non sarà necessario copiare i metodi utilizzando i proxy una volta entrati a far parte dello standard. Il codice di SimpleDeclare è davvero, molto facile da leggere e modificare ...
Merc

0

Vorrei usare ds.oop . È simile a prototype.js e altri. rende l'eredità multipla molto semplice ed è minimalista. (solo 2 o 3 kb) Supporta anche alcune altre funzionalità come interfacce e iniezione di dipendenza

/*** 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();

0

Che ne dici, implementa l'ereditarietà multipla 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();

Ed ecco il codice per la funzione di utilità specialize_with ():

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

Questo è il vero codice che viene eseguito. Puoi copiarlo e incollarlo nel file html e provarlo tu stesso. Funziona

Questo è lo sforzo per implementare l'MI in JavaScript. Non molto di codice, più di un know-how.

Non esitate a consultare il mio articolo completo su questo, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS


0

Ho appena usato per assegnare le classi di cui ho bisogno nelle proprietà degli altri e aggiungere un proxy per puntare automaticamente su di esse che mi piacciono:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.