Multiple inheritance/prototypes in JavaScript

I've come to a point where I need to have some sort of rudimentary multiple inheritance happening in JavaScript. (I'm not here to discuss whether this is a good idea or not, so please kindly keep those comments to yourself.)

I just want to know if anyone's attempted this with any (or not) success, and how they went about it.

To boil it down, what I really need is to be able to have an object capable of inheriting a property from more than one prototype chain (ie each prototype could have its own proper chain), but in a given order of precedence (it will search the chains in order for the first definition).

To demonstrate how this is theoretically possible, it could be achieved by attaching the secondary chain onto the end of the primary chain, but this would affect all instances of any of those previous prototypes and that's not what I want.

Thoughts?

Edit Appreciate the responses folks, but while the consensus seems to be statically copying over the properties from both trees, which would work in most cases (and will probably be what I end up doing), I was most interested in a dynamic solution that would allow the separate prototype chains to be altered, and still have those changes "picked up" by the instance.


Mixins可以用于JavaScript,以实现您可能想通过多重继承来解决的相同目标。


Multiple inheritance can be achieved in ECMAScript 6 by using Proxy objects.

Implementation

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

Explanation

A proxy object consists of a target object and some traps, which define custom behavior for fundamental operations.

When creating an object which inherits from another one, we use Object.create(obj) . But in this case we want multiple inheritance, so instead of obj I use a proxy that will redirect fundamental operations to the appropriate object.

I use these traps:

  • The has trap is a trap for the in operator. I use some to check if at least one prototype contains the property.
  • The get trap is a trap for getting property values. I use find to find the first prototype which contains that property, and I return the value, or call the getter on the appropriate receiver. This is handled by Reflect.get . If no prototype contains the property, I return undefined .
  • The set trap is a trap for setting property values. I use find to find the first prototype which contains that property, and I call its setter on the appropriate receiver. If there is no setter or no prototype contains the property, the value is defined on the appropriate receiver. This is handled by Reflect.set .
  • The enumerate trap is a trap for for...in loops. I iterate the enumerable properties from the first prototype, then from the second, and so on. Once a property has been iterated, I store it in a hash table to avoid iterating it again.
    Warning : This trap has been removed in ES7 draft and is deprecated in browsers.
  • The ownKeys trap is a trap for Object.getOwnPropertyNames() . Since ES7, for...in loops keep calling [[GetPrototypeOf]] and getting the own properties of each one. So in order to make it iterate the properties of all prototypes, I use this trap to make all enumerable inherited properties appear like own properties.
  • The getOwnPropertyDescriptor trap is a trap for Object.getOwnPropertyDescriptor() . Making all enumerable properties appear like own properties in the ownKeys trap is not enough, for...in loops will get the descriptor to check if they are enumerable. So I use find to find the first prototype which contains that property, and I iterate its prototypical chain until I find the property owner, and I return its descriptor. If no prototype contains the property, I return undefined . The descriptor is modified to make it configurable, otherwise we could break some proxy invariants.
  • The preventExtensions and defineProperty traps are only included to prevent these operations from modifying the proxy target. Otherwise we could end up breaking some proxy invariants.
  • There are more traps available, which I don't use

  • The getPrototypeOf trap could be added, but there is no proper way to return the multiple prototypes. This implies instanceof won't work neither. Therefore, I let it get the prototype of the target, which initially is null.
  • The setPrototypeOf trap could be added and accept an array of objects, which would replace the prototypes. This is left as an exercice for the reader. Here I just let it modify the prototype of the target, which is not much useful because no trap uses the target.
  • The deleteProperty trap is a trap for deleting own properties. The proxy represents the inheritance, so this wouldn't make much sense. I let it attempt the deletion on the target, which should have no property anyway.
  • The isExtensible trap is a trap for getting the extensibility. Not much useful, given that an invariant forces it to return the same extensibility as the target. So I just let it redirect the operation to the target, which will be extensible.
  • The apply and construct traps are traps for calling or instantiating. They are only useful when the target is a function or a constructor.
  • Example

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

    Multiple inheritance [edit, not proper inheritance of type, but of properties; mixins] in Javascript is pretty straightforward if you use constructed prototypes rather than generic-object ones. Here are two parent classes to inherit from:

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

    Note that I have used the same "name" member in each case, which could be a problem if the parents did not agree about how "name" should be handled. But they're compatible (redundant, really) in this case.

    Now we just need a class that inherits from both. Inheritance is done by call ing the constructor function (without using the new keyword) for the prototypes and the object constructors. First, the prototype has to inherit from the parent prototypes

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

    And the constructor has to inherit from the parent constructors:

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

    Now you can grow, eat, and harvest different instances:

    var fp1 = new FoodPlant('Radish', 28);
    var fp2 = new FoodPlant('Corn', 90);
    
    fp1.grow();
    fp2.grow();
    fp1.harvest();
    fp1.eat();
    fp2.harvest();
    fp2.eat();
    
    链接地址: http://www.djcxy.com/p/27236.html

    上一篇: 为什么使用Object.prototype。

    下一篇: JavaScript中的多重继承/原型