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?
Odpowiedzi:
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 tegoobj
używam proxy, które przekieruje podstawowe operacje do odpowiedniego obiektu.Używam tych pułapek:
has
Pułapka jest pułapka nain
operatora . Używamsome
do sprawdzenia, czy przynajmniej jeden prototyp zawiera właściwość.get
Pułapka jest pułapką dla uzyskania wartości nieruchomości. Używam,find
aby znaleźć pierwszy prototyp, który zawiera tę właściwość, i zwracam wartość lub wywołuję getter na odpowiednim odbiorniku. Jest to obsługiwane przezReflect.get
. Jeśli żaden prototyp nie zawiera właściwości, zwracamundefined
.set
Pułapka jest pułapką do ustawiania wartości właściwości. Używamfind
do 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 przezReflect.set
.enumerate
Pułapka jest pułapką dlafor...in
pę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.
ownKeys
Pułapka jest pułapką dlaObject.getOwnPropertyNames()
. Od ES7for...in
pę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.getOwnPropertyDescriptor
Pułapka jest pułapką dlaObject.getOwnPropertyDescriptor()
.ownKeys
Nie wystarczyfor...in
sprawić, 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żywamfind
do 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, zwracamundefined
. Deskryptor jest modyfikowany, aby był konfigurowalny, w przeciwnym razie moglibyśmy złamać niektóre niezmienniki proxy.preventExtensions
IdefineProperty
puł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
getPrototypeOf
Puł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.setPrototypeOf
Puł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.deleteProperty
Puł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.isExtensible
Puł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.apply
Iconstruct
puł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"
źródło
multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
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!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();
źródło
Array.call(...)
ale wydaje się, że nie ma to wpływu na to, co nazywamthis
.Array.prototype.constructor.call()
Ten służy
Object.create
do 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 === 3
itpźródło
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
instanceOf
tutaj 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.
źródło
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
B
w przyszłość.Dziedziczenie właściwości np
thisA
ithisB
uż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 zA
C
dziedziczy prototyp zB
objC
jest przykłademC
To jest dobre wyjaśnienie powyższych kroków:
OOP w JavaScript: co musisz wiedzieć
źródło
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]()); } }
źródło
c.prototype
Wielokrotne ustawienie nie daje wielu prototypów. Na przykład, gdyby tak byłoAnimal.isAlive = true
,Cat.isAlive
nadal byłby niezdefiniowany.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.
źródło
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
źródło
Myślę, że to absurdalnie proste. Problem polega na tym, że klasa podrzędna będzie odnosić się tylko do
instanceof
pierwszej klasy, do której dzwoniszhttps://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
źródło
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());
źródło
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
class
sł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
makeClass
funkcji akceptujeObject
nazwy klas rodzicielskich odwzorowane na klasy nadrzędne. Akceptuje również funkcję, która zwracaObject
właściwości zawierające dla definiowanej klasy. Ta funkcja ma parametrprotos
, 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
makeClass
sama funkcja, która wykonuje całkiem sporo pracy. Oto ona, wraz z resztą kodu. SkomentowałemmakeClass
dość 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();
makeClass
Funkcja 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ąDragon
klasę, 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
makeClass
uważ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 aRunningFlying
spowoduje DWIE wywołaniaNamed
konstruktora!Dzieje się tak, ponieważ wykres dziedziczenia wygląda następująco:
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, klasyB
iC
, które zarówno dziedziczą poA
, jak i klasę,BC
która dziedziczy zB
iC
(a zatem koncepcyjnie „dziedziczy podwójne” zA
):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ąć
BC
podwójnego wywoływaniaA.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
,Object
zawierają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 parametrypropertiesFn
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
makeClass
jest to, że mamy dodatkowy argument dostarczony do naszego,propertiesFn
gdy wywołujemymakeClass
. Powinniśmy również mieć świadomość, że każda funkcja zdefiniowana w dowolnej klasie może teraz otrzymać parametr po wszystkich swoich nazwachdup
, czyli a,Set
któ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 programuBC
jest inicjowana. Ale są trzy wady, z których trzecia jest bardzo krytyczna :util.invokeNoDuplicates
funkcją 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śnydups
parametr, który naprawdę musi być zdefiniowany w każdej funkcji w klasie . Auć.NiftyClass
przesłania funkcjęniftyFunction
i użyje jejutil.invokeNoDuplicates(this, 'niftyFunction', ...)
do uruchomienia bez wywołania duplikatu,NiftyClass.prototype.niftyFunction
wywoła funkcję o nazwieniftyFunction
każ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śliNiftyClass
dziedziczyCoolClass
iGoodClass
, a obie te klasy nadrzędne dostarczająniftyFunction
własnych definicji,NiftyClass.prototype.niftyFunction
nigdy (bez ryzyka wielokrotnego wywołania) nie będą w stanie:NiftyClass
najpierw wyspecjalizowaną logikę , a następnie wyspecjalizowaną logikę klas rodzicielskichNiftyClass
w dowolnym momencie innym niż po zakończeniu całej wyspecjalizowanej logiki nadrzędnejniftyFunction
ogóle prowadzenia specjalizacji konkretnego rodzicaOczywiście, możemy rozwiązać każdy problem z literami powyżej, definiując wyspecjalizowane funkcje w
util
:util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)
(gdzieparentName
jest nazwą rodzica, którego wyspecjalizowana logika zostanie bezpośrednio poprzedzona wyspecjalizowaną logiką klas potomnych)util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)
(W tym przypadkutestFn
otrzyma wynik wyspecjalizowanej logiki dla nazwanego rodzicaparentName
i zwrócitrue/false
wartość wskazującą, czy powinno nastąpić zwarcie)util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)
(w tym przypadkublackList
byłoby toArray
nazwami 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,
makeClass
któ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!źródło
Spójrz na pakiet IeUnit .
Asymilacja koncepcji zaimplementowana w IeUnit wydaje się oferować to, czego szukasz, w dość dynamiczny sposób.
źródło
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 :
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 .
źródło
child
Dziedziczy poparent1
iparent2
). Twój przykład mówi tylko o jednym łańcuchu.Spóźniony na scenie to SimpleDeclare . Jednak w przypadku dziedziczenia wielokrotnego nadal będziesz mieć kopie oryginalnych konstruktorów. To konieczność w Javascript ...
Merc.
źródło
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();
źródło
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
źródło
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
źródło