Héritage multiple / prototypes en JavaScript


132

J'en suis arrivé à un point où j'ai besoin d'une sorte d'héritage multiple rudimentaire en JavaScript. (Je ne suis pas ici pour discuter de la question de savoir si c'est une bonne idée ou non, alors veuillez garder ces commentaires pour vous.)

Je veux juste savoir si quelqu'un a tenté cela avec un succès (ou pas), et comment ils l'ont fait.

Pour résumer, ce dont j'ai vraiment besoin, c'est de pouvoir avoir un objet capable d'hériter d'une propriété de plus d'une chaîne de prototypes (c'est-à-dire que chaque prototype pourrait avoir sa propre chaîne), mais dans un ordre de préséance donné (il sera rechercher les chaînes dans l'ordre pour la première définition).

Pour démontrer comment cela est théoriquement possible, cela pourrait être réalisé en attachant la chaîne secondaire à l'extrémité de la chaîne primaire, mais cela affecterait toutes les instances de l'un de ces prototypes précédents et ce n'est pas ce que je veux.

Pensées?


1
Je pense que dojo declare gère l'héritage multiple src aussi j'ai le sentiment que mootools le fait aussi, une grande partie de cela me dépasse mais je vais avoir une lecture rapide de cela comme le suggère le dojo
TI

Jetez un œil à TraitsJS ( lien 1 , lien 2 ), c'est une très bonne alternative à l'héritage multiple et aux mixins ...
CMS

1
@Pointy parce que ce n'est pas très dynamique. J'aimerais être en mesure de prendre en compte les modifications apportées à l'une ou l'autre chaîne parent au fur et à mesure qu'elles se produisent. Cependant, cela dit, je devrai peut-être y recourir si ce n'est tout simplement pas possible.
devios1


1
Une lecture intéressante à ce sujet: webreflection.blogspot.co.uk/2009/06/…
Nobita

Réponses:


49

L'héritage multiple peut être obtenu dans ECMAScript 6 à l'aide d' objets Proxy .

la mise en oeuvre

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

Explication

Un objet proxy se compose d'un objet cible et de quelques interruptions, qui définissent un comportement personnalisé pour les opérations fondamentales.

Lors de la création d'un objet qui hérite d'un autre, nous utilisons Object.create(obj). Mais dans ce cas, nous voulons l'héritage multiple, donc au lieu d' objutiliser un proxy qui redirigera les opérations fondamentales vers l'objet approprié.

J'utilise ces pièges:

  • Le haspiège est un piège pour l' inopérateur . J'utilise somepour vérifier si au moins un prototype contient la propriété.
  • Le getpiège est un piège pour obtenir des valeurs de propriété. J'utilise findpour trouver le premier prototype qui contient cette propriété, et je renvoie la valeur, ou j'appelle le getter sur le récepteur approprié. Ceci est géré par Reflect.get. Si aucun prototype ne contient la propriété, je retourne undefined.
  • Le setpiège est un piège pour définir les valeurs de propriété. J'utilise findpour trouver le premier prototype qui contient cette propriété, et j'appelle son setter sur le récepteur approprié. S'il n'y a pas de setter ou si aucun prototype ne contient la propriété, la valeur est définie sur le récepteur approprié. Ceci est géré par Reflect.set.
  • Le enumeratepiège est un piège pour les for...inboucles . J'itère les propriétés énumérables du premier prototype, puis du second, et ainsi de suite. Une fois qu'une propriété a été itérée, je la stocke dans une table de hachage pour éviter de l'itérer à nouveau.
    Avertissement : cette interruption a été supprimée dans le brouillon ES7 et est obsolète dans les navigateurs.
  • Le ownKeyspiège est un piège pour Object.getOwnPropertyNames(). Depuis ES7, les for...inboucles continuent d'appeler [[GetPrototypeOf]] et d'obtenir les propres propriétés de chacune. Donc, afin de faire itérer les propriétés de tous les prototypes, j'utilise ce piège pour faire apparaître toutes les propriétés héritées énumérables comme des propriétés propres.
  • Le getOwnPropertyDescriptorpiège est un piège pour Object.getOwnPropertyDescriptor(). Faire apparaître toutes les propriétés énumérables comme des propriétés propres dans le ownKeystrap ne suffit pas, les for...inboucles obtiendront le descripteur pour vérifier si elles sont énumérables. J'utilise donc findpour trouver le premier prototype qui contient cette propriété, et j'itère sa chaîne prototypique jusqu'à ce que je trouve le propriétaire, et je renvoie son descripteur. Si aucun prototype ne contient la propriété, je retourne undefined. Le descripteur est modifié pour le rendre configurable, sinon nous pourrions casser certains invariants de proxy.
  • Les interruptions preventExtensionset definePropertyne sont incluses que pour empêcher ces opérations de modifier la cible proxy. Sinon, nous pourrions finir par casser certains invariants de proxy.

Il y a plus de pièges disponibles, que je n'utilise pas

  • Le getPrototypeOfpiège pourrait être ajouté, mais il n'y a pas de moyen approprié de renvoyer les multiples prototypes. Cela implique instanceofne fonctionnera pas non plus. Par conséquent, je le laisse obtenir le prototype de la cible, qui est initialement nul.
  • Le setPrototypeOfpiège pourrait être ajouté et accepter un tableau d'objets, qui remplaceraient les prototypes. Ceci est laissé comme exercice pour le lecteur. Ici, je le laisse simplement modifier le prototype de la cible, ce qui n'est pas très utile car aucun trap n'utilise la cible.
  • Le deletePropertypiège est un piège pour supprimer ses propres propriétés. Le proxy représente l'héritage, donc cela n'aurait pas beaucoup de sens. Je le laisse tenter la suppression sur la cible, qui ne devrait avoir aucune propriété de toute façon.
  • Le isExtensiblepiège est un piège pour obtenir l'extensibilité. Pas très utile, étant donné qu'un invariant l'oblige à retourner la même extensibilité que la cible. Je le laisse donc simplement rediriger l'opération vers la cible, qui sera extensible.
  • Les pièges applyet constructsont des pièges pour appeler ou instancier. Ils ne sont utiles que lorsque la cible est une fonction ou un constructeur.

Exemple

// 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
N'y a-t-il pas des problèmes de performances qui deviendraient pertinents même sur des applications à échelle normale?
Tomáš Zato - Réintégrer Monica le

1
@ TomášZato Ce sera plus lent que les propriétés de données dans un objet normal, mais je ne pense pas que ce sera bien pire que les propriétés d'accesseur.
Oriol

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

4
J'envisagerais de remplacer "Héritage multiple" par "Délégation multiple" pour avoir une meilleure idée de ce qui se passe. Le concept clé de votre implémentation est que le proxy choisit en fait le bon objet pour déléguer (ou transférer) le message. La puissance de votre solution réside dans le fait que vous pouvez étendre dynamiquement le ou les prototypes cibles. D'autres réponses utilisent la concaténation (ala Object.assign) ou l'obtention d'un graphe assez différent, à la fin toutes obtiennent une chaîne de prototypes unique entre les objets. La solution proxy offre un branchement à l'exécution, et c'est génial!
sminutoli

À propos des performances, si vous créez un objet qui hérite de plusieurs objets, qui héritent de plusieurs objets, etc., il deviendra exponentiel. Alors oui, ce sera plus lent. Mais dans des cas normaux, je ne pense pas que ce sera si grave.
Oriol

16

Mise à jour (2019): le message d'origine devient assez obsolète. Cet article (maintenant lien d'archive Internet, puisque le domaine a disparu) et sa bibliothèque GitHub associée sont une bonne approche moderne.

Message original: Héritage multiple [modifier, pas l'héritage approprié du type, mais des propriétés; mixins] en Javascript est assez simple si vous utilisez des prototypes construits plutôt que des prototypes génériques. Voici deux classes parentes dont hériter:

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

Notez que j'ai utilisé le même membre "nom" dans chaque cas, ce qui pourrait être un problème si les parents n'étaient pas d'accord sur la façon dont le "nom" devrait être traité. Mais ils sont compatibles (redondants, vraiment) dans ce cas.

Maintenant, nous avons juste besoin d'une classe qui hérite des deux. L'héritage se fait en appelant la fonction constructeur (sans utiliser le mot clé new) pour les prototypes et les constructeurs d'objets. Premièrement, le prototype doit hériter des prototypes parents

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

Et le constructeur doit hériter des constructeurs parents:

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

Vous pouvez maintenant cultiver, manger et récolter différentes instances:

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

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

Pouvez-vous faire cela avec des prototypes intégrés? (Array, String, Number)
Tomáš Zato - Réintégrer Monica

Je ne pense pas que les prototypes intégrés aient des constructeurs que vous pouvez appeler.
Roy J

Eh bien, je peux le faire, Array.call(...)mais cela ne semble pas affecter ce que je passe pour this.
Tomáš Zato - Réintégrer Monica le

@ TomášZato Vous pourriez faireArray.prototype.constructor.call()
Roy J

1
@AbhishekGupta Merci de me l'avoir fait savoir. J'ai remplacé le lien par un lien vers la page Web archivée.
Roy J

7

Celui-ci sert Object.createà faire une véritable chaîne prototype:

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

Par exemple:

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

retournera:

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

de sorte que obj.a === 1, obj.b === 3etc.


Juste une petite question hypothétique: je voulais créer une classe Vector en mélangeant des prototypes Number et Array (pour le plaisir). Cela me donnerait à la fois des index de tableau et des opérateurs mathématiques. Mais cela fonctionnerait-il?
Tomáš Zato - Réintégrer Monica le

@ TomášZato, cela vaut la peine de consulter cet article si vous recherchez des tableaux de sous-classes; cela pourrait vous éviter des maux de tête. bonne chance!
user3276552

5

J'aime l'implémentation par John Resig d'une structure de classe: http://ejohn.org/blog/simple-javascript-inheritance/

Cela peut être simplement étendu à quelque chose comme:

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

    // same code
}

ce qui vous permettra de passer plusieurs objets dont hériter. Vous allez perdre la instanceOfcapacité ici, mais c'est une évidence si vous voulez l'héritage multiple.


mon exemple plutôt alambiqué de ce qui précède est disponible sur https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js

Notez qu'il y a du code mort dans ce fichier, mais cela permet l'héritage multiple si vous voulez jeter un coup d'œil.


Si vous voulez l'héritage chaîné (PAS l'héritage multiple, mais pour la plupart des gens, c'est la même chose), cela peut être accompli avec Class comme:

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

ce qui préservera la chaîne de prototypes d'origine, mais vous aurez également beaucoup de code inutile en cours d'exécution.


7
Cela crée un clone superficiel fusionné. L'ajout d'une nouvelle propriété aux objets "hérités" ne provoquera pas l'apparition de la nouvelle propriété sur l'objet dérivé, comme ce serait le cas dans un véritable héritage de prototype.
Daniel Earwicker

@DanielEarwicker - C'est vrai, mais si vous voulez "l'héritage multiple" dans cette classe dérive de deux classes, il n'y a pas vraiment d'alternative. Réponse modifiée pour refléter que le simple fait de chaîner des classes ensemble est la même chose dans la plupart des cas.
Mark Kahn

Il semble que votre GitHUb soit parti, avez-vous toujours github.com/cwolves/Fetch/blob/master/support/plugins/klass/ ... Cela ne me dérangerait pas de le regarder si vous souhaitez partager?
JasonDavis

4

Ne vous méprenez pas avec les implémentations de framework JavaScript d'héritage multiple.

Tout ce que vous avez à faire est d'utiliser Object.create () pour créer un nouvel objet à chaque fois avec l'objet prototype et les propriétés spécifiés, puis assurez-vous de modifier Object.prototype.constructor à chaque étape si vous prévoyez d'instancier Bdans le futur.

Pour hériter des propriétés d'occurrence thisAet thisBnous utilisons Function.prototype.call () à la fin de chaque fonction d'objet. Ceci est facultatif si vous ne vous souciez que d'hériter du prototype.

Exécutez le code suivant quelque part et observez 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 hérite du prototype de A
  • C hérite du prototype de B
  • objC est une instance de C

Ceci est une bonne explication des étapes ci-dessus:

POO en JavaScript: ce que vous devez savoir


Cela ne copie-t-il pas toutes les propriétés dans le nouvel objet? Donc, si vous avez deux prototypes, A et B, et que vous les recréez tous les deux sur C, la modification d'une propriété de A n'affectera pas cette propriété sur C et vice versa. Vous vous retrouverez avec une copie de toutes les propriétés en A et B stockées en mémoire. Ce serait la même performance que si vous aviez codé en dur toutes les propriétés de A et B en C.C'est bien pour la lisibilité, et la recherche de propriété n'a pas à se déplacer vers les objets parents, mais ce n'est pas vraiment un héritage - plus comme le clonage. Changer une propriété sur A ne change pas la propriété clonée sur C.
Frank

2

Je ne suis en aucun cas un expert de la POO javascript, mais si je vous comprends bien, vous voulez quelque chose comme (pseudo-code):

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

Cat inherit from (Earth, Animal);

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

Dans ce cas, j'essaierais quelque chose comme:

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
N'est-ce pas simplement choisir le premier prototype et ignorer le reste? Définir c.prototypeplusieurs fois ne donne pas plusieurs prototypes. Par exemple, si vous l'aviez fait Animal.isAlive = true, Cat.isAliveserait toujours indéfini.
devios1

Oui, je voulais mélanger les prototypes, corrigé ... (j'ai utilisé l'extension de jQuery ici, mais vous voyez l'image)
David Hellsing

2

Il est possible d'implémenter l'héritage multiple en JavaScript, bien que très peu de bibliothèques le fassent.

Je pourrais pointer Ring.js , le seul exemple que je connaisse.


2

J'y travaillais beaucoup aujourd'hui et j'essayais d'y parvenir moi-même dans ES6. La façon dont je l'ai fait a été d'utiliser Browserify, Babel et ensuite je l'ai testé avec Wallaby et cela a semblé fonctionner. Mon objectif est d'étendre la baie actuelle, d'inclure ES6, ES7 et d'ajouter des fonctionnalités personnalisées supplémentaires dont j'ai besoin dans le prototype pour traiter les données audio.

Wallaby passe 4 de mes tests. Le fichier example.js peut être collé dans la console et vous pouvez voir que la propriété 'includes' est dans le prototype de la classe. Je veux toujours tester cela plus demain.

Voici ma méthode: (Je vais probablement refactoriser et reconditionner en module après un peu de sommeil!)

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

Je pense que c'est ridiculement simple. Le problème ici est que la classe enfant ne fera référence qu'à instanceofla première classe que vous appelez

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

Vérifiez le code ci-dessous qui EST montrant la prise en charge de l'héritage multiple. Fait en utilisant 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

J'ai tout à fait la fonction de permettre aux classes d'être définies avec l'héritage multiple. Il permet un code comme celui-ci. Dans l'ensemble, vous noterez un départ complet des techniques de classement natives en javascript (par exemple, vous ne verrez jamais le classmot - clé):

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

pour produire une sortie comme celle-ci:

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

Voici à quoi ressemblent les définitions de classe:

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

Nous pouvons voir que chaque définition de classe utilisant la makeClassfonction accepte un Objectdes noms de classe parente mappés à des classes parentes. Il accepte également une fonction qui retourne Objectdes propriétés contenant pour la classe en cours de définition. Cette fonction a un paramètre protos, qui contient suffisamment d'informations pour accéder à toute propriété définie par l'une des classes parentes.

Le dernier élément requis est la makeClassfonction elle-même, qui fait pas mal de travail. Le voici, avec le reste du code. J'ai makeClassbeaucoup commenté :

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 makeClassfonction prend également en charge les propriétés de classe; ceux-ci sont définis en préfixant les noms de propriété avec le $symbole (notez que le nom de propriété final qui en résulte sera $supprimé). Dans cet esprit, nous pourrions écrire une Dragonclasse spécialisée qui modélise le "type" du Dragon, où la liste des types de Dragon disponibles est stockée sur la classe elle-même, par opposition aux instances:

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

Les défis de l'héritage multiple

Quiconque a suivi le code de makeClassprès notera un phénomène indésirable assez important se produisant silencieusement lorsque le code ci-dessus s'exécute: l' instanciation de a RunningFlyingentraînera DEUX appels au Namedconstructeur!

C'est parce que le graphique d'héritage ressemble à ceci:

 (^^ More Specialized ^^)

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

  (vv More Abstract vv)

Lorsqu'il y a plusieurs chemins vers la même classe parente dans le graphe d'héritage d'une sous-classe , les instanciations de la sous-classe invoqueront le constructeur de cette classe parente plusieurs fois.

Combattre cela n'est pas trivial. Regardons quelques exemples avec des noms de classes simplifiés. Nous allons considérer la classe A, la classe parente la plus abstraite, les classes Bet C, qui héritent toutes deux de A, et la classe BCqui hérite de Bet C(et donc conceptuellement "hérite en double" de 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');
  }
}));

Si nous voulons éviter BCde double-invoquer, A.prototype.initnous devrons peut-être abandonner le style d'appel direct des constructeurs hérités. Nous aurons besoin d'un certain niveau d'indirection pour vérifier si des appels en double se produisent, et court-circuiter avant qu'ils ne se produisent.

Nous pourrions envisager de changer les paramètres fournis à la fonction properties: à côté protos, une Objectcontenant des données brutes décrivant les propriétés héritées, nous pourrions également inclure une fonction utilitaire pour appeler une méthode d'instance de telle sorte que les méthodes parentes soient également appelées, mais que les appels en double soient détectés et empêché. Jetons un œil à l'endroit où nous établissons les paramètres pour 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 ... */

};

Le but de la modification ci-dessus makeClassest de faire en sorte que nous ayons un argument supplémentaire fourni à notre propertiesFnlorsque nous invoquons makeClass. Nous devons également être conscients que chaque fonction définie dans une classe peut maintenant recevoir un paramètre après tous ses autres, nommé dup, qui est un Setqui contient toutes les fonctions qui ont déjà été appelées à la suite de l'appel de la méthode héritée:

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

Ce nouveau style réussit en fait à garantir qu'il "Construct A"n'est journalisé qu'une seule fois lorsqu'une instance de BCest initialisée. Mais il y a trois inconvénients, dont le troisième est très critique :

  1. Ce code est devenu moins lisible et maintenable. Une grande complexité se cache derrière la util.invokeNoDuplicatesfonction, et réfléchir à la manière dont ce style évite les invocations multiples n'est pas intuitif et induit des maux de tête. Nous avons également ce dupsparamètre embêtant , qui doit vraiment être défini sur chaque fonction de la classe . Aie.
  2. Ce code est plus lent - un peu plus d'indirection et de calcul sont nécessaires pour obtenir des résultats souhaitables avec l'héritage multiple. Malheureusement, ce sera probablement le cas avec toute solution à notre problème d'invocation multiple.
  3. Plus important encore, la structure des fonctions qui reposent sur l'héritage est devenue très rigide . Si une sous-classe NiftyClassremplace une fonction niftyFunction, et utilise util.invokeNoDuplicates(this, 'niftyFunction', ...)pour l'exécuter sans duplicate-invocation, NiftyClass.prototype.niftyFunctionappellera la fonction nommée niftyFunctionde chaque classe parent qui la définit, ignorera toutes les valeurs de retour de ces classes et exécutera enfin la logique spécialisée de NiftyClass.prototype.niftyFunction. C'est la seule structure possible . Si NiftyClasshérite CoolClasset GoodClass, et que ces deux classes parentes fournissent niftyFunctionleurs propres définitions, NiftyClass.prototype.niftyFunctionil ne sera jamais (sans risquer de multiples invocations) de:
    • A. Exécutez la logique spécialisée du NiftyClasspremier, puis la logique spécialisée des classes-parents
    • B. Exécutez la logique spécialisée de NiftyClassà tout moment autre que lorsque toute la logique parent spécialisée est terminée
    • C. Se comporter de manière conditionnelle en fonction des valeurs de retour de la logique spécialisée de son parent
    • D. Évitez l' exécution d' un parent particulier est spécialisé niftyFunctiontout à fait

Bien sûr, nous pourrions résoudre chaque problème lettré ci-dessus en définissant des fonctions spécialisées sous util:

  • A. définirutil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. define util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(Où parentNameest le nom du parent dont la logique spécialisée sera immédiatement suivie par la logique spécialisée des classes enfants)
  • C. define util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(dans ce cas testFnrecevrait le résultat de la logique spécialisée pour le parent nommé parentName, et renverrait une true/falsevaleur indiquant si le court-circuit devrait se produire)
  • D. définir util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(Dans ce cas , blackListserait un Arraydes noms de parents dont la logique spécialisée devrait être tout à fait sautée)

Ces solutions sont toutes disponibles, mais c'est un chaos total ! Pour chaque structure unique qu'un appel de fonction hérité peut prendre, nous aurions besoin d'une méthode spécialisée définie sous util. Quel désastre absolu.

Dans cet esprit, nous pouvons commencer à voir les défis de la mise en œuvre d'un bon héritage multiple. L'implémentation complète de makeClassj'ai fournie dans cette réponse ne prend même pas en compte le problème d'invocation multiple, ou de nombreux autres problèmes qui se posent concernant l'héritage multiple.

Cette réponse devient très longue. J'espère que l' makeClassimplémentation que j'ai incluse est toujours utile, même si elle n'est pas parfaite. J'espère également que toute personne intéressée par ce sujet aura acquis plus de contexte à garder à l'esprit lors de la lecture plus approfondie!


0

Jetez un œil au package IeUnit .

Le concept d'assimilation implémenté dans IeUnit semble offrir ce que vous recherchez de manière assez dynamique.


0

Voici un exemple de chaînage de prototype utilisant des fonctions de constructeur :

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

Ce concept utilise la définition de Yehuda Katz d'une "classe" pour JavaScript:

... une "classe" JavaScript est juste un objet Function qui sert de constructeur plus un objet prototype attaché. ( Source: Guru Katz )

Contrairement à l' approche Object.create , lorsque les classes sont construites de cette manière et que nous voulons créer des instances d'une «classe», nous n'avons pas besoin de savoir de quoi chaque «classe» hérite. Nous utilisons juste 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'ordre de priorité doit avoir un sens. Tout d'abord, il regarde dans l'objet instance, puis c'est le prototype, puis le prototype suivant, etc.

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

Nous pouvons également modifier les prototypes qui affecteront tous les objets construits sur la classe.

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

J'en ai initialement écrit une partie avec cette réponse .


2
L'OP demande plusieurs chaînes prototypes (par exemple, childhérite de parent1et parent2). Votre exemple ne parle que d'une chaîne.
chic du

0

Un retardataire dans la scène est SimpleDeclare . Cependant, lorsque vous traitez avec l'héritage multiple, vous vous retrouverez toujours avec des copies des constructeurs d'origine. C'est une nécessité en Javascript ...

Merc.


C'est une nécessité en Javascript ... jusqu'à ES6 Proxies.
Jonathon

Les procurations sont intéressantes! Je vais certainement chercher à changer SimpleDeclare afin qu'il n'ait pas besoin de copier des méthodes sur l'utilisation de proxies une fois qu'ils feront partie de la norme. Le code de SimpleDeclare est vraiment, vraiment facile à lire et à modifier ...
Merc

0

J'utiliserais ds.oop . C'est similaire à prototype.js et autres. rend l'héritage multiple très facile et minimaliste. (seulement 2 ou 3 ko) Prend également en charge d'autres fonctionnalités intéressantes telles que les interfaces et l'injection de dépendances

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

Que diriez-vous de cela, il implémente l'héritage multiple en 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();

Et voici le code de la fonction utilitaire specialize_with ():

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

C'est du vrai code qui s'exécute. Vous pouvez le copier-coller dans un fichier html et l'essayer vous-même. Ça marche.

C'est l'effort pour implémenter MI en JavaScript. Pas beaucoup de code, plus de savoir-faire.

N'hésitez pas à consulter mon article complet à ce sujet, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS


0

Je viens d'attribuer les classes dont j'ai besoin dans les propriétés des autres et d'ajouter un proxy pour les pointer automatiquement, j'aime:

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.