JavaScript中的多个继承/原型


132

我已经到了需要在JavaScript中进行一些基本的多重继承的地步。(我不是在这里讨论这是否是一个好主意,所以请您保留那些评论。)

我只想知道是否有人尝试过成功(或没有成功),以及他们是如何做到的。

归根结底,我真正需要的是能够拥有一个对象,该对象能够从多个原型中继承属性(即,每个原型可以具有自己的适当链),但是要具有给定的优先顺序(它将搜索链以获取第一个定义)。

为了说明从理论上讲这是怎么可能的,可以通过将辅助链附加到主链的末端来实现,但这会影响那些以前的原型的所有实例,这不是我想要的。

有什么想法吗?


1
我认为dojo声明还可以处理多个继承src,我也觉得mootools也可以,很多都超出了我的范围,但我会像dojo所建议的那样快速阅读一下
TI

看一下TraitsJS(链接1链接2),它是替代多重继承和混合的好方法...
CMS

1
@Pointy,因为它不是很动态。我希望能够对任何一个父链进行更改。不管怎么说,如果不可能的话,我可能不得不求助于此。
devios1 2012年


1
有关此内容的有趣读物:webreflection.blogspot.co.uk/2009/06/…–
大雄市

Answers:


49

通过使用Proxy对象,可以在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用来查找包含该属性的第一个原型,然后返回值,或在适当的接收器上调用getter。由处理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。修改描述符以使其可配置,否则我们可能会破坏一些代理不变式。
  • preventExtensionsdefineProperty陷阱只包括防止修改代理目标这些操作。否则,我们最终可能会破坏一些代理不变式。

有更多可用的陷阱,我不使用

  • getPrototypeOf陷阱可以添加,但返回多个原型不正确的方法。这意味着instanceof两者都不起作用。因此,我让它获得目标的原型,该原型最初为null。
  • 所述setPrototypeOf陷阱可以添加并接受对象的数组,其将取代原型。留给读者练习。在这里,我只是让它修改目标的原型,这没有多大用处,因为没有陷阱使用目标。
  • deleteProperty陷阱是删除自己的属性陷阱。代理代表继承,所以这没有多大意义。我让它尝试在目标上删除,但该目标应该没有任何属性。
  • isExtensible陷阱是获得可扩展性陷阱。鉴于不变式会强制其返回与目标相同的可扩展性,因此并没有太大用处。因此,我只是让它将操作重定向到目标,这将是可扩展的。
  • applyconstruct陷阱陷阱调用或实例。仅当目标是函数或构造函数时,它们才有用。

// 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
即使在正常规模的应用程序上,也没有一些性能问题会变得很重要吗?
托马什Zato -恢复莫妮卡

1
@TomášZato它会比普通对象中的数据属性慢,但我认为它不会比访问器属性差很多。
Oriol 2015年

multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
直到

4
我会考虑用“多重授权”代替“多重继承”,以更好地了解发生了什么。实现中的关键概念是代理实际上是选择正确的对象来委派 (或转发)消息。解决方案的强大功能在于您可以动态扩展目标原型。其他答案是使用级联(ala Object.assign)或获得完全不同的图,最后,所有答案都得到了对象之间的唯一原型链。代理解决方案提供了运行时分支,这真是麻烦!
sminutoli

关于性能,如果您创建一个从多个对象继承的对象,一个从多个对象继承的对象,依此类推,那么它将变成指数。所以是的,它将变慢。但是在正常情况下,我认为这不会那么糟。
Oriol

16

更新(2019):原始帖子已经过时了。本文(自从域消失以来,现在是Internet存档链接)及其关联的GitHub库是一种很好的现代方法。

原帖: 多重继承[编辑,不是类型的适当继承,而是属性的继承;如果您使用构造的原型而不是通用对象的原型,那么Javascript中的[mixins]非常简单。这里是两个继承自的父类:

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

请注意,在每种情况下,我都使用相同的“姓名”成员,如果父母不同意应如何使用“姓名”,这可能是一个问题。但是在这种情况下,它们是兼容的(实际上是冗余的)。

现在我们只需要一个从两者继承的类。继承是通过调用原型和对象构造函数的构造函数(不使用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();

您可以使用内置的原型来做到这一点吗?(阵列,字符串,数字)
托马什Zato -恢复莫妮卡

我认为内置原型没有可调用的构造函数。
罗伊J

好吧,我可以,Array.call(...)但是似乎并不会影响我通过的任何东西this
托马什Zato -恢复莫妮卡

@TomášZato你可以做Array.prototype.constructor.call()
Roy J

1
@AbhishekGupta感谢您让我知道。我已将链接替换为指向已存档网页的链接。
罗伊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 === 1obj.b === 3等等。


只是一个快速的假设问题:我想通过混合Number和Array原型来制作Vector类(出于娱乐目的)。这会给我数组索引和数学运算符。但这行得通吗?
托马什Zato -恢复莫妮卡

@TomášZato,如果您正在研究数组的子类化,那么值得阅读这篇文章。它可以减轻您的头痛。祝好运!
user3276552 2015年

5

我喜欢John Resig对类结构的实现: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中找到

请注意,该文件中包含一些无效代码,但是如果您想看一看,它允许多重继承。


如果您想要链式继承(不是多重继承,但对于大多数人来说是同一件事),则可以使用Class来实现,例如:

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

这将保留原始的原型链,但是您还将运行大量毫无意义的代码。


7
这将创建合并的浅克隆。向“继承的”对象中添加新属性不会像在真正的原型继承中那样使新属性出现在派生对象上。
Daniel Earwicker 2012年

@DanielEarwicker-是的,但是如果您希望“多重继承”中的一个类派生自两个类,则实际上没有其他选择。修改后的答案反映出在大多数情况下,将类简单地链接在一起是相同的。
马克·卡恩

您的GitHUb似乎不见了,您是否还有github.com/cwolves/Fetch/blob/master/support/plugins/klass / ...如果您愿意分享,我不介意查看它吗?
JasonDavis

4

不要与多重继承的JavaScript框架实现混淆。

您需要做的就是每次使用Object.create()创建具有指定原型对象和属性的新对象,然后如果打算在实例化实例中实例化,请确保在此方法的每一步都更改Object.prototype.constructorB。未来。

为了继承实例属性thisAthisB我们在每个对象函数的末尾使用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中的OOP:您需要了解的内容


但是,这不是将所有属性复制到新对象中吗?因此,如果您有两个原型A和B,并且都在C上重新创建它们,则更改A的属性不会影响C上的属性,反之亦然。您最终将获得存储在内存中的A和B中所有属性的副本。就像您将A和B的所有属性硬编码到C中一样,它的性能也一样。这对于可读性很好,并且属性查找不必传递给父对象,但它并不是真正的继承-更像是克隆。更改A上的属性不会更改C上的克隆属性
Frank

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 = trueCat.isAlive仍将是未定义的。
devios1'2

是的,我的意思是混合使用原型,并进行更正...(我在这里使用jQuery的扩展,但您会看到图片)
David Hellsing 2012年

2

尽管很少有库可以执行JavaScript,但可以在JavaScript中实现多重继承。

我可以指出Ring.js,这是我知道的唯一示例。


2

我今天在这方面做了很多工作,并试图在ES6中自己实现。我这样做的方法是使用Browserify,Babel,然后我用Wallaby对其进行了测试,它似乎可以工作。我的目标是扩展当前的Array,包括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

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类来模拟Dragon的“类型”,在其中,可用Dragon类型的列表存储在Class本身而不是实例上:

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 将导致对构造函数的两次调用RunningFlyingNamed

这是因为继承图如下所示:

 (^^ More Specialized ^^)

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

  (vv More Abstract vv)

子类的继承图中存在指向同一父类的多个路径时,子类的实例化将多次调用该父类的构造函数。

与之抗争并非易事。让我们看一些使用简化的类名的示例。我们将考虑class A,最抽象的父类,class BC,它们都从继承A,以及BCB和继承的类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,这是一个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继承了CoolClassGoodClass,并且这两个父类都提供niftyFunction了它们自己的定义,NiftyClass.prototype.niftyFunction则决不能(不会冒着多次调用的危险)进行以下操作:
    • A.首先运行专用逻辑NiftyClass然后运行父类的专用逻辑
    • B. 所有专用父逻辑都完成之后的NiftyClass任何时间运行专用逻辑
    • C.有条件地取决于其父级的特殊逻辑的返回值
    • D.避免niftyFunction完全运行特定父母的专门知识

当然,我们可以通过在util以下代码中定义专用功能来解决上述每个字母问题:

  • A.定义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

这个概念使用了Yehuda Katz 对JavaScript “类”的定义:

... JavaScript“类”只是一个Function对象,它充当构造函数以及附加的原型对象。(资料来源:Guru Katz

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正在请求多个原型链(例如,childparent1和继承parent2)。您的示例仅讨论一个链。
2015年

0

后来者是SimpleDeclare。但是,在处理多重继承时,您仍然会得到原始构造函数的副本。这是Javascript中的必要条件...

Merc。


在Javascript中这是必需的...直到ES6 Proxies。
乔纳森(Jonathon)

代理很有趣!我一定会研究更改SimpleDeclare,以便一旦成为标准的一部分,就不需要使用代理来复制方法。SimpleDeclare的代码非常非常易于阅读和更改...
Merc

0

我会使用ds.oop。它类似于prototype.js等。使多重继承变得非常容易且极简。(仅2或3 kb)还支持其他一些简洁功能,例如接口和依赖项注入

/*** 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文件中,然后自己尝试。确实有效。

这就是在JavaScript中实现MI的努力。没有太多的代码,更多的是专有技术。

请随时查看我关于此的完整文章,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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.