Множественное наследование / прототипы в JavaScript

132

Я подошел к моменту, когда мне нужно иметь какое-то рудиментарное множественное наследование в JavaScript. (Я здесь не для того, чтобы обсуждать, хорошая это идея или нет, поэтому, пожалуйста, оставьте эти комментарии при себе.)

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

Вкратце, мне действительно нужно иметь объект, способный наследовать свойство от более чем одной цепочки прототипов (т.е. каждый прототип может иметь свою собственную цепочку), но в заданном порядке приоритета (он будет поиск по цепочкам для первого определения).

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

Мысли?

devios1
источник
1
Я думаю, что dojo declare обрабатывает множественное наследование src, также у меня есть ощущение, что mootools тоже, многое из этого вне меня, но я собираюсь быстро прочитать это, как предлагает додзё
TI
Взгляните на TraitsJS ( ссылка 1 , ссылка 2 ), это действительно хорошая альтернатива множественному наследованию и миксинам ...
CMS
1
@Pointy, потому что это не очень динамично. Я хотел бы иметь возможность получать изменения, внесенные в любую родительскую цепочку, по мере их появления. Тем не менее, мне, возможно, придется прибегнуть к этому, если это просто невозможно.
devios1 06
1
Интересное прочтение об этом: webreflection.blogspot.co.uk/2009/06/…
Нобита

Ответы:

49

Множественное наследование может быть достигнуто в ECMAScript 6 с помощью прокси-объектов .

Реализация

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 Это будет медленнее, чем свойства данных в обычном объекте, но я не думаю, что это будет намного хуже, чем свойства доступа.
Oriol
TIL:multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
bloodyKnuckles
4
Я бы подумал о замене «Множественное наследование» на «Множественное делегирование», чтобы лучше понять, что происходит. Ключевой концепцией в вашей реализации является то, что прокси-сервер фактически выбирает правильный объект для делегирования (или пересылки) сообщения. Сила вашего решения заключается в том, что вы можете динамически расширять целевой прототип / ы. Другие ответы используют конкатенацию (ала Object.assign) или получают совершенно другой график, в конце концов все они получают единственную цепочку прототипов между объектами. Прокси-решение предлагает ветвление во время выполнения, и это здорово!
sminutoli
Что касается производительности, если вы создаете объект, который наследуется от нескольких объектов, который наследуется от нескольких объектов и т. Д., Тогда он станет экспоненциальным. Так что да, это будет медленнее. Но в обычных случаях не думаю, что все будет так плохо.
Oriol
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();

Обратите внимание, что я использовал один и тот же элемент «name» в каждом случае, что могло быть проблемой, если бы родители не пришли к соглашению о том, как следует обращаться с «name». Но в этом случае они совместимы (на самом деле избыточны).

Теперь нам просто нужен класс, наследующий от обоих. Наследование осуществляется вызова функции конструктора (без использования ключевого слова new) для прототипов и конструкторов объектов. Во-первых, прототип должен унаследовать от родительских прототипов.

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()
Рой Дж
1
@AbhishekGupta Спасибо, что сообщили мне. Я заменил ссылку ссылкой на заархивированную веб-страницу.
Рой Джей
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и т. д.

pimvdb
источник
Небольшой гипотетический вопрос: я хотел создать класс Vector, смешав прототипы Number и Array (для развлечения). Это даст мне как индексы массива, так и математические операторы. Но сработает ли это?
Томаш Зато - Восстановите Монику
@ TomášZato, стоит почитать эту статью , если вы изучаете подклассы массивов; это может избавить вас от головной боли. удачи!
user3276552 06
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

Это хорошее объяснение вышеперечисленных шагов:

ООП в JavaScript: что нужно знать

Дейв
источник
Но разве это не копирует все свойства в новый объект? Итак, если у вас есть два прототипа, A и B, и вы воссоздаете их оба на C, изменение свойства A не повлияет на это свойство на C и наоборот. В результате вы получите копию всех свойств в A и B, хранящихся в памяти. Это будет такая же производительность, как если бы вы жестко закодировали все свойства A и B в C. Это удобно для чтения, и поиск свойств не должен перемещаться к родительским объектам, но на самом деле это не наследование - больше похоже на клонирование. Изменение свойства на A не изменяет клонированное свойство на C.
Фрэнк
2

Я никоим образом не эксперт по ООП javascript, но, если я правильно вас понял, вам нужно что-то вроде (псевдокода):

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 06
Да, я хотел смешать прототипы, поправил ... (здесь я использовал расширение jQuery, но вы понимаете)
Дэвид Хеллсинг
2

В JavaScript можно реализовать множественное наследование, хотя очень немногие библиотеки это делают.

Я мог бы указать Ring.js , единственный известный мне пример.

Николаса ван
источник
2

Сегодня я много работал над этим и сам пытался добиться этого в ES6. Я сделал это с помощью Browserify, Babel, а затем я протестировал его с Wallaby, и, похоже, он работал. Моя цель - расширить текущий массив, включить ES6, ES7 и добавить некоторые дополнительные настраиваемые функции, которые мне нужны в прототипе для работы с аудиоданными.

Валлаби проходит 4 моих теста. Файл example.js можно вставить в консоль, и вы увидите, что свойство includes находится в прототипе класса. Я все еще хочу проверить это завтра.

Вот мой метод: (Я, скорее всего, проведу рефакторинг и перепакую как модуль после некоторого сна!)

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: 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
BarryBones41
источник
1

Проверьте код ниже, который показывает поддержку множественного наследования. Сделано с использованием ПРОТОТИПНОГО НАСЛЕДОВАНИЯ

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. Мы также должны знать, что каждая функция, определенная в любом классе, теперь может получать параметр после всех своих других с именем named dup, который 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никогда (без риска многократного вызова) не смогут:
    • A. Запустите NiftyClassсначала специализированную логику , затем специализированную логику родительских классов
    • Б. Запускать специализированную логику NiftyClassв любой момент, кроме как после завершения всей специализированной родительской логики.
    • C. Вести себя условно в зависимости от возвращаемых значений специализированной логики его родителя
    • D. Избегайте работы конкретного родителя специализировалась в niftyFunctionцелом

Конечно, мы могли бы решить каждую указанную выше проблему, указав специализированные функции в util:

  • А. определитьutil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. define util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(где parentNameимя родителя, чья специализированная логика будет сразу следовать специализированной логике дочерних классов)
  • C. define util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(в этом случае testFnбудет получен результат специализированной логики для указанного родительского объекта parentNameи будет возвращено true/falseзначение, указывающее, должно ли произойти короткое замыкание)
  • D. define 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 - это просто объект Function, который служит конструктором, плюс присоединенный объект-прототип. ( Источник: Гуру Кац )

В отличие от подхода 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
OP запрашивает несколько цепочек прототипов (например, childнаследуется от parent1и parent2). Ваш пример говорит только об одной цепочке.
Posest 02
0

Опоздавшим на сцену является SimpleDeclare . Однако, имея дело с множественным наследованием, вы все равно получите копии исходных конструкторов. Это необходимость в Javascript ...

Merc.

Merc
источник
Это необходимо в Javascript ... до ES6 Proxies.
Джонатон
Прокси интересные! Я обязательно рассмотрю возможность изменения 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
shamaseen
источник