Herança / protótipos múltiplos em JavaScript

132

Cheguei a um ponto em que preciso ter algum tipo de herança múltipla rudimentar acontecendo em JavaScript. (Não estou aqui para discutir se é uma boa ideia ou não, por favor, guarde esses comentários para si mesmo.)

Eu só quero saber se alguém tentou isso com algum (ou não) sucesso, e como eles fizeram isso.

Para resumir, o que realmente preciso é ser capaz de ter um objeto capaz de herdar uma propriedade de mais de uma cadeia de protótipos (ou seja, cada protótipo pode ter sua própria cadeia), mas em uma ordem de precedência (será procure as cadeias para obter a primeira definição).

Para demonstrar como isso é teoricamente possível, isso poderia ser alcançado anexando a cadeia secundária ao final da cadeia primária, mas isso afetaria todas as instâncias de qualquer um desses protótipos anteriores e não é isso que eu quero.

Pensamentos?

devios1
fonte
1
Acho dojo de declaração de alças herança múltipla src também tenho a sensação de mootools se também, grande parte desta está além de mim, mas eu vou ter uma rápida leitura do presente como dojo sugere
TI
Dê uma olhada para TraitsJS ( ligação 1 , ligação 2 ) é realmente uma boa alternativa para herança múltipla e mixins ...
CMS
1
@ Pointy, porque isso não é muito dinâmico. Eu gostaria de poder captar as alterações feitas na cadeia pai quando elas ocorrerem. No entanto, dito isso, talvez eu precise recorrer a isso, se não for possível.
Devios1:
possível duplicata faz javascript herança suporte a múltiplos como C ++
Daniel Earwicker
1
Uma leitura interessante sobre isso: webreflection.blogspot.co.uk/2009/06/…
Nobita

Respostas:

49

Herança múltipla pode ser alcançada no ECMAScript 6 usando objetos Proxy .

Implementação

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

Explicação

Um objeto proxy consiste em um objeto de destino e em alguns traps, que definem o comportamento customizado para operações fundamentais.

Ao criar um objeto que herda de outro, usamos Object.create(obj). Mas, neste caso, queremos herança múltipla; portanto, em vez de objusar um proxy que redirecionará operações fundamentais para o objeto apropriado.

Eu uso essas armadilhas:

  • A hasarmadilha é uma armadilha para o inoperador . Uso somepara verificar se pelo menos um protótipo contém a propriedade
  • A getarmadilha é uma armadilha para obter valores de propriedade. Eu uso findpara encontrar o primeiro protótipo que contém essa propriedade e retorno o valor ou chamo o getter no destinatário apropriado. Isso é tratado por Reflect.get. Se nenhum protótipo contiver a propriedade, eu retornarei undefined.
  • A setarmadilha é uma armadilha para definir valores de propriedade. Utilizo findpara encontrar o primeiro protótipo que contém essa propriedade e chamo seu setter no receptor apropriado. Se não houver nenhum setter ou nenhum protótipo contiver a propriedade, o valor será definido no receptor apropriado. Isso é tratado por Reflect.set.
  • A enumeratearmadilha é uma armadilha para for...inloops . Eu itero as propriedades enumeráveis ​​do primeiro protótipo, depois do segundo e assim por diante. Depois que uma propriedade é iterada, eu a armazeno em uma tabela de hash para evitar iterá-la novamente.
    Aviso : essa interceptação foi removida no rascunho do ES7 e está obsoleta nos navegadores.
  • A ownKeysarmadilha é uma armadilha para Object.getOwnPropertyNames(). Desde o ES7, os for...inloops continuam chamando [[GetPrototypeOf]] e obtendo as próprias propriedades de cada um. Portanto, para fazer iterar as propriedades de todos os protótipos, eu uso essa interceptação para fazer com que todas as propriedades herdadas enumeráveis ​​apareçam como propriedades próprias.
  • A getOwnPropertyDescriptorarmadilha é uma armadilha para Object.getOwnPropertyDescriptor(). Fazer com que todas as propriedades enumeráveis ​​apareçam como propriedades próprias na ownKeysarmadilha não é suficiente, os for...inloops receberão o descritor para verificar se são enumeráveis. Então, eu uso findpara encontrar o primeiro protótipo que contém essa propriedade e iteramos sua cadeia prototípica até encontrar o proprietário da propriedade e retornar seu descritor. Se nenhum protótipo contiver a propriedade, eu retornarei undefined. O descritor é modificado para torná-lo configurável, caso contrário, poderíamos quebrar alguns invariantes de proxy.
  • Os traps preventExtensionse definePropertysão incluídos apenas para impedir que essas operações modifiquem o destino do proxy. Caso contrário, poderíamos acabar quebrando alguns invariantes de proxy.

Existem mais armadilhas disponíveis, que eu não uso

  • A getPrototypeOfarmadilha pode ser adicionada, mas não há maneira adequada de retornar os vários protótipos. Isso implica instanceofque não funcionará também. Portanto, deixo que ele obtenha o protótipo do alvo, que inicialmente é nulo.
  • A setPrototypeOfarmadilha pode ser adicionada e aceitar uma matriz de objetos, que substituiria os protótipos. Isso é deixado como um exercício para o leitor. Aqui, deixei que ele modificasse o protótipo do alvo, o que não é muito útil porque nenhuma armadilha usa o alvo.
  • A deletePropertyarmadilha é uma armadilha para excluir propriedades próprias. O proxy representa a herança, portanto, isso não faria muito sentido. Eu deixei tentar a exclusão no destino, que não deveria ter nenhuma propriedade.
  • A isExtensiblearmadilha é uma armadilha para obter a extensibilidade. Não é muito útil, uma vez que uma invariante o força a retornar a mesma extensibilidade que o alvo. Então, eu apenas deixei redirecionar a operação para o destino, que será extensível.
  • As armadilhas applye constructsão armadilhas para chamar ou instanciar. Eles são úteis apenas quando o destino é uma função ou um construtor.

Exemplo

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

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

// Setting properties
obj.c = 3;

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

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

// Property enumeration
for(var p in obj) p; // "c", "b", "a"
Oriol
fonte
1
Não existem problemas de desempenho que se tornariam relevantes mesmo em aplicativos de escala normal?
Tomáš Zato - Restabelecer Monica
1
@ TomášZato Será mais lento que as propriedades dos dados em um objeto normal, mas não acho que seja muito pior do que as propriedades do acessador.
Oriol
TIL:multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
bloodyKnuckles
4
Eu consideraria substituir "Herança múltipla" por "Delegação múltipla" para ter uma idéia melhor do que está acontecendo. O conceito chave em sua implementação é que o proxy está realmente escolhendo o objeto certo para delegar (ou encaminhar) a mensagem. O poder da sua solução é que você pode estender os protótipos de destino dinamicamente. Outras respostas estão usando concatenação (ala Object.assign) ou obtendo um gráfico bem diferente; no final, todas elas estão recebendo uma cadeia de protótipos únicos entre objetos. A solução de proxy oferece uma ramificação em tempo de execução, e isso é demais!
Sminutoli # 28/17
Sobre o desempenho, se você criar um objeto que herda de vários objetos, que herda de vários objetos e assim por diante, ele se tornará exponencial. Então, sim, será mais lento. Mas, em casos normais, não acho que seja tão ruim assim.
226 Oriol
16

Atualização (2019): A postagem original está ficando bastante desatualizada. Este artigo (agora link do arquivo da Internet, desde que o domínio foi embora) e sua biblioteca GitHub associada são uma boa abordagem moderna.

Post original: herança múltipla [editar, herança não apropriada do tipo, mas de propriedades; mixins] em Javascript é bastante simples se você usar protótipos construídos em vez de genéricos. Aqui estão duas classes pai para herdar:

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

Observe que eu usei o mesmo membro "nome" em cada caso, o que pode ser um problema se os pais não concordarem sobre como o "nome" deve ser tratado. Mas eles são compatíveis (redundantes, realmente) neste caso.

Agora só precisamos de uma classe que herda de ambos. A herança é feita chamando a função construtora (sem usar a nova palavra-chave) para os protótipos e construtores de objetos. Primeiro, o protótipo deve herdar dos protótipos pai

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

E o construtor deve herdar dos construtores pai:

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

Agora você pode crescer, comer e colher diferentes instâncias:

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

fp1.grow();
fp2.grow();
fp1.harvest();
fp1.eat();
fp2.harvest();
fp2.eat();
Roy J
fonte
Você pode fazer isso com protótipos embutidos? (Matriz, String, Número)
Tomáš Zato - Reinstate Monica
Eu não acho que os protótipos embutidos tenham construtores que você pode chamar.
Roy J
Bem, eu posso fazer, Array.call(...)mas isso não parece afetar o que eu passo this.
Tomáš Zato - Restabelece Monica
@ TomášZato Você poderia fazerArray.prototype.constructor.call()
Roy J
1
@AbhishekGupta Obrigado por me informar. Substituí o link por um link para a página da Web arquivada.
21419 Roy J
7

Este usa Object.createpara criar uma cadeia de protótipos 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 exemplo:

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

retornará:

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

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

pimvdb
fonte
Apenas uma pergunta hipotética rápida: eu queria fazer a classe Vector misturando os protótipos Number e Array (por diversão). Isso me daria índices de matriz e operadores matemáticos. Mas isso funcionaria?
Tomáš Zato - Restabelecer Monica
@ TomášZato, vale a pena conferir este artigo se você estiver olhando para matrizes de subclasse; isso poderia te poupar alguma dor de cabeça. boa sorte!
user3276552
5

Gosto da implementação de John Resig de uma estrutura de classes: http://ejohn.org/blog/simple-javascript-inheritance/

Isso pode ser simplesmente estendido para 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
}

o que permitirá que você passe vários objetos dos quais herdar. Você perderá a instanceOfcapacidade aqui, mas isso é um fato, se você quiser herança múltipla.


meu exemplo bastante complicado está disponível em https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js

Observe que há algum código morto nesse arquivo, mas permite herança múltipla se você quiser dar uma olhada.


Se você deseja herança encadeada (NÃO herança múltipla, mas para a maioria das pessoas é a mesma coisa), isso pode ser conseguido com Classe como:

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

que preservará a cadeia de protótipos original, mas você também terá muito código inútil em execução.

Mark Kahn
fonte
7
Isso cria um clone superficial mesclado. Adicionar uma nova propriedade aos objetos "herdados" não fará com que a nova propriedade apareça no objeto derivado, como faria na verdadeira herança de protótipo.
21118 Daniel Earwicker
@DanielEarwicker - É verdade, mas se você deseja "herança múltipla" nessa classe, deriva de duas classes, não há realmente uma alternativa. Resposta modificada para refletir que simplesmente encadear classes é a mesma coisa na maioria dos casos.
Mark Kahn
Parece que seu GitHUb se foi. Você ainda tem o github.com/cwolves/Fetch/blob/master/support/plugins/klass/…? Eu não me importaria de olhar para ele se você quiser compartilhar?
precisa saber é o seguinte
4

Não se confunda com implementações de estrutura JavaScript de herança múltipla.

Tudo o que você precisa fazer é usar Object.create () para criar um novo objeto toda vez com o objeto e propriedades de protótipo especificado e, em seguida, certifique-se de alterar o Object.prototype.constructor a cada passo do caminho, se você planeja instanciarB o futuro.

Para herdar propriedades da instância thisAe thisBusamos Function.prototype.call () no final de cada função de objeto. Isso é opcional se você só se preocupa em herdar o protótipo.

Execute o seguinte código em algum lugar e 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 herda o protótipo de A
  • C herda o protótipo de B
  • objC é uma instância de C

Esta é uma boa explicação das etapas acima:

OOP em JavaScript: o que você precisa saber

Dave
fonte
Isso não copia todas as propriedades para o novo objeto? Portanto, se você tiver dois protótipos, A e B, e os recriar em C, alterar uma propriedade de A não afetará essa propriedade em C e vice-versa. Você terminará com uma cópia de todas as propriedades em A e B armazenadas na memória. Seria o mesmo desempenho como se você tivesse codificado todas as propriedades de A e B em C. É bom para legibilidade, e a pesquisa de propriedades não precisa ser direcionada aos objetos pai, mas não é realmente uma herança - mais como a clonagem. Alterando uma propriedade em um não muda a propriedade clonado em C.
Frank
2

Eu não sou de forma alguma um especialista em javascript OOP, mas se eu entendi corretamente, você deseja algo como (pseudo-código):

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

Cat inherit from (Earth, Animal);

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

Nesse caso, eu tentaria 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]());
    }
}
David Hellsing
fonte
1
Isso não é apenas escolher o primeiro protótipo e ignorar o resto? Definir c.prototypevárias vezes não gera vários protótipos. Por exemplo, se você tivesse Animal.isAlive = true, Cat.isAliveainda seria indefinido.
Devios1:
Sim, eu estava querendo misturar os protótipos, corrigido ... (eu usei jQuery de estender aqui, mas você começa a foto)
David Hellsing
2

É possível implementar herança múltipla em JavaScript, embora poucas bibliotecas o façam.

Eu poderia apontar Ring.js , o único exemplo que conheço.

nicolas-van
fonte
2

Hoje eu estava trabalhando muito nisso e tentando conseguir isso sozinho no ES6. O jeito que eu fiz isso foi usando o Browserify, Babel e depois testei com o Wallaby e ele pareceu funcionar. Meu objetivo é estender a matriz atual, incluir o ES6, ES7 e adicionar alguns recursos personalizados adicionais necessários no protótipo para lidar com dados de áudio.

O Wallaby passa em 4 dos meus testes. O arquivo example.js pode ser colado no console e você pode ver que a propriedade 'includes' está no protótipo da classe. Ainda quero testar isso amanhã.

Aqui está o meu método: (provavelmente refatorarei e reembalarei como um módulo depois de dormir!)

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

Repositório do Github: https://github.com/danieldram/array-includes-polyfill

Daniel Ram
fonte
2

Eu acho isso ridiculamente simples. O problema aqui é que a classe filho se refere apenas à instanceofprimeira classe que você chama

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

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

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

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

  return this;
}

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

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

var cool = new Cool();

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

Verifique o código abaixo do qual IS está mostrando suporte para herança múltipla. Feito usando a herança prototí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());
Shivang Gupta
fonte
1

Eu tenho bastante a função de permitir que classes sejam definidas com herança múltipla. Ele permite códigos como os seguintes. No geral, você observará uma saída completa das técnicas nativas de classificação em javascript (por exemplo, você nunca verá a classpalavra - chave):

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 produzir uma saída como esta:

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

Aqui estão as definições das classes:

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 definição de classe usando a makeClassfunção aceita um Objectnome de classe pai mapeado para classe pai. Ele também aceita uma função que retorna Objectpropriedades contendo a classe que está sendo definida. Esta função possui um parâmetroprotos , que contém informações suficientes para acessar qualquer propriedade definida por qualquer uma das classes-pai.

A peça final necessária é a makeClassprópria função, que faz bastante trabalho. Aqui está, junto com o restante do código. Eu comentei 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();

A makeClassfunção também suporta propriedades de classe; elas são definidas prefixando os nomes das propriedades com o $símbolo (observe que o nome da propriedade final resultante será $removido). Com isso em mente, poderíamos escrever uma Dragonclasse especializada que modela o "tipo" do dragão, onde a lista de tipos de dragão disponíveis é armazenada na própria classe, e não nas instâncias:

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

Os desafios da herança múltipla

Quem seguiu o código de makeClassperto notará um fenômeno indesejável bastante significativo ocorrendo silenciosamente quando o código acima for executado: instanciar a RunningFlyingresultará em DUAS chamadas ao Namedconstrutor!

Isso ocorre porque o gráfico de herança se parece com isso:

 (^^ More Specialized ^^)

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

  (vv More Abstract vv)

Quando existem vários caminhos para a mesma classe pai no gráfico de herança de uma subclasse da subclasse, as instanciações da subclasse invocam o construtor dessa classe-pai várias vezes.

Combater isso não é trivial. Vejamos alguns exemplos com nomes de classe simplificados. Consideraremos classe A, a classe pai mais abstrata, classes Be C, que herdam de A, e classe BCque herda de Be C(e, portanto, conceitualmente "herda duas vezes" 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');
  }
}));

Se quisermos impedir BCa chamada dupla A.prototype.init, talvez seja necessário abandonar o estilo de chamar diretamente os construtores herdados. Precisamos de algum nível de indireção para verificar se chamadas duplicadas estão ocorrendo e entrar em curto-circuito antes que elas aconteçam.

Poderíamos considerar mudar os parâmetros fornecidos para as propriedades funcionar: ao lado protos, uma Objectcontendo dados brutos que descrevem propriedades herdadas, que poderia também incluir uma função de utilidade para chamar um método de exemplo, de tal forma que os métodos mãe também são chamados, mas chamadas duplicados são detectados e impedido. Vamos dar uma olhada em onde estabelecemos os 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 o objetivo da alteração acima makeClassé para que tenhamos um argumento adicional fornecido propertiesFnquando invocamos makeClass. Também devemos estar cientes de que todas as funções definidas em qualquer classe agora podem receber um parâmetro depois de todas as outras, nomeadas dup, Setque contém todas as funções que já foram chamadas como resultado da chamada do método herdado:

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

Esse novo estilo realmente consegue garantir que "Construct A"seja registrado apenas uma vez quando uma instância de BCfor inicializada. Mas há três desvantagens, a terceira das quais é muito crítica :

  1. Este código se tornou menos legível e de manutenção. Muita complexidade se esconde por trás da util.invokeNoDuplicatesfunção, e pensar em como esse estilo evita a invocação múltipla não é intuitivo e causa dor de cabeça. Também temos esse dupsparâmetro irritante , que realmente precisa ser definido em todas as funções da classe . Ai.
  2. Esse código é mais lento - é necessário um pouco mais de indireção e computação para alcançar resultados desejáveis ​​com herança múltipla. Infelizmente, é provável que isso aconteça com qualquer solução para nosso problema de invocação múltipla.
  3. Mais significativamente, a estrutura de funções que dependem de herança se tornou muito rígida . Se uma subclasse NiftyClasssubstituir uma função niftyFunctione utilizá util.invokeNoDuplicates(this, 'niftyFunction', ...)-la para executá-la sem invocação duplicada, NiftyClass.prototype.niftyFunctionchamará a função nomeada niftyFunctionde cada classe pai que a define, ignorará quaisquer valores de retorno dessas classes e, finalmente, executará a lógica especializada de NiftyClass.prototype.niftyFunction. Essa é a única estrutura possível . Se NiftyClassherda CoolClasse GoodClass, e ambas as classes-pai fornecem niftyFunctiondefinições próprias, NiftyClass.prototype.niftyFunctionnunca (sem arriscar invocação múltipla) será capaz de:
    • A. Execute NiftyClassprimeiro a lógica especializada , depois a lógica especializada das classes pai
    • B. Execute a lógica especializada NiftyClassem qualquer ponto que não seja após a conclusão de toda lógica lógica especializada
    • C. Comporte-se condicionalmente, dependendo dos valores de retorno da lógica especializada de seus pais
    • D. Evitar a execução de um pai especial é especializada niftyFunctiontotalmente

Obviamente, poderíamos resolver cada problema com letras acima, definindo funções especializadas em util:

  • A. definirutil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. define util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(Onde parentNameé o nome do pai cuja lógica especializada será imediatamente seguida pela lógica especializada das classes filho)
  • C. define util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(nesse caso testFn, receberia o resultado da lógica especializada para o pai nomeado parentNamee retornaria um true/falsevalor indicando se o curto-circuito deveria ocorrer)
  • D. define util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(Nesse caso, blackListseria um Arraydos nomes dos pais cuja lógica especializada deve ser ignorada por completo)

Essas soluções estão todas disponíveis, mas isso é total caos ! Para toda estrutura exclusiva que uma chamada de função herdada pode suportar, precisaríamos de um método especializado definido em util. Que desastre absoluto.

Com isso em mente, podemos começar a ver os desafios da implementação de uma boa herança múltipla. A implementação completa de makeClassI fornecida nesta resposta nem sequer considera o problema de invocação múltipla ou muitos outros problemas que surgem com relação à herança múltipla.

Esta resposta está ficando muito longa. Espero que a makeClassimplementação incluída ainda seja útil, mesmo que não seja perfeita. Eu também espero que qualquer pessoa interessada neste tópico tenha ganhado mais contexto para ter em mente enquanto lê mais!

Gershom
fonte
0

Dê uma olhada no pacote IeUnit .

A assimilação de conceito implementada no IeUnit parece oferecer o que você está procurando de uma maneira bastante dinâmica.

James
fonte
0

Aqui está um exemplo de encadeamento de protótipo usando funções de construtor :

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 conceito usa a definição de Yehuda Katz de "classe" para JavaScript:

... uma "classe" JavaScript é apenas um objeto Function que serve como construtor e um objeto protótipo anexado. ( Fonte: Guru Katz )

Diferentemente da abordagem Object.create , quando as classes são criadas dessa maneira e queremos criar instâncias de uma "classe", não precisamos saber do que cada "classe" está herdando. Nós apenas 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

A ordem de precedência deve fazer sentido. Primeiro, ele olha no objeto de instância, depois é protótipo, depois no próximo protótipo, 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);

Também podemos modificar os protótipos que afetarão todos os objetos criados na classe.

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

Eu originalmente escrevi um pouco disso com esta resposta .

Lucas
fonte
2
O OP está solicitando várias cadeias de protótipos (por exemplo, childherda de parent1e parent2). Seu exemplo fala apenas de uma cadeia.
chique
0

Um retardatário na cena é SimpleDeclare . No entanto, ao lidar com várias heranças, você ainda terá cópias dos construtores originais. Isso é uma necessidade em Javascript ...

Merc.

Merc
fonte
Isso é uma necessidade em Javascript ... até ES6 Proxies.
22615 Jonathon
Proxies são interessantes! Definitivamente vou procurar alterar o SimpleDeclare para que ele não precise copiar métodos usando proxies depois que eles se tornarem parte do padrão. Código de SimpleDeclare é muito, muito fácil de ler e mudança ...
Merc
0

Eu usaria ds.oop . É semelhante ao prototype.js e outros. torna a herança múltipla muito fácil e minimalista. (apenas 2 ou 3 kb) Também suporta alguns outros recursos interessantes, como interfaces e injeção de dependência

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

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

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

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

var person = new Person();

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

Que tal isso, implementa herança múltipla em 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();

E aqui está o código para a função de utilitário specialize_with ():

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

Este é um código real que é executado. Você pode copiar e colar no arquivo html e tentar você mesmo. Isso funciona.

Esse é o esforço para implementar o MI em JavaScript. Não há muito código, mais conhecimento.

Sinta-se à vontade para consultar meu artigo completo sobre isso, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS

Leonid Titov
fonte
0

Eu apenas usei para atribuir quais classes eu preciso nas propriedades de outras pessoas e adicionar um proxy para apontar automaticamente para elas, como eu gosto:

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
shamaseen
fonte