Кілька спадкових / прототипів у JavaScript


132

Я дійшов до моменту, коли мені потрібно мати якесь рудиментарне множинне спадкування, що відбувається в JavaScript. (Я не тут, щоб обговорювати, чи це гарна ідея чи ні, тому, будь ласка, будь ласка, тримайте ці коментарі при собі.)

Я просто хочу знати, чи хтось намагався зробити це з будь-яким (чи ні) успіхом, і як вони це зробили.

Кип'ятити його вниз, що мені дійсно потрібно, щоб бути в змозі мати об'єкт , здатний успадковувати майно з більш ніж одного прототипу ланцюга (тобто кожен прототипу може мати свою власну ланцюг), але в певному порядку старшинства (воно буде пошук ланцюгів для першого визначення).

Щоб продемонструвати, як це теоретично можливо, цього можна досягти, приєднавши вторинну ланцюг до кінця первинного ланцюга, але це вплине на всі екземпляри будь-якого з попередніх прототипів, і це не те, чого я хочу.

Думки?


1
Я думаю, що доджо заявляє, що обробляє багатократну спадщину src, також у мене є відчуття, що mootools також робить, багато чого це поза мною, але я швидко прочитаю це, як пропонує доджо
TI

Погляньте на TraitsJS ( посилання 1 , посилання 2 ) - це дійсно гарна альтернатива багатократному успадкуванню та міксингу ...
CMS

1
@Pointy, тому що це не дуже динамічно. Я хотів би мати можливість змінити зміни, внесені до будь-якого з батьківських ланцюгів у міру їх виникнення. Однак сказане, можливо, мені доведеться вдатися до цього, якщо це просто неможливо.
devios1


1
Цікаво читайте про це: webreflection.blogspot.co.uk/2009/06/…
Nobita

Відповіді:


49

Багаторазове успадкування може бути досягнуто в ECMAScript 6 за допомогою об'єктів Proxy .

Впровадження

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

Пояснення

Проксі-об'єкт складається з цільового об'єкта та деяких пасток, які визначають власну поведінку для основних операцій.

При створенні об'єкта, який успадковується від іншого, ми використовуємо Object.create(obj). Але в цьому випадку ми хочемо багаторазового успадкування, тому замість цього objя використовую проксі, який перенаправить основні операції на відповідний об'єкт.

Я використовую ці пастки:

  • hasПастка пастка для inоператора . Я використовую someдля перевірки, чи принаймні один прототип містить властивість.
  • getПастка пастка для отримання значень властивостей. Я використовую findдля пошуку першого прототипу, який містить це властивість, і повертаю значення або зателефоную отримувачу на відповідний приймач. Цим займається Reflect.get. Якщо жоден прототип не містить властивості, я повертаюсь undefined.
  • setПастка являє собою пастку для установки значень властивостей. Я використовую findдля пошуку першого прототипу, який містить це властивість, і я називаю його сеттер на відповідному приймачі. Якщо немає налаштування або не встановлено прототип, властивість визначається на відповідному приймачі. Цим займається Reflect.set.
  • enumerateПастка являє собою пастку для for...inпетель . Я повторюю перелічені властивості з першого прототипу, потім з другого тощо. Після ітерації властивості я зберігаю його в хеш-таблиці, щоб уникнути повторення.
    Попередження : ця пастка видалена в чернеті ES7 і застаріла в браузерах.
  • ownKeysПастка пастка для Object.getOwnPropertyNames(). Починаючи з ES7, for...inпетлі продовжують дзвонити [[GetPrototypeOf]] та отримувати власні властивості кожного. Тож для того, щоб зробити його ітерацією властивостей усіх прототипів, я використовую цю пастку, щоб усі перелічені спадкові властивості виглядали як власні властивості.
  • getOwnPropertyDescriptorПастка пастка для Object.getOwnPropertyDescriptor(). Якщо всі перелічені властивості виглядають так, як власних властивостей у ownKeysпастці недостатньо, for...inпетлі отримають дескриптор, щоб перевірити, чи є вони численними. Тому я використовую findдля пошуку першого прототипу, який містить це властивість, і я повторюю його прототипний ланцюг, поки не знайду власника властивості, і поверну його дескриптор. Якщо жоден прототип не містить властивості, я повертаюсь undefined. Дескриптор модифікований, щоб зробити його конфігуруваним, інакше ми можемо зламати деякі проксі-інваріанти.
  • preventExtensionsІ definePropertyпастки включені тільки , щоб запобігти цим операції від зміни проксі - цілі. Інакше ми могли б врешті-решт зламати деяких проксі-інваріантів.

Є більше пасток, якими я не користуюся

  • getPrototypeOfПастки можуть бути додані, але не правильний спосіб повернути кілька прототипів. Це означає, що instanceofтакож не буде працювати. Тому я дозволяю йому отримати прототип цілі, який спочатку є нульовим.
  • setPrototypeOfПастка може бути додана і прийняти масив об'єктів, який замінить прототипи. Це залишається читачею як вправа. Тут я просто дозволю йому змінити прототип цілі, що не дуже корисно, оскільки жодна пастка не використовує ціль.
  • deletePropertyПастка являє собою пастку для видалення власних властивостей. Проксі представляє спадщину, тому це не має особливого сенсу. Я дозволяю йому спробувати видалити ціль, яка не повинна мати властивості.
  • isExtensibleПастка пастка для отримання розтяжності. Не дуже корисно, враховуючи, що інваріант змушує його повернути ту ж розширюваність, що і ціль. Тому я просто дозволю перенаправити операцію на ціль, яка буде розширюваною.
  • applyІ constructпастка пастка для виклику або інстанцірованія. Вони корисні лише тоді, коли ціль - це функція або конструктор.

Приклад

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

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

// Setting properties
obj.c = 3;

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

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

// Property enumeration
for(var p in obj) p; // "c", "b", "a"

1
Чи не є якісь проблеми з ефективністю, які стали б актуальними навіть у звичайних масштабних програмах?
Томаш Зато - Відновити Моніку

1
@ TomášZato Це буде повільніше, ніж властивості даних у звичайного об'єкта, але я не думаю, що це буде набагато гірше, ніж властивості аксесуара.
Оріол

TIL:multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
криваві ручки

4
Я б подумав замінити "Багатократне успадкування" на "Багатократне делегування", щоб краще зрозуміти, що відбувається. Основна концепція вашої реалізації полягає в тому, що проксі-сервер фактично вибирає правильний об'єкт для делегування (або пересилання) повідомлення. Сила вашого рішення полягає в тому, що ви можете динамічно розширювати цільовий прототип / с. Інші відповіді використовують конкатенацію (ала Object.assign) або отримання зовсім іншого графіка, врешті-решт всі вони отримують єдиний прототип ланцюга між об'єктами. Проксі-рішення пропонує розгалуження часу виконання, і це скеля!
sminutoli

Щодо продуктивності, якщо ви створите об’єкт, який успадковує від декількох об'єктів, які успадковують від декількох об'єктів тощо, він стане експоненціальним. Так що так, буде повільніше. Але в нормальних випадках я не думаю, що це буде так погано.
Оріол

16

Оновлення (2019): Оригінальна публікація стає досить застарілою. Ця стаття (зараз посилання на Інтернет-архів, оскільки домен відійшла) та пов'язана з нею бібліотека GitHub - хороший сучасний підхід.

Оригінальний пост: Множинне успадкування [редагувати, не належне успадкування типу, а властивості; mixins] у Javascript досить простий, якщо ви використовуєте побудовані прототипи, а не загальнооб'єктні. Ось два батьківські класи, які слід успадкувати від:

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

Зауважте, що я використовував один і той самий член "імені" у кожному випадку, що може бути проблемою, якщо батьки не погодились би з тим, як слід використовувати "ім'я". Але вони в цьому випадку сумісні (надлишкові).

Тепер нам просто потрібен клас, який успадковується від обох. Спадкування здійснюється за допомогою виклику функції конструктора (без використання нового ключового слова) для прототипів та конструкторів об'єктів. По-перше, прототип повинен успадкувати від батьківських прототипів

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

І конструктор повинен успадкувати від батьківських конструкторів:

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

Тепер ви можете вирощувати, їсти та збирати різні екземпляри:

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

Ну, я можу, Array.call(...)але це, здається, не впливає на те, що я проходжу this.
Томаш Зато - Відновити Моніку

@ TomášZato Ви могли б зробитиArray.prototype.constructor.call()
Roy J

1
@AbhishekGupta Дякую за те, що повідомили мені. Я замінив посилання на посилання на архівну веб-сторінку.
Roy J

7

Цей використовується Object.createдля створення справжнього прототипу:

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

Наприклад:

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

повернеться:

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

так що obj.a === 1, obj.b === 3і т.д.


Лише швидке гіпотетичне запитання: я хотів скласти векторний клас шляхом змішування прототипів Number та Array (для задоволення). Це дасть мені як індекси масиву, так і математичні оператори. Але це спрацювало б?
Томаш Зато - Відновити Моніку

@ TomášZato, варто перевірити цю статтю , якщо ви переглядаєте масиви підкласифікації; це може врятувати вам головний біль. Щасти!
користувач3276552

5

Мені подобається реалізація структури класу Джона Ресіга: http://ejohn.org/blog/simple-javascript-inheritance/

Це можна просто поширити на щось на кшталт:

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

    // same code
}

що дозволить вам передавати в декілька об'єктів, які слід успадковувати. Тут ви втратите instanceOfможливості, але це дається, якщо ви хочете багаторазово успадкувати.


мій доволі суперечливий приклад вищезазначеного доступний на веб- сайті https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js

Зауважте, що у цьому файлі є якийсь мертвий код, але він дозволяє кілька разів успадкувати, якщо ви хочете подивитися.


Якщо ви хочете сковувати спадщину (НЕ багаторазове успадкування, але для більшості людей це одне і те ж), це можна досягти за допомогою класу типу:

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

який збереже оригінальний прототип ланцюга, але ви також матимете безліч безглуздих кодів.


7
Це створює злитий неглибокий клон. Додавання нового властивості до "успадкованих" об'єктів не призведе до появи нового властивості на похідному об'єкті, як це було б у справжньому успадкуванні прототипу.
Даніель Ервікер

@DanielEarwicker - Правда, але якщо ви хочете "багаторазового успадкування", що один клас походить з двох класів, альтернативи насправді не існує. Модифікована відповідь, що відображає, що просто зв’язувати заняття разом - це одне і те ж у більшості випадків.
Марк Кан

Здається, ваш GitHUb зник, у вас все ще є github.com/cwolves/Fetch/blob/master/support/plugins/klass/… Я не заперечував би на це, якщо ви хочете поділитися?
JasonDavis

4

Не плутайте з реалізацією рамки JavaScript багаторазового успадкування.

Все, що вам потрібно зробити, - це використовувати Object.create () для створення нового об'єкта кожен раз із зазначеним прототипом і властивостями, а потім обов'язково змінюйте Object.prototype.constructor кожен крок, якщо ви плануєте інстанціювати Bв майбутнє

Для успадкування властивостей екземпляра thisAі thisBми використовуємо Function.prototype.call () в кінці кожної функції об'єкта. Це необов’язково, якщо ви дбаєте лише про успадкування прототипу.

Запустіть такий код десь і спостерігайте 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 успадковує прототип від A
  • C успадковує прототип від B
  • objC є екземпляром C

Це добре пояснення наведених вище кроків:

OOP В JavaScript: що вам потрібно знати


Невже це не копіює всі властивості в новий об’єкт? Отже, якщо у вас є два прототипи, A і B, і ви відтворили їх обидва на C, зміна властивості A не вплине на цю властивість на C і візу навпаки. Ви отримаєте копію всіх властивостей A і B, що зберігаються в пам'яті. Це було б такою ж ефективністю, як якщо б ви жорстко закодували всі властивості A і B на C. Це приємно для читабельності, а пошук властивості не повинен подорожувати до батьківських об'єктів, але це насправді не спадкування - більше схоже на клонування. Зміна власності на A не змінює клоновану власність на C.
Френк

2

Я жодним чином не експерт з JavaScript OOP, але якщо я правильно вас зрозумів, вам потрібно щось на зразок (псевдо-код):

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

Cat inherit from (Earth, Animal);

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

У такому випадку я б спробував щось на кшталт:

var Earth = function(){};
Earth.prototype.shape = 'round';

var Animal = function(){};
Animal.prototype.shape = 'random';
Animal.prototype.head = true;

var Cat = function(){};

MultiInherit(Cat, Earth, Animal);

console.log(new Cat().shape); // yields "round", since I reversed the inheritance order
console.log(new Cat().head); // true

function MultiInherit() {
    var c = [].shift.call(arguments),
        len = arguments.length
    while(len--) {
        $.extend(c.prototype, new arguments[len]());
    }
}

1
Це не просто підбір першого прототипу та ігнорування решти? Встановлення c.prototypeдекількох разів не дає декількох прототипів. Наприклад, якщо б у вас було Animal.isAlive = true, Cat.isAliveвсе одно не було б визначено.
devios1

Так, я мав на увазі змішати прототипи, виправлені ... (тут я використовував розширення jQuery, але ви отримаєте картинку)
Девід Хеллінг

2

Можна реалізувати багатократне успадкування в JavaScript, хоча це дуже мало бібліотек.

Я міг би вказати на Ring.js , єдиний мені відомий приклад.


2

Я над цим багато працював сьогодні і намагався цього досягти в ES6. Як я це робив, використовував Browrify, Babel, а потім я протестував це з Wallaby, і, здавалося, це працює. Моя мета - розширити поточний масив, включити ES6, ES7 та додати деякі додаткові спеціальні функції, потрібні мені в прототипі для роботи зі звуковими даними.

Уоллабі проходить 4 мої тести. Файл example.js можна вставити в консоль, і ви можете бачити, що властивість 'включає' є в прототипі класу. Я все ж хочу завтра перевірити це ще більше.

Ось мій метод: (Я, швидше за все, буду рефактор і перепаковувати як модуль після сну!)

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


2

Я думаю, що це смішно просто. Тут питання полягає в тому, що дочірній клас стосуватиметься лише instanceofпершого класу, до якого ви телефонуєте

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

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

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

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

  return this;
}

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

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

var cool = new Cool();

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

1

Перевірте код, нижче якого IS відображає підтримку багаторазового успадкування. Зроблено за допомогою ПРОТОТИПНОГО НАСЛІДЖЕННЯ

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

    this.name = name;
}

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

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

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

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

const a = new A("shivang");
const b = new B(32);
console.log(a.name);
console.log(b.age);
const ab = new AB("indu", 27);
console.log(ab.toString());

1

У мене є ціла функція, що дозволяє визначати класи з множинним успадкуванням. Він дозволяє ввести наступний код. Загалом ви відзначите повний відхід від рідних методів класифікації у 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ініціалізується екземпляр . Але є три мінуси, третій з яких є дуже критичним :

  1. Цей код став менш читабельним та ретельним. За util.invokeNoDuplicatesфункцією ховається чимало складностей , а думка про те, як цей стиль уникає багатовикликання, не інтуїтивно зрозуміла і не викликає головного болю. У нас також є той прискіпливий dupsпараметр, який дійсно потрібно визначати для кожної функції в класі . Ой.
  2. Цей код повільніше - для досягнення бажаних результатів при багатократному успадкуванні потрібно трохи більше опосередкованості та обчислень. На жаль, це, мабуть, стосується будь-якого рішення нашої проблеми з кількома викликами.
  3. Найбільш суттєво, структура функцій, які покладаються на спадкування, стала дуже жорсткою . Якщо підклас 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впроваджена мною програма все ще корисна, навіть якщо вона не є досконалою. Я також сподіваюся, що хтось, хто цікавиться цією темою, отримав більше контексту, щоб пам’ятати, коли вони читають далі!


0

Погляньте на пакунок IeUnit .

Концепція засвоєння, реалізована в IeUnit, здається, пропонує те, що ви шукаєте, досить динамічно.


0

Ось приклад ланцюга прототипу з використанням конструкторських функцій :

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

Ця концепція використовує визначення Єгуди Кац "класу" для JavaScript:

... JavaScript "клас" - це лише об'єкт Функції, який виконує функції конструктора плюс доданий об'єкт-прототип. ( Джерело: Гуру Кац )

На відміну від підходу Object.create , коли класи будуються таким чином, і ми хочемо створити екземпляри "класу", нам не потрібно знати, від чого успадковується кожен "клас". Ми просто використовуємо 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

Порядок черговості повинен мати сенс. Спочатку він виглядає в об'єкті екземпляра, потім - прототип, потім наступний прототип тощо.

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

Ми також можемо модифікувати прототипи, які впливатимуть на всі об'єкти, побудовані в класі.

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

Я спочатку написав щось із цього у відповідь .


2
ОП запитує кілька прототипів ланцюгів (наприклад, childуспадковує від parent1та parent2). Ваш приклад говорить лише про один ланцюг.
найсильніший

0

Запізнення на сцені - SimpleDeclare . Однак, маючи справу з багаторазовим успадкуванням, ви все одно закінчите копії оригінальних конструкторів. Це необхідність у Javascript ...

Merc.


Це необхідність у Javascript ... до ES6 проксі.
Джонатан

Проксі є цікавими! Я обов'язково розглядаю зміну SimpleDeclare, щоб не потрібно було копіювати методи через проксі, як тільки вони стануть частиною стандарту. Код SimpleDeclare насправді дуже легко читати та змінювати ...
Merc

0

Я б користувався ds.oop . Його схоже на prototype.js та інші. робить багаторазове успадкування дуже легким і його мінімалістичним. (лише 2 або 3 кб) Також підтримує деякі інші акуратні функції, такі як інтерфейси та введення залежності

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

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

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

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

var person = new Person();

person.run();
person.walk();
person.eat();

0

Як щодо цього, він реалізує багатократне успадкування в 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();

А ось код для функції утиліти specialize_with ():

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

Це справжній код, який працює. Ви можете скопіювати та вставити його у файл html та спробувати самостійно. Це справді працює.

Це зусилля для впровадження MI в JavaScript. Не так багато коду, більше ноу-хау.

Будь ласка, перегляньте мою повну статтю з цього приводу, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS


0

Я щойно призначав, які класи мені потрібні у властивостях інших, і додавати проксі для автоматичного вказування на них, мені подобається:

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.