JavaScript 中的多重继承/原型

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

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

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

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

思潮?


答案 1

在 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

我使用这些陷阱:

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

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

  • 可以添加 getPrototypeOf 陷阱,但没有正确的方法来返回多个原型。这意味着也行不通。因此,我让它获取目标的原型,该原型最初是空的。instanceof
  • 可以添加 setPrototypeOf 陷阱并接受一个对象数组,这将替换原型。这是留给读者的练习。在这里,我只是让它修改目标的原型,这并没有多大用处,因为没有陷阱使用目标。
  • 删除属性陷阱是用于删除自己的属性的陷阱。代理表示继承,因此这没有多大意义。我让它尝试在目标上删除,无论如何都应该没有属性。
  • isExtensible 陷阱是用于获取可扩展性的陷阱。没有多大用处,因为不变性迫使它返回与目标相同的可扩展性。因此,我只是让它将操作重定向到目标,这将是可扩展的。
  • 应用构造陷阱是用于调用或实例化的陷阱。仅当目标是函数或构造函数时,它们才有用。

// 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"

答案 2

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