Herencia múltiple / prototipos en JavaScript


132

He llegado a un punto en el que necesito tener algún tipo de herencia múltiple rudimentaria en JavaScript. (No estoy aquí para discutir si es una buena idea o no, así que por favor guarde esos comentarios para usted).

Solo quiero saber si alguien ha intentado esto con algún (o no) éxito, y cómo lo hicieron.

Para reducirlo, lo que realmente necesito es poder tener un objeto capaz de heredar una propiedad de más de una cadena de prototipo (es decir, cada prototipo podría tener su propia cadena adecuada), pero en un orden de precedencia dado (lo hará buscar las cadenas para la primera definición).

Para demostrar cómo esto es teóricamente posible, podría lograrse uniendo la cadena secundaria al final de la cadena primaria, pero esto afectaría todas las instancias de cualquiera de esos prototipos anteriores y eso no es lo que quiero.

Pensamientos?


1
Creo que dojo declare maneja src de herencia múltiple también tengo la sensación de que los mootools también lo hacen, gran parte de esto está más allá de mí, pero voy a tener una lectura rápida de esto como lo sugiere el dojo
TI

Eche un vistazo a TraitsJS ( enlace 1 , enlace 2 ) es una muy buena alternativa a la herencia múltiple y mixins ...
CMS

1
@Pointy porque eso no es muy dinámico. Me gustaría poder recoger los cambios realizados en cualquiera de las cadenas principales a medida que ocurren. Sin embargo, dicho esto, es posible que tenga que recurrir a esto si simplemente no es posible.
devios1


1
Una lectura interesante sobre esto: webreflection.blogspot.co.uk/2009/06/…
Nobita

Respuestas:


49

Se puede lograr la herencia múltiple en ECMAScript 6 utilizando objetos Proxy .

Implementación

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

Explicación

Un objeto proxy consta de un objeto de destino y algunas trampas, que definen un comportamiento personalizado para operaciones fundamentales.

Al crear un objeto que hereda de otro, usamos Object.create(obj). Pero en este caso queremos herencia múltiple, así que en lugar deobj usar un proxy que redirigirá las operaciones fundamentales al objeto apropiado.

Yo uso estas trampas:

  • La hastrampa es una trampa para el inoperador . Yo uso somepara verificar si al menos un prototipo contiene la propiedad.
  • La gettrampa es una trampa para obtener valores de propiedad. Utilizo findpara encontrar el primer prototipo que contiene esa propiedad, y devuelvo el valor, o llamo al captador en el receptor apropiado. Esto es manejado por Reflect.get. Si ningún prototipo contiene la propiedad, regreso undefined.
  • La settrampa es una trampa para establecer valores de propiedad. Utilizo findpara encontrar el primer prototipo que contiene esa propiedad, y llamo a su setter en el receptor apropiado. Si no hay setter o ningún prototipo contiene la propiedad, el valor se define en el receptor apropiado. Esto es manejado por Reflect.set.
  • La enumeratetrampa es una trampa para for...inbucles . Repito las propiedades enumerables del primer prototipo, luego del segundo, y así sucesivamente. Una vez que se ha iterado una propiedad, la almaceno en una tabla hash para evitar repetirla nuevamente.
    Advertencia : esta trampa se ha eliminado en el borrador de ES7 y está en desuso en los navegadores.
  • La ownKeystrampa es una trampa para Object.getOwnPropertyNames(). Desde ES7,for...in bucles siguen llamando [[GetPrototypeOf]] y obteniendo las propias propiedades de cada uno. Entonces, para hacer que itere las propiedades de todos los prototipos, uso esta trampa para hacer que todas las propiedades heredables enumerables aparezcan como propiedades propias.
  • La getOwnPropertyDescriptortrampa es una trampa para Object.getOwnPropertyDescriptor(). Hacer que todas las propiedades enumerables aparezcan como propiedades propias en la ownKeystrampa no es suficiente, los for...inbucles obtendrán el descriptor para verificar si son enumerables. Por lo tanto, utilizo findpara encontrar el primer prototipo que contiene esa propiedad, e itero su cadena prototípica hasta que encuentro al dueño de la propiedad, y devuelvo su descriptor. Si ningún prototipo contiene la propiedad, regreso undefined. El descriptor se modifica para hacerlo configurable, de lo contrario podríamos romper algunos invariantes proxy.
  • Las trampas preventExtensionsy definePropertysolo se incluyen para evitar que estas operaciones modifiquen el objetivo proxy. De lo contrario, podríamos terminar rompiendo algunos invariantes proxy.

Hay más trampas disponibles, que no uso

  • Se podría agregar la getPrototypeOftrampa , pero no hay una forma adecuada de devolver los múltiples prototipos. Esto implica instanceofque tampoco funcionará. Por lo tanto, dejo que obtenga el prototipo del objetivo, que inicialmente es nulo.
  • La setPrototypeOftrampa podría agregarse y aceptar una serie de objetos, que reemplazarían a los prototipos. Esto se deja como ejercicio para el lector. Aquí solo dejo que modifique el prototipo del objetivo, lo que no es muy útil porque ninguna trampa usa el objetivo.
  • La deletePropertytrampa es una trampa para eliminar propiedades propias. El proxy representa la herencia, por lo que esto no tendría mucho sentido. Lo dejé intentar la eliminación en el destino, que de todos modos no debería tener ninguna propiedad.
  • La isExtensibletrampa es una trampa para obtener la extensibilidad. No es muy útil, dado que un invariante lo obliga a devolver la misma extensibilidad que el objetivo. Así que simplemente dejé que redirija la operación al objetivo, que será extensible.
  • Las trampas applyy constructson trampas para llamar o crear instancias. Solo son útiles cuando el objetivo es una función o un constructor.

Ejemplo

// 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
¿No hay algunos problemas de rendimiento que serían relevantes incluso en aplicaciones a escala normal?
Tomáš Zato - Restablece a Monica el

1
@ TomášZato Será más lento que las propiedades de datos en un objeto normal, pero no creo que sea mucho peor que las propiedades de acceso.
Oriol

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

44
Consideraría reemplazar "Herencia múltiple" por "Delegación múltiple" para tener una mejor idea de lo que está sucediendo. El concepto clave en su implementación es que el proxy realmente está eligiendo el objeto correcto para delegar (o reenviar) el mensaje. El poder de su solución es que puede extender el / los prototipo / s objetivo dinámicamente. Otras respuestas están usando la concatenación (ala Object.assign) u obteniendo un gráfico bastante diferente, al final todos ellos están obteniendo una cadena de prototipo única entre los objetos. La solución proxy ofrece una ramificación en tiempo de ejecución, ¡y esto es genial!
sminutoli

Sobre el rendimiento, si crea un objeto que hereda de varios objetos, que hereda de varios objetos, etc., se volverá exponencial. Entonces sí, será más lento. Pero en casos normales no creo que sea tan malo.
Oriol

16

Actualización (2019): la publicación original está bastante desactualizada. Este artículo (ahora enlace de archivo de Internet, ya que el dominio desapareció) y su biblioteca GitHub asociada son un buen enfoque moderno.

Publicación original: herencia múltiple [editar, no una herencia adecuada de tipo, sino de propiedades; mixins] en Javascript es bastante sencillo si utiliza prototipos construidos en lugar de los de objetos genéricos. Aquí hay dos clases principales para heredar:

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

Tenga en cuenta que he usado el mismo miembro "nombre" en cada caso, lo que podría ser un problema si los padres no estuvieran de acuerdo sobre cómo se debe manejar "nombre". Pero son compatibles (redundantes, en realidad) en este caso.

Ahora solo necesitamos una clase que herede de ambos. La herencia se realiza llamando a la función constructora (sin usar la nueva palabra clave) para los prototipos y los constructores de objetos. Primero, el prototipo tiene que heredar de los prototipos principales.

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

Y el constructor tiene que heredar de los constructores principales:

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

Ahora puede cultivar, comer y cosechar diferentes instancias:

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

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

¿Puedes hacer esto con prototipos incorporados? (Array, String, Number)
Tomáš Zato - Restablecer Monica

No creo que los prototipos incorporados tengan constructores a los que pueda llamar.
Roy J

Bueno, puedo hacerlo, Array.call(...)pero no parece afectar lo que sea que paso this.
Tomáš Zato - Restablece a Monica el

@ TomášZato Usted podría hacerArray.prototype.constructor.call()
Roy J

1
@AbhishekGupta Gracias por hacérmelo saber. He reemplazado el enlace con un enlace a la página web archivada.
Roy J

7

Este se usa Object.createpara hacer una cadena de prototipo real:

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

Por ejemplo:

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

volverá:

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

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


Solo una pregunta hipotética rápida: quería hacer una clase de Vector mezclando prototipos de Número y Matriz (por diversión). Esto me daría tanto índices de matriz como operadores matemáticos. ¿Pero funcionaría?
Tomáš Zato - Restablece a Monica el

@ TomášZato, vale la pena revisar este artículo si está buscando subclases de matrices; podría ahorrarte un poco de dolor de cabeza. ¡buena suerte!
user3276552

5

Me gusta la implementación de John Resig de una estructura de clases: http://ejohn.org/blog/simple-javascript-inheritance/

Esto puede extenderse simplemente a algo como:

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

    // same code
}

que le permitirá pasar múltiples objetos de los cuales heredar. instanceOfAquí perderá capacidad, pero eso es un hecho si desea herencia múltiple.


mi ejemplo bastante complicado de lo anterior está disponible en https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js

Tenga en cuenta que hay algún código muerto en ese archivo, pero permite la herencia múltiple si desea echar un vistazo.


Si desea herencia encadenada (NO herencia múltiple, pero para la mayoría de las personas es lo mismo), se puede lograr con una clase como:

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

que preservará la cadena del prototipo original, pero también tendrá un montón de código inútil en ejecución.


77
Eso crea un clon superficial fusionado. Agregar una nueva propiedad a los objetos "heredados" no hará que la nueva propiedad aparezca en el objeto derivado, como en una verdadera herencia de prototipo.
Daniel Earwicker

@DanielEarwicker: es cierto, pero si quieres "herencia múltiple" en esa clase deriva de dos clases, no hay realmente una alternativa. Respuesta modificada para reflejar que simplemente encadenar clases juntas es lo mismo en la mayoría de los casos.
Mark Kahn

Parece que tu GitHUb se ha ido, ¿todavía tienes github.com/cwolves/Fetch/blob/master/support/plugins/klass/... No me importaría mirarlo si te gustaría compartirlo?
JasonDavis

4

No se confunda con las implementaciones de framework JavaScript de herencia múltiple.

Todo lo que necesita hacer es usar Object.create () para crear un nuevo objeto cada vez con el objeto prototipo y las propiedades especificadas, luego asegúrese de cambiar Object.prototype.constructor en cada paso del camino si planea crear instancias Ben el futuro.

Para heredar las propiedades de instancia thisAy thisBusamos Function.prototype.call () al final de cada función de objeto. Esto es opcional si solo te importa heredar el prototipo.

Ejecute el siguiente código en alguna parte y observe 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 hereda el prototipo de A
  • C hereda el prototipo de B
  • objC es una instancia de C

Esta es una buena explicación de los pasos anteriores:

OOP en JavaScript: lo que necesita saber


Sin embargo, ¿esto no copia todas las propiedades en el nuevo objeto? Entonces, si tiene dos prototipos, A y B, y los recrea en C, cambiar una propiedad de A no afectará esa propiedad en C y viceversa. Terminará con una copia de todas las propiedades en A y B almacenadas en la memoria. Sería el mismo rendimiento que si hubiera codificado todas las propiedades de A y B en C. Es agradable para la legibilidad, y la búsqueda de propiedades no tiene que viajar a objetos primarios, pero en realidad no es herencia, más bien como clonación. Cambiar una propiedad en A no cambia la propiedad clonada en C.
Frank

2

De ninguna manera soy un experto en JavaScript OOP, pero si te entiendo correctamente, quieres algo como (pseudocódigo):

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

Cat inherit from (Earth, Animal);

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

En ese caso, probaría algo como:

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
¿No es solo elegir el primer prototipo e ignorar el resto? Establecer c.prototypevarias veces no produce múltiples prototipos. Por ejemplo, si lo hubiera hecho Animal.isAlive = true, Cat.isAliveaún estaría indefinido.
devios1

Sí, tenía la intención de mezclar los prototipos, corregido ... (Utilicé la extensión de jQuery aquí, pero entiendes)
David Hellsing

2

Es posible implementar herencia múltiple en JavaScript, aunque muy pocas bibliotecas lo hacen.

Podría señalar Ring.js , el único ejemplo que conozco.


2

Trabajé mucho en esto hoy e intenté lograrlo yo mismo en ES6. La forma en que lo hice fue usando Browserify, Babel y luego lo probé con Wallaby y pareció funcionar. Mi objetivo es extender la matriz actual, incluir ES6, ES7 y agregar algunas características personalizadas adicionales que necesito en el prototipo para manejar datos de audio.

Wallaby pasa 4 de mis pruebas. El archivo example.js se puede pegar en la consola y puede ver que la propiedad 'incluye' está en el prototipo de la clase. Todavía quiero probar esto más mañana.

Este es mi método: (¡lo más probable es que refactorice y reempaquete como módulo después de dormir un poco!)

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

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


2

Creo que es ridículamente simple. El problema aquí es que la clase secundaria solo se referirá a instanceofla primera clase a la que llame

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

Verifique el siguiente código que muestra compatibilidad con herencia múltiple. Hecho mediante el uso de la herencia proteotípica

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

Tengo bastante la función de permitir que las clases se definan con herencia múltiple. Permite código como el siguiente. En general, notará una desviación completa de las técnicas de clasificación nativas en javascript (por ejemplo, nunca verá la classpalabra clave):

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

para producir resultados como este:

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

A continuación se muestran las definiciones de clase:

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

Podemos ver que cada definición de clase que usa la makeClassfunción acepta uno Objectde los nombres de clase padre mapeados a clases padre. También acepta una función que devuelve Objectpropiedades que contienen para la clase que se está definiendo. Esta función tiene un parámetroprotos , que contiene suficiente información para acceder a cualquier propiedad definida por cualquiera de las clases principales.

La pieza final requerida es la makeClassfunción en sí, que hace bastante trabajo. Aquí está, junto con el resto del código. He comentado makeClassbastante:

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 makeClassfunción también admite propiedades de clase; estos se definen prefijando los nombres de las propiedades con el $símbolo (tenga en cuenta que el nombre de la propiedad final que resulte tendrá el $eliminado). Con esto en mente, podríamos escribir una Dragonclase especializada que modele el "tipo" del Dragón, donde la lista de tipos de Dragón disponibles se almacena en la Clase en lugar de en las instancias:

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

Los desafíos de la herencia múltiple

Cualquiera que haya seguido el código de makeClasscerca notará un fenómeno indeseable bastante significativo que ocurre silenciosamente cuando se ejecuta el código anterior: ¡ crear una instancia RunningFlyingdará como resultado DOS llamadas al Namedconstructor!

Esto se debe a que el gráfico de herencia se ve así:

 (^^ More Specialized ^^)

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

  (vv More Abstract vv)

Cuando hay múltiples rutas a la misma clase padre en un gráfico de herencia de la subclase, las instancias de la subclase invocarán ese constructor de la clase padre varias veces.

Combatir esto no es trivial. Veamos algunos ejemplos con nombres de clase simplificados. Consideraremos la clase A, la clase padre más abstracta, las clases By C, que ambas heredan de A, y la clase BCque hereda de By C(y por lo tanto conceptualmente "doble hereda" 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 queremos evitar la BCdoble invocación, A.prototype.inites posible que debamos abandonar el estilo de llamar directamente a los constructores heredados. Necesitaremos cierto nivel de indirección para verificar si se producen llamadas duplicadas y cortocircuitos antes de que sucedan.

Podríamos considerar cambiar los parámetros suministrados a la función de propiedades: junto con protosuna Objectinformación sin procesar que describe las propiedades heredadas, también podríamos incluir una función de utilidad para llamar a un método de instancia de tal manera que también se invoquen métodos principales, pero se detectan llamadas duplicadas y prevenido. Echemos un vistazo a dónde establecemos los parámetros para 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 ... */

};

Todo el propósito del cambio anterior makeClasses para que tengamos un argumento adicional suministrado propertiesFncuando invoquemos makeClass. También debemos tener en cuenta que cada función definida en cualquier clase ahora puede recibir un parámetro después de todos los demás, llamado dup, que es el Setque contiene todas las funciones que ya se han llamado como resultado de llamar al método heredado:

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

Este nuevo estilo realmente garantiza "Construct A"que solo se registre una vez cuando BCse inicializa una instancia de . Pero hay tres desventajas, la tercera de las cuales es muy crítica :

  1. Este código se ha vuelto menos legible y mantenible. Una gran complejidad se esconde detrás de la util.invokeNoDuplicatesfunción, y pensar en cómo este estilo evita la invocación múltiple no es intuitivo e induce dolor de cabeza. También tenemos ese dupsparámetro molesto , que realmente necesita ser definido en cada función de la clase . Ay.
  2. Este código es más lento: se requiere un poco más de indirección y cómputo para lograr resultados deseables con herencia múltiple. Desafortunadamente, es probable que este sea el caso con cualquier solución a nuestro problema de invocación múltiple.
  3. Más significativamente, la estructura de funciones que dependen de la herencia se ha vuelto muy rígida . Si una subclase NiftyClassanula una función niftyFunctiony la usa util.invokeNoDuplicates(this, 'niftyFunction', ...)para ejecutarla sin invocación duplicada, NiftyClass.prototype.niftyFunctionllamará a la función nombrada niftyFunctionde cada clase padre que la defina, ignorará cualquier valor de retorno de esas clases y finalmente realizará la lógica especializada de NiftyClass.prototype.niftyFunction. Esta es la única estructura posible . Si NiftyClasshereda CoolClassy GoodClass, y ambas clases principales proporcionan niftyFunctiondefiniciones propias, NiftyClass.prototype.niftyFunctionnunca (sin arriesgarse a invocación múltiple) podrá:
    • A. Ejecute la lógica especializada de NiftyClassprimero, luego la lógica especializada de las clases padre
    • B. Ejecute la lógica especializada NiftyClassen cualquier punto que no sea después de que se haya completado toda la lógica principal especializada
    • C. Comportarse condicionalmente dependiendo de los valores de retorno de la lógica especializada de su padre
    • D. Evite dirigir por niftyFunctioncompleto a un padre en particular especializado

Por supuesto, podríamos resolver cada problema con letras arriba definiendo funciones especializadas en util:

  • A. definirutil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. define util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(dónde parentNameestá el nombre del padre cuya lógica especializada será seguida inmediatamente por la lógica especializada de las clases secundarias)
  • C. definir util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(en este caso testFnrecibiría el resultado de la lógica especializada para el padre nombrado parentNamey devolvería un true/falsevalor que indica si el cortocircuito debería ocurrir)
  • D. definir util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(en este caso blackListsería uno Arrayde los nombres principales cuya lógica especializada debería omitirse por completo)

Estas soluciones están disponibles, ¡ pero esto es un caos total ! Para cada estructura única que puede tomar una llamada de función heredada, necesitaríamos un método especializado definido en util. Qué desastre absoluto.

Con esto en mente, podemos comenzar a ver los desafíos de implementar una buena herencia múltiple. La implementación completa de lo makeClassque proporcioné en esta respuesta ni siquiera considera el problema de la invocación múltiple o muchos otros problemas que surgen con respecto a la herencia múltiple.

Esta respuesta se está haciendo muy larga. Espero que la makeClassimplementación que incluí siga siendo útil, incluso si no es perfecta. ¡También espero que cualquier persona interesada en este tema haya adquirido más contexto para tener en cuenta mientras leen más!


0

Echa un vistazo al paquete IeUnit .

El concepto de asimilación implementado en IeUnit parece ofrecer lo que está buscando de una manera bastante dinámica.


0

Aquí hay un ejemplo de encadenamiento de prototipos usando funciones de constructor :

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

Este concepto utiliza la definición de Yehuda Katz de una "clase" para JavaScript:

... una "clase" de JavaScript es solo un objeto Function que sirve como un constructor más un objeto prototipo adjunto. ( Fuente: Guru Katz )

A diferencia del enfoque Object.create , cuando las clases se crean de esta manera y queremos crear instancias de una "clase", no necesitamos saber de qué está heredando cada "clase". Nosotros solo usamos 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

El orden de precedencia debería tener sentido. Primero se ve en el objeto de instancia, luego es el prototipo, luego el siguiente prototipo, 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);

También podemos modificar los prototipos que afectarán a todos los objetos creados en la clase.

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

Originalmente escribí algo de esto con esta respuesta .


2
El OP solicita múltiples cadenas de prototipos (por ejemplo, childhereda de parent1y parent2). Su ejemplo solo habla de una cadena.
poshest

0

Un recién llegado en la escena es SimpleDeclare . Sin embargo, cuando se trata de herencia múltiple, aún terminará con copias de los constructores originales. Eso es una necesidad en Javascript ...

Merc.


Eso es una necesidad en Javascript ... hasta ES6 Proxies.
Jonathon

Los poderes son interesantes! Definitivamente buscaré cambiar SimpleDeclare para que no sea necesario copiar métodos sobre el uso de proxies una vez que se conviertan en parte del estándar. El código de SimpleDeclare es realmente fácil de leer y cambiar ...
Merc

0

Yo usaría ds.oop . Es similar a prototype.js y otros. hace que la herencia múltiple sea muy fácil y minimalista. (solo 2 o 3 kb) También admite algunas otras características interesantes como interfaces e inyección de dependencias

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

¿Qué tal esto? Implementa herencia múltiple 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();

Y aquí está el código para la función de utilidad specialize_with ():

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

Este es el código real que se ejecuta. Puede copiarlo y pegarlo en un archivo html y probarlo usted mismo. Funciona

Ese es el esfuerzo para implementar MI en JavaScript. No hay mucho código, más un saber hacer.

Por favor, siéntase libre de mirar mi artículo completo sobre esto, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS


0

Solo solía asignar qué clases necesito en las propiedades de otros, y agrego un proxy para señalarlas automáticamente, me gusta:

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.