JavaScript 中的多重继承/原型

IT技术 javascript prototype multiple-inheritance
2021-01-20 07:25:14

我已经到了需要在 JavaScript 中进行某种基本多重继承的地步。(我不是来讨论这是否是一个好主意,所以请把这些评论留给自己。)

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

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

为了证明这在理论上是如何可能的,可以通过将辅助链附加到主链的末端来实现,但这会影响任何先前原型的所有实例,这不是我想要的。

想法?

6个回答

在 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用来查找包含该属性的第一个原型,然后返回该值,或者在适当的接收器上调用 getter。这由 处理Reflect.get如果没有原型包含该属性,则返回undefined.
  • 所述set陷阱是设置属性值陷阱。find用来查找包含该属性的第一个原型,并在适当的接收器上调用它的 setter。如果没有 setter 或没有原型包含该属性,则在适当的接收器上定义该值。这由 处理Reflect.set
  • enumerate陷阱是陷阱for...in循环我从第一个原型迭代可枚举属性,然后从第二个原型迭代,依此类推。一旦一个属性被迭代,我将它存储在一个哈希表中以避免再次迭代它。
    警告:此陷阱已在 ES7 草案中删除,并且在浏览器中已弃用。
  • 所述ownKeys陷阱为陷阱Object.getOwnPropertyNames()从 ES7 开始,for...in循环不断调用 [[GetPrototypeOf]] 并获取每个循环的属性。所以为了让它迭代所有原型的属性,我使用这个陷阱让所有可枚举的继承属性看起来像自己的属性。
  • 所述getOwnPropertyDescriptor陷阱为陷阱Object.getOwnPropertyDescriptor()使所有可枚举属性在ownKeys陷阱中看起来像自己的属性是不够的,for...in循环将获取描述符以检查它们是否可枚举。因此,我使用find查找包含该属性的第一个原型,并迭代其原型链,直到找到属性所有者,然后返回其描述符。如果没有原型包含该属性,则返回undefined. 修改描述符以使其可配置,否则我们可能会破坏一些代理不变量。
  • preventExtensionsdefineProperty陷阱只包括防止修改代理目标这些操作。否则我们最终可能会破坏一些代理不变量。

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

  • getPrototypeOf陷阱可以添加,但返回多个原型不正确的方法。这意味着instanceof也不会起作用。因此,我让它获取目标的原型,该原型最初为空。
  • 所述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"
我会考虑用“多重委托”替换“多重继承”,以更好地了解正在发生的事情。实现中的关键概念是代理实际上选择了正确的对象来委托 (或转发)消息。您的解决方案的强大之处在于您可以动态扩展目标原型。其他答案是使用串联 (ala Object.assign) 或获得完全不同的图,最终所有这些都在对象之间获得一对一的原型链。代理解决方案提供了运行时分支,这很震撼!
2021-03-20 07:25:14
直到: multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
2021-04-01 07:25:14
@TomášZato 它会比普通对象中的数据属性慢,但我认为它不会比访问器属性差多少。
2021-04-03 07:25:14
关于性能,如果你创建一个对象,它从多个对象继承,从多个对象继承,依此类推,那么它会变得指数级。所以是的,它会更慢。但在正常情况下,我认为不会那么糟糕。
2021-04-03 07:25:14
即使在正常规模的应用程序中,是否也存在一些相关的性能问题?
2021-04-09 07:25:14

更新(2019 年):原帖已经过时了。这篇文章(现在是互联网存档链接,因为域名消失了)及其相关的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();
@AbhishekGupta 谢谢你让我知道。我已将链接替换为指向存档网页的链接。
2021-03-15 07:25:14
是的,这就是为什么我在文本中“没有正确的类型继承,而是属性的继承;mixin”。
2021-03-23 07:25:14
我认为内置原型没有您可以调用的构造函数。
2021-03-30 07:25:14
你能用内置原型做到这一点吗?(数组、字符串、数字)
2021-04-01 07:25:14
这不是像使用Object.assign(target, source)吗?如果您以这种方式“继承”任何东西,您将获得所有属性,但它们基本上会被复制到新原型中。对原始原型所做的任何更改都不会反映在新对象中,因此它不是真正的继承,只是从多个对象复制属性。我一直为此使用 Object.assign。它有效,但从技术上讲,它是复制,而不是真正的继承,所有子项都指向同一个父对象。他们指向自己的属性。
2021-04-07 07:25:14

这个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 原型来制作 Vector 类(为了好玩)。这将为我提供数组索引和数学运算符。但它会起作用吗?
2021-03-23 07:25:14
@TomášZato,如果您正在研究子类化数组,那么值得查看这篇文章它可以为您节省一些头痛。祝你好运!
2021-03-25 07:25:14

我喜欢 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 )

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

这会创建一个合并的浅克隆。向“继承的”对象添加新属性不会导致新属性出现在派生对象上,就像在真正的原型继承中一样。
2021-03-22 07:25:14
看来你的 GitHUb 不见了,你还有github.com/cwolves/Fetch/blob/master/support/plugins/klass/...如果你愿意分享,我不介意看看它?
2021-03-25 07:25:14
@DanielEarwicker——没错,但如果你想在一个类派生自两个类中实现“多重继承”,那么真的没有其他选择。修改答案以反映在大多数情况下简单地将类链接在一起是同一件事。
2021-03-31 07:25:14

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

所有你需要做的是使用的Object.create()每次创建新对象与指定的原型对象和属性,然后一定要改变Object.prototype.constructor的每一步,如果你打算在实例B中未来。

为了继承实例属性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 上的克隆属性。
2021-03-21 07:25:14
这不是 OP 所要求的。这只是单继承。如果你var objB = new B()会发现objB instanceof A是真的。如果 OP 想要混合到类型中的类型TankWeaponand Vehicle,则在您的解决方案中 aWeapon必须是 a Vehicle,反之亦然。这两者都没有意义。
2021-03-28 07:25:14