У мене є ціла функція, що дозволяє визначати класи з множинним успадкуванням. Він дозволяє ввести наступний код. Загалом ви відзначите повний відхід від рідних методів класифікації у JavaScript (наприклад, ви ніколи не побачите class
ключове слово):
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();
отримати такий випуск:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
Ось як виглядають визначення класу:
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();
}
}));
Ми можемо бачити, що кожне визначення класу за допомогою makeClass
функції приймає Object
імена батьківського класу, відображені в батьківських класах. Він також приймає функцію, яка повертає Object
містять властивості для визначеного класу. Ця функція має параметр protos
, який містить достатньо інформації для доступу до будь-якого властивості, визначеного будь-яким з батьківських класів.
Останній необхідний фрагмент - сама makeClass
функція, яка робить трохи роботи. Ось він разом із рештою коду. Я makeClass
досить сильно прокоментував :
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
Функція також підтримує властивість класу; вони визначаються за допомогою префіксації імен властивостей $
символом (зауважте, що остаточне ім'я властивості, яке отримає результати, буде $
видалено). Зважаючи на це, ми могли б написати спеціалізований Dragon
клас, який моделює "тип" Дракона, де список доступних типів Драконів зберігається в самому Класі, на відміну від примірників:
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 });
Виклики багаторазового спадкування
Кожен, хто makeClass
уважно стежив за кодом , відзначить досить значне небажане явище, яке виникає мовчки при запуску вищезазначеного коду: інстанціювання a RunningFlying
призведе до ДВА викликів Named
конструктору!
Це тому, що графік успадкування виглядає так:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
Коли в графі спадкування підкласу є кілька шляхів до одного і того ж батьківського класу , інстанції підкласу будуть викликати конструктор цього батьківського класу кілька разів.
Боротьба з цим нетривіальна. Давайте розглянемо деякі приклади зі спрощеними іменами класів. Ми розглянемо клас A
, найбільш абстрактний батьківський клас, класи B
і C
, які успадковують A
, і клас, BC
який успадковує від B
і C
(і, отже, концептуально "подвійно успадковує" від 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');
}
}));
Якщо ми хочемо запобігти BC
подвійному виклику, A.prototype.init
нам може знадобитися відмовитися від стилю прямого виклику успадкованих конструкторів. Нам знадобиться певний рівень непрямості, щоб перевірити, чи відбуваються повторювані дзвінки, та коротке замикання перед тим, як вони відбудуться.
Ми можемо розглянути можливість зміни параметрів, що надходять до функції властивостей: поряд protos
із Object
вмістом необроблених даних, що описують успадковані властивості, ми також могли б включати функцію утиліти для виклику методу екземпляра таким чином, що також викликуються батьківські методи, але виявляються повторювані виклики і запобігти. Давайте подивимось, де ми встановлюємо параметри для 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 ... */
};
Вся ціль вищезазначеного зміни на makeClass
те, щоб у нас був додатковий аргумент, який ми надаємо propertiesFn
при зверненні makeClass
. Ми також повинні знати, що кожна функція, визначена в будь-якому класі, тепер може отримувати параметр після всіх інших його назв dup
, який є a, Set
який містить усі функції, які вже були викликані в результаті виклику успадкованого методу:
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');
}
}));
Цей новий стиль насправді вдається забезпечити, "Construct A"
він реєструється лише один раз, коли BC
ініціалізується екземпляр . Але є три мінуси, третій з яких є дуже критичним :
- Цей код став менш читабельним та ретельним. За
util.invokeNoDuplicates
функцією ховається чимало складностей , а думка про те, як цей стиль уникає багатовикликання, не інтуїтивно зрозуміла і не викликає головного болю. У нас також є той прискіпливий dups
параметр, який дійсно потрібно визначати для кожної функції в класі . Ой.
- Цей код повільніше - для досягнення бажаних результатів при багатократному успадкуванні потрібно трохи більше опосередкованості та обчислень. На жаль, це, мабуть, стосується будь-якого рішення нашої проблеми з кількома викликами.
- Найбільш суттєво, структура функцій, які покладаються на спадкування, стала дуже жорсткою . Якщо підклас
NiftyClass
замінює функцію niftyFunction
і використовує util.invokeNoDuplicates(this, 'niftyFunction', ...)
для запуску її без повторного виклику, NiftyClass.prototype.niftyFunction
викличе функцію, названу niftyFunction
кожного батьківського класу, який її визначає, ігнорує будь-які зворотні значення з цих класів і, нарешті, виконує спеціалізовану логіку NiftyClass.prototype.niftyFunction
. Це єдина можлива структура . Якщо NiftyClass
успадковується CoolClass
і GoodClass
, і обидва ці батьківські класи забезпечують niftyFunction
визначення своїх власні, NiftyClass.prototype.niftyFunction
ніколи не буде (без ризику множинного виклику) бути в змозі:
- А. Запустіть
NiftyClass
спочатку спеціалізовану логіку , потім спеціалізовану логіку батьківських класів
- B. Запустіть спеціалізовану логіку
NiftyClass
в будь-якій точці, ніж після того, як виконана спеціалізована логіка батьків
- C. Поведіть себе умовно залежно від повернених значень спеціалізованої логіки свого батька
- D. Уникайте роботи конкретного батька спеціалізувалася в
niftyFunction
цілому
Звичайно, ми могли вирішити кожну проблему з літерами вище, визначивши спеціалізовані функції у розділі util
:
- А. визнач
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B. визначити
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)
(Де parentName
ім'я батька, спеціалізована логіка якого буде негайно дотримуватися спеціалізованої логіки дочірніх класів)
- C. визначити
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)
(у цьому випадку testFn
отримає результат спеціалізованої логіки для імені батька parentName
і поверне true/false
значення, що вказує, чи має відбуватися коротке замикання)
- D. визначити
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)
(у цьому випадку blackList
це буде Array
прізвище батьків, спеціалізовану логіку якого слід повністю пропустити)
Ці рішення доступні, але це загальний хаос ! Для кожної унікальної структури, яку може прийняти успадкований виклик функції, нам знадобиться спеціалізований метод, визначений в util
. Яке абсолютне лихо.
Зважаючи на це, ми можемо почати бачити проблеми впровадження доброго багаторазового успадкування. Повна реалізація, яку makeClass
я надав у цій відповіді, навіть не враховує проблему багаторазового виклику чи багатьох інших проблем, які виникають у зв'язку з багаторазовим успадкуванням.
Ця відповідь стає дуже довгою. Я сподіваюся, що makeClass
впроваджена мною програма все ще корисна, навіть якщо вона не є досконалою. Я також сподіваюся, що хтось, хто цікавиться цією темою, отримав більше контексту, щоб пам’ятати, коли вони читають далі!