Wiele dziedziczenia / prototypów w JavaScript

132

Doszedłem do punktu, w którym muszę mieć pewne podstawowe dziedziczenie wielokrotne w JavaScript. (Nie jestem tutaj, aby dyskutować, czy to dobry pomysł, czy nie, więc prosimy o zachowanie tych komentarzy dla siebie).

Chcę tylko wiedzieć, czy ktoś próbował tego dokonać z jakimkolwiek (lub nie) sukcesem i jak sobie z tym poradzili.

Podsumowując, to, czego naprawdę potrzebuję, to mieć obiekt zdolny do dziedziczenia właściwości z więcej niż jednego łańcucha prototypów (tj. Każdy prototyp mógłby mieć swój własny łańcuch), ale w określonej kolejności (będzie przeszukaj łańcuchy w celu znalezienia pierwszej definicji).

Aby zademonstrować, jak jest to teoretycznie możliwe, można to osiągnąć, dołączając łańcuch wtórny na końcu łańcucha głównego, ale wpłynęłoby to na wszystkie wystąpienia któregokolwiek z tych poprzednich prototypów, a nie tego chcę.

Myśli?

devios1
źródło
1
Myślę dojo DECLARE uchwyty wielokrotne dziedziczenie src też mam Mootools uczucie ma również dużo tego jest poza mną, ale będę mieć szybki odczyt z tego co sugeruje dojo
TI
Spójrz na TraitsJS ( link 1 , link 2 ) to naprawdę dobra alternatywa dla wielokrotnego dziedziczenia i miksów ...
Christian C. Salvadó,
1
@Pointy, ponieważ nie jest to zbyt dynamiczne. Chciałbym móc odbierać zmiany wprowadzone w dowolnym łańcuchu nadrzędnym w miarę ich pojawiania się. Jednak to powiedziawszy, być może będę musiał uciekać się do tego, jeśli to po prostu nie jest możliwe.
devios1
1
Ciekawa lektura na ten temat: webreflection.blogspot.co.uk/2009/06/…
Aurelio

Odpowiedzi:

50

Wielokrotne dziedziczenie można osiągnąć w ECMAScript 6 przy użyciu obiektów Proxy .

Realizacja

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

Wyjaśnienie

Obiekt proxy składa się z obiektu docelowego i kilku pułapek, które definiują niestandardowe zachowanie dla podstawowych operacji.

Tworząc obiekt, który dziedziczy po innym, używamy Object.create(obj). Ale w tym przypadku chcemy dziedziczenia wielokrotnego, więc zamiast tego objużywam proxy, które przekieruje podstawowe operacje do odpowiedniego obiektu.

Używam tych pułapek:

  • hasPułapka jest pułapka na inoperatora . Używam somedo sprawdzenia, czy przynajmniej jeden prototyp zawiera właściwość.
  • getPułapka jest pułapką dla uzyskania wartości nieruchomości. Używam, findaby znaleźć pierwszy prototyp, który zawiera tę właściwość, i zwracam wartość lub wywołuję getter na odpowiednim odbiorniku. Jest to obsługiwane przez Reflect.get. Jeśli żaden prototyp nie zawiera właściwości, zwracam undefined.
  • setPułapka jest pułapką do ustawiania wartości właściwości. Używam finddo znalezienia pierwszego prototypu, który zawiera tę właściwość, i wywołuję jego ustawiającą na odpowiednim odbiorniku. Jeśli nie ma metody ustawiającej lub prototyp nie zawiera właściwości, wartość jest definiowana w odpowiednim odbiorniku. Jest to obsługiwane przez Reflect.set.
  • enumeratePułapka jest pułapką dla for...inpętli . Iteruję wyliczalne właściwości od pierwszego prototypu, potem od drugiego i tak dalej. Po przeprowadzeniu iteracji właściwości przechowuję ją w tablicy mieszania, aby uniknąć jej ponownego wykonywania.
    Ostrzeżenie : ta pułapka została usunięta w wersji roboczej ES7 i jest przestarzała w przeglądarkach.
  • ownKeysPułapka jest pułapką dla Object.getOwnPropertyNames(). Od ES7 for...inpętle wciąż wywołują [[GetPrototypeOf]] i pobierają własne właściwości każdej z nich. Aby więc iterować właściwości wszystkich prototypów, używam tej pułapki, aby wszystkie wyliczalne odziedziczone właściwości wyglądały jak własne właściwości.
  • getOwnPropertyDescriptorPułapka jest pułapką dla Object.getOwnPropertyDescriptor(). ownKeysNie wystarczy for...insprawić, by wszystkie wyliczalne właściwości wyglądały jak własne właściwości w pułapce, pętle otrzymają deskryptor, aby sprawdzić, czy są one wyliczalne. Więc używam finddo znalezienia pierwszego prototypu, który zawiera tę właściwość, i powtarzam jego łańcuch prototypowy, aż znajdę właściciela nieruchomości i zwracam jej deskryptor. Jeśli żaden prototyp nie zawiera właściwości, zwracam undefined. Deskryptor jest modyfikowany, aby był konfigurowalny, w przeciwnym razie moglibyśmy złamać niektóre niezmienniki proxy.
  • preventExtensionsI definePropertypułapki są jedynie w celu zapobieżenia tych operacji z modyfikując bramkę proxy. W przeciwnym razie mogłoby dojść do zerwania niektórych niezmienników proxy.

Dostępnych jest więcej pułapek, których nie używam

  • getPrototypeOfPułapka można dodać, ale nie jest właściwa droga do powrotu wielu prototypów. Oznacza to, instanceofże też nie zadziała. Dlatego pozwoliłem mu uzyskać prototyp celu, który początkowo jest zerowy.
  • setPrototypeOfPułapka może być dodany i przyjmować szereg obiektów, które zastępują prototypy. Czytelnikowi pozostaje to ćwiczeniem. Tutaj po prostu pozwolę mu zmodyfikować prototyp celu, co nie jest zbyt przydatne, ponieważ żadna pułapka nie używa celu.
  • deletePropertyPułapka jest pułapką na usuwanie właściwości własnych. Proxy reprezentuje dziedziczenie, więc nie miałoby to większego sensu. Pozwoliłem mu na próbę usunięcia celu, który i tak nie powinien mieć żadnej właściwości.
  • isExtensiblePułapka jest pułapką dla uzyskania rozciągliwości. Niezbyt przydatne, biorąc pod uwagę, że niezmiennik zmusza go do zwrócenia takiej samej rozszerzalności jak cel. Więc po prostu pozwoliłem mu przekierować operację do celu, który będzie rozszerzalny.
  • applyI constructpułapki są pułapki dla telefonicznie lub instancji. Są użyteczne tylko wtedy, gdy celem jest funkcja lub konstruktor.

Przykład

// 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
źródło
1
Czy nie ma problemów z wydajnością, które stałyby się istotne nawet w aplikacjach o normalnej skali?
Tomáš Zato - Przywróć Monikę
1
@ TomášZato Będzie wolniej niż właściwości danych w normalnym obiekcie, ale nie sądzę, że będzie znacznie gorsze niż właściwości akcesora.
Oriol
TIL:multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
bloodyKnuckles
4
Rozważałbym zastąpienie „Wielokrotne dziedziczenie” przez „Wielokrotne delegowanie”, aby lepiej zrozumieć, co się dzieje. Kluczową koncepcją w twojej implementacji jest to, że proxy faktycznie wybiera właściwy obiekt do delegowania (lub przekazywania dalej) wiadomości. Moc twojego rozwiązania polega na tym, że możesz dynamicznie rozszerzać docelowy prototyp / prototypy. Inne odpowiedzi używają konkatenacji (ala Object.assign) lub uzyskują zupełnie inny wykres, w końcu wszystkie otrzymują łańcuch prototypów jeden-jedyny między obiektami. Rozwiązanie proxy oferuje rozgałęzienie środowiska wykonawczego, a to rządzi!
sminutoli
Jeśli chodzi o wydajność, jeśli utworzysz obiekt, który dziedziczy z wielu obiektów, który dziedziczy z wielu obiektów itd., Stanie się wykładniczy. Więc tak, będzie wolniej. Ale w normalnych przypadkach nie sądzę, że będzie tak źle.
Oriol
16

Aktualizacja (2019): oryginalny post staje się dość nieaktualny. Ten artykuł (teraz link do archiwum internetowego, odkąd domena zniknęła) i związana z nim biblioteka GitHub to dobre, nowoczesne podejście.

Oryginalny post: Dziedziczenie wielokrotne [edytuj, nie jest właściwe dziedziczenie typu, ale właściwości; mixins] w JavaScript jest całkiem proste, jeśli używasz skonstruowanych prototypów zamiast prototypów obiektów generycznych. Oto dwie klasy nadrzędne do dziedziczenia:

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

Zwróć uwagę, że w każdym przypadku użyłem tego samego członka „imię”, co mogłoby być problemem, gdyby rodzice nie uzgodnili, jak należy traktować „imię”. Ale w tym przypadku są one zgodne (tak naprawdę zbędne).

Teraz potrzebujemy tylko klasy, która dziedziczy po obu. Dziedziczenie odbywa się poprzez wywołanie funkcji konstruktora (bez użycia słowa kluczowego new) dla prototypów i konstruktorów obiektów. Po pierwsze, prototyp musi dziedziczyć po prototypach nadrzędnych

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

Konstruktor musi dziedziczyć po konstruktorach nadrzędnych:

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

Teraz możesz uprawiać, jeść i zbierać różne instancje:

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.
źródło
Czy możesz to zrobić za pomocą wbudowanych prototypów? (Array, String, Number)
Tomáš Zato - Przywróć Monikę
Nie sądzę, by wbudowane prototypy miały konstruktorów, których można by wywołać.
Roy J
Cóż, mogę to zrobić, Array.call(...)ale wydaje się, że nie ma to wpływu na to, co nazywam this.
Tomáš Zato - Przywróć Monikę
@ TomášZato Możesz to zrobićArray.prototype.constructor.call()
Roy J
1
@AbhishekGupta Dzięki za poinformowanie mnie. Zamieniłem łącze na łącze do zarchiwizowanej strony internetowej.
Roy J
7

Ten służy Object.createdo stworzenia prawdziwego łańcucha prototypów:

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

Na przykład:

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

wróci:

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

tak że obj.a === 1, obj.b === 3itp

pimvdb
źródło
Tylko krótkie hipotetyczne pytanie: chciałem stworzyć klasę Vector, mieszając prototypy Number i Array (dla zabawy). To dałoby mi zarówno indeksy tablicowe, jak i operatory matematyczne. Ale czy to zadziała?
Tomáš Zato - Przywróć Monikę
@ TomášZato, warto sprawdzić ten artykuł , jeśli szukasz podklasy tablic; może zaoszczędzić trochę bólu głowy. powodzenia!
user3276552
5

Podoba mi się implementacja struktury klas przez Johna Resiga: http://ejohn.org/blog/simple-javascript-inheritance/

Można to po prostu rozszerzyć na coś takiego:

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

    // same code
}

co pozwoli ci przekazać wiele obiektów dziedziczonych. Stracisz instanceOftutaj zdolność, ale to jest dane, jeśli chcesz dziedziczyć wiele.


mój dość zawiły przykład powyższego jest dostępny pod adresem https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js

Zauważ, że w tym pliku jest jakiś martwy kod, ale pozwala na wielokrotne dziedziczenie, jeśli chcesz się przyjrzeć.


Jeśli chcesz mieć dziedziczenie łańcuchowe (NIE dziedziczenie wielokrotne, ale dla większości ludzi jest to to samo), można to osiągnąć za pomocą klasy, takiej jak:

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

który zachowa oryginalny łańcuch prototypów, ale będziesz też mieć uruchomionych wiele bezcelowego kodu.

Mark Kahn
źródło
7
To tworzy scalony, płytki klon. Dodanie nowej właściwości do „dziedziczonych” obiektów nie spowoduje pojawienia się nowej właściwości w obiekcie pochodnym, tak jak w przypadku prawdziwego dziedziczenia prototypów.
Daniel Earwicker,
@DanielEarwicker - prawda, ale jeśli chcesz, aby „wielokrotne dziedziczenie” w tej jednej klasie pochodziło z dwóch klas, tak naprawdę nie ma alternatywy. Zmodyfikowana odpowiedź, aby odzwierciedlić, że zwykłe łączenie klas w łańcuch jest w większości przypadków to samo.
Mark Kahn,
Wygląda na to, że zniknął twój GitHUb, czy nadal masz github.com/cwolves/Fetch/blob/master/support/plugins/klass/ ... Nie miałbym nic przeciwko temu, żeby go zobaczyć, gdybyś chciał się nim podzielić?
JasonDavis
4

Nie daj się zmylić z implementacjami wielokrotnego dziedziczenia w środowisku JavaScript.

Wszystko, co musisz zrobić, to użyć Object.create (), aby za każdym razem utworzyć nowy obiekt z określonym obiektem prototypowym i właściwościami, a następnie pamiętaj, aby zmienić Object.prototype.constructor na każdym kroku, jeśli planujesz tworzenie instancji Bw przyszłość.

Dziedziczenie właściwości np thisAi thisBużywamy Function.prototype.call () pod koniec każdego obiektu funkcji. Jest to opcjonalne, jeśli zależy Ci tylko na odziedziczeniu prototypu.

Uruchom gdzieś następujący kod i obserwuj 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 dziedziczy prototyp z A
  • C dziedziczy prototyp z B
  • objC jest przykładem C

To jest dobre wyjaśnienie powyższych kroków:

OOP w JavaScript: co musisz wiedzieć

Dave
źródło
Czy to jednak nie kopiuje wszystkich właściwości do nowego obiektu? Więc jeśli masz dwa prototypy, A i B, i odtworzysz je oba w C, zmiana właściwości A nie wpłynie na tę właściwość w C i odwrotnie. Otrzymasz kopię wszystkich właściwości w A i B zapisanych w pamięci. Byłaby to taka sama wydajność, jak gdybyś na stałe zakodował wszystkie właściwości A i B w C. Jest to przyjemne dla czytelności, a wyszukiwanie właściwości nie musi podróżować do obiektów nadrzędnych, ale tak naprawdę nie jest dziedziczeniem - bardziej jak klonowanie. Zmiana właściwości na A nie zmienia sklonowanej właściwości na C.
Frank,
2

W żaden sposób nie jestem ekspertem w javascript OOP, ale jeśli dobrze cię rozumiem, chcesz coś takiego (pseudo-kod):

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

Cat inherit from (Earth, Animal);

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

W takim razie spróbuję czegoś takiego:

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
źródło
1
Czy to nie jest po prostu wybranie pierwszego prototypu i zignorowanie reszty? c.prototypeWielokrotne ustawienie nie daje wielu prototypów. Na przykład, gdyby tak było Animal.isAlive = true, Cat.isAlivenadal byłby niezdefiniowany.
devios1
Tak, miałem zamiar wymieszać prototypy, poprawić ... (użyłem tutaj rozszerzenia jQuery, ale masz zdjęcie)
David Hellsing,
2

Możliwe jest zaimplementowanie wielokrotnego dziedziczenia w JavaScript, chociaż robi to bardzo niewiele bibliotek.

Mógłbym wskazać Ring.js , jedyny przykład, jaki znam.

nicolas-van
źródło
2

Dużo dzisiaj nad tym pracowałem i próbowałem to osiągnąć samodzielnie w ES6. Sposób, w jaki to zrobiłem, polegał na użyciu Browserify, Babel, a następnie przetestowałem go z Wallaby i wydawało się, że działa. Moim celem jest rozszerzenie obecnego Array, włączenie ES6, ES7 i dodanie dodatkowych niestandardowych funkcji, których potrzebuję w prototypie do obsługi danych audio.

Wallaby zdał 4 z moich testów. Plik example.js można wkleić w konsoli i widać, że właściwość „include” znajduje się w prototypie klasy. Nadal chcę jutro to przetestować.

Oto moja metoda: (najprawdopodobniej zrefaktoruję i przepakuję jako moduł po pewnym czasie snu!)

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

class ArrayIncludesPollyfills extends Array {}

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

inherit(keys);

module.exports = ArrayIncludesPollyfills

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

Daniel Ram
źródło
2

Myślę, że to absurdalnie proste. Problem polega na tym, że klasa podrzędna będzie odnosić się tylko do instanceofpierwszej klasy, do której dzwonisz

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
źródło
1

Sprawdź poniższy kod, który pokazuje, że obsługuje dziedziczenie wielokrotne. Wykonano przy użyciu PROTOTYPAL INHERITANCE

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

    this.name = name;
}

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

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

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

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

const a = new A("shivang");
const b = new B(32);
console.log(a.name);
console.log(b.age);
const ab = new AB("indu", 27);
console.log(ab.toString());
Shivang Gupta
źródło
1

Mam całkiem funkcję umożliwiającą definiowanie klas z wielokrotnym dziedziczeniem. Pozwala na kod podobny do następującego. Ogólnie zauważysz całkowite odejście od natywnych technik klasyfikowania w javascript (np. Nigdy nie zobaczysz classsłowa kluczowego):

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

aby wygenerować taki wynik:

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

Oto jak wyglądają definicje klas:

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

Widzimy, że każda definicja klasy używająca makeClassfunkcji akceptuje Objectnazwy klas rodzicielskich odwzorowane na klasy nadrzędne. Akceptuje również funkcję, która zwraca Objectwłaściwości zawierające dla definiowanej klasy. Ta funkcja ma parametr protos, który zawiera wystarczającą ilość informacji, aby uzyskać dostęp do dowolnej właściwości zdefiniowanej przez którąkolwiek z klas nadrzędnych.

Ostatnim wymaganym elementem jest makeClasssama funkcja, która wykonuje całkiem sporo pracy. Oto ona, wraz z resztą kodu. Skomentowałem makeClassdość mocno:

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

makeClassFunkcja obsługuje także właściwości klasy; są one definiowane przez poprzedzanie nazw właściwości $symbolem (zwróć uwagę, że ostateczna nazwa właściwości, która zostanie wywołana, zostanie $usunięta). Mając to na uwadze, moglibyśmy napisać wyspecjalizowaną Dragonklasę, która modeluje „typ” Smoka, gdzie lista dostępnych typów Smoka jest przechowywana w samej klasie, w przeciwieństwie do instancji:

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

Wyzwania wielokrotnego dziedziczenia

Każdy, kto makeClassuważnie śledził kod , zauważy dość znaczące niepożądane zjawisko występujące po cichu, gdy powyższy kod zostanie uruchomiony: utworzenie wystąpienia a RunningFlyingspowoduje DWIE wywołania Namedkonstruktora!

Dzieje się tak, ponieważ wykres dziedziczenia wygląda następująco:

 (^^ More Specialized ^^)

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

  (vv More Abstract vv)

Gdy istnieje wiele ścieżek do tej samej klasy nadrzędnej na wykresie dziedziczenia podklasy, wystąpienia tej podklasy będą wielokrotnie wywoływać konstruktor tej klasy nadrzędnej.

Zwalczanie tego jest nietrywialne. Spójrzmy na kilka przykładów z uproszczonymi nazwami klas. Rozważymy klasę A, najbardziej abstrakcyjną klasę-rodzica, klasy Bi C, które zarówno dziedziczą po A, jak i klasę, BCktóra dziedziczy z Bi C(a zatem koncepcyjnie „dziedziczy podwójne” z 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');
  }
}));

Jeśli chcemy uniknąć BCpodwójnego wywoływania A.prototype.init, może zaistnieć potrzeba porzucenia stylu bezpośredniego wywoływania konstruktorów dziedziczonych. Będziemy potrzebować pewnego poziomu pośrednictwa, aby sprawdzić, czy występują zduplikowane połączenia, i zwarcia, zanim się pojawią.

Możemy rozważyć zmianę parametrów dostarczanej do nieruchomości funkcjonować: obok protos, Objectzawierające surowe dane opisujące odziedziczone właściwości, możemy także funkcję użytkową dla wywołanie metody instancji w taki sposób, że metody macierzyste nazywane są również, ale są wykrywane zduplikowane połączenia i zapobiec. Przyjrzyjmy się, gdzie ustalamy parametry 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 ... */

};

Celem powyższej zmiany makeClassjest to, że mamy dodatkowy argument dostarczony do naszego, propertiesFngdy wywołujemy makeClass. Powinniśmy również mieć świadomość, że każda funkcja zdefiniowana w dowolnej klasie może teraz otrzymać parametr po wszystkich swoich nazwach dup, czyli a, Setktóry zawiera wszystkie funkcje, które zostały już wywołane w wyniku wywołania odziedziczonej metody:

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

Ten nowy styl faktycznie zapewnia, że "Construct A"jest rejestrowany tylko raz, gdy instancja programu BCjest inicjowana. Ale są trzy wady, z których trzecia jest bardzo krytyczna :

  1. Ten kod stał się mniej czytelny i trudny do utrzymania. Za tą util.invokeNoDuplicatesfunkcją kryje się duża złożoność , a myślenie o tym, jak ten styl unika wielu wywołań, jest nieintuicyjne i wywołuje ból głowy. Mamy również ten nieznośny dupsparametr, który naprawdę musi być zdefiniowany w każdej funkcji w klasie . Auć.
  2. Ten kod jest wolniejszy - do uzyskania pożądanych wyników z wielokrotnym dziedziczeniem wymagane jest trochę więcej pośrednich i obliczeniowych. Niestety, prawdopodobnie tak będzie w przypadku każdego rozwiązania naszego problemu z wielokrotnymi wywołaniami.
  3. Co najważniejsze, stała się struktura funkcji, które opierają się na dziedziczeniu bardzo sztywna . Jeśli podklasaNiftyClassprzesłania funkcjęniftyFunctioni użyje jejutil.invokeNoDuplicates(this, 'niftyFunction', ...)do uruchomienia bez wywołania duplikatu,NiftyClass.prototype.niftyFunctionwywoła funkcję o nazwieniftyFunctionkażdej klasy nadrzędnej, która ją definiuje, zignoruje wszelkie wartości zwracane z tych klas i na koniec wykona wyspecjalizowaną logikęNiftyClass.prototype.niftyFunction. To jedyna możliwa konstrukcja . JeśliNiftyClassdziedziczyCoolClassiGoodClass, a obie te klasy nadrzędne dostarczająniftyFunctionwłasnych definicji,NiftyClass.prototype.niftyFunctionnigdy (bez ryzyka wielokrotnego wywołania) nie będą w stanie:
    • A. Uruchom NiftyClassnajpierw wyspecjalizowaną logikę , a następnie wyspecjalizowaną logikę klas rodzicielskich
    • B. Uruchom wyspecjalizowaną logikę NiftyClassw dowolnym momencie innym niż po zakończeniu całej wyspecjalizowanej logiki nadrzędnej
    • C. Zachowywać się warunkowo w zależności od wartości zwracanych przez wyspecjalizowaną logikę rodzica
    • D. Unikaj w niftyFunctionogóle prowadzenia specjalizacji konkretnego rodzica

Oczywiście, możemy rozwiązać każdy problem z literami powyżej, definiując wyspecjalizowane funkcje w util:

  • A. zdefiniowaćutil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. define util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(gdzie parentNamejest nazwą rodzica, którego wyspecjalizowana logika zostanie bezpośrednio poprzedzona wyspecjalizowaną logiką klas potomnych)
  • DO. define util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(W tym przypadku testFnotrzyma wynik wyspecjalizowanej logiki dla nazwanego rodzica parentNamei zwróci true/falsewartość wskazującą, czy powinno nastąpić zwarcie)
  • RE. zdefiniować util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(w tym przypadku blackListbyłoby to Arraynazwami rodziców, których wyspecjalizowana logika powinna zostać całkowicie pominięta)

Wszystkie te rozwiązania są dostępne, ale to totalny chaos ! Dla każdej unikalnej struktury, którą może przyjąć wywołanie funkcji dziedziczonej, potrzebowalibyśmy wyspecjalizowanej metody zdefiniowanej w util. Co za absolutna katastrofa.

Mając to na uwadze, możemy zacząć dostrzegać wyzwania związane z wdrażaniem dobrego wielokrotnego dziedziczenia. Pełna implementacja, makeClassktórą podałem w tej odpowiedzi, nie uwzględnia nawet problemu wielokrotnych wywołań ani wielu innych problemów, które pojawiają się w związku z wielokrotnym dziedziczeniem.

Ta odpowiedź jest bardzo długa. Mam nadzieję, makeClassże dołączona przeze mnie implementacja jest nadal przydatna, nawet jeśli nie jest idealna. Mam również nadzieję, że każdy zainteresowany tym tematem zyskał więcej kontekstu, o którym powinien pamiętać, czytając dalej!

Gershom
źródło
0

Spójrz na pakiet IeUnit .

Asymilacja koncepcji zaimplementowana w IeUnit wydaje się oferować to, czego szukasz, w dość dynamiczny sposób.

James
źródło
0

Oto przykład tworzenia łańcucha prototypów przy użyciu funkcji konstruktora :

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

W tej koncepcji wykorzystano definicję „klasy” dla języka JavaScript Yehuda Katz :

... „klasa” JavaScript to po prostu obiekt Function, który służy jako konstruktor oraz dołączony obiekt prototypowy. ( Źródło: Guru Katz )

W przeciwieństwie do metody Object.create , kiedy klasy są budowane w ten sposób i chcemy tworzyć instancje „klasy”, nie musimy wiedzieć, z czego dziedziczy każda „klasa”. Po prostu używamy 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

Porządek prekendencji powinien mieć sens. Najpierw wygląda w obiekcie instancji, potem jest to prototyp, potem następny prototyp itd.

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

Możemy również modyfikować prototypy, które będą miały wpływ na wszystkie obiekty zbudowane na tej klasie.

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

Pierwotnie napisałem niektóre z nich za pomocą tej odpowiedzi .

Łukasz
źródło
2
OP prosi o wiele łańcuchów prototypów (np. childDziedziczy po parent1i parent2). Twój przykład mówi tylko o jednym łańcuchu.
najpiękniejszy
0

Spóźniony na scenie to SimpleDeclare . Jednak w przypadku dziedziczenia wielokrotnego nadal będziesz mieć kopie oryginalnych konstruktorów. To konieczność w Javascript ...

Merc.

Merc
źródło
To konieczność w Javascript ... aż do serwerów proxy ES6.
Jonathon
Proxy są interesujące! Zdecydowanie przyjrzę się zmianie SimpleDeclare, aby nie trzeba było kopiować metod za pomocą serwerów proxy, gdy staną się częścią standardu. Kod SimpleDeclare jest naprawdę, bardzo łatwy do odczytania i zmiany ...
Merc
0

Użyłbym ds.oop . Jest podobny do prototype.js i innych. sprawia, że ​​wielokrotne dziedziczenie jest bardzo łatwe i minimalistyczne. (tylko 2 lub 3 kb) Obsługuje również inne fajne funkcje, takie jak interfejsy i wstrzykiwanie zależności

/*** 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
źródło
0

Co powiesz na to, implementuje dziedziczenie wielokrotne w 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();

A oto kod funkcji narzędzia specialize_with ():

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

To jest prawdziwy kod, który działa. Możesz go skopiować i wkleić do pliku html i spróbować samemu. To działa.

To wysiłek związany z wdrożeniem MI w JavaScript. Niewiele kodu, więcej know-how.

Zapraszam do obejrzenia mojego pełnego artykułu na ten temat, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS

latitov
źródło
0

Po prostu przypisywałem potrzebne mi klasy we właściwościach innych i dodawałem proxy do automatycznego wskazywania na nie, które lubię:

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
źródło