JavaScript中的多重继承/原型

dev*_*os1 126 javascript prototype multiple-inheritance

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

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

简而言之,我真正需要的是能够拥有一个能够从多个原型继承属性的对象(即每个原型可以拥有自己的正确链),但是在给定的优先顺序中(它将会搜索链以便第一个定义).

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

思考?

Ori*_*iol 44

通过使用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,
  }));
}
Run Code Online (Sandbox Code Playgroud)

说明

代理对象由目标对象和一些陷阱组成,这些陷阱定义基本操作的自定义行为.

在创建从另一个继承的对象时,我们使用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不会起作用.因此,我让它获得目标的原型,最初为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"
Run Code Online (Sandbox Code Playgroud)

  • 我会考虑用"多重授权"代替"多重继承",以便更好地了解最新情况.在您的实现中的关键概念是代理实际上正在选择正确的对象来_delegate_(或转发)消息.解决方案的强大之处在于您可以动态扩展目标原型.其他答案是使用连接(ala`Object.assign`)或得到一个完全不同的图,最后所有这些都在对象之间获得一个唯一的原型链.代理解决方案提供了运行时分支,这就好了! (3认同)
  • 即使在正常规模的应用程序中,是否也存在一些相关的性能问题? (2认同)

Roy*_*y J 13

更新(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();
Run Code Online (Sandbox Code Playgroud)

请注意,我在每种情况下都使用了相同的"名称"成员,如果父母不同意应如何处理"名称",这可能是一个问题.但在这种情况下,它们是兼容的(多余的,真的).

现在我们只需要一个继承自两者的类.通过为原型和对象构造函数调用构造函数(不使用new关键字)来完成继承.首先,原型必须从父原型继承

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

构造函数必须从父构造函数继承:

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();
Run Code Online (Sandbox Code Playgroud)

现在你可以成长,吃掉并收获不同的实例:

var fp1 = new FoodPlant('Radish', 28);
var fp2 = new FoodPlant('Corn', 90);

fp1.grow();
fp2.grow();
fp1.harvest();
fp1.eat();
fp2.harvest();
fp2.eat();
Run Code Online (Sandbox Code Playgroud)


pim*_*vdb 6

这个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;
}
Run Code Online (Sandbox Code Playgroud)

例如:

var obj = makeChain([{a:1}, {a: 2, b: 3}, {c: 4}]);
Run Code Online (Sandbox Code Playgroud)

将返回:

a: 1
  a: 2
  b: 3
    c: 4
      <Object.prototype stuff>
Run Code Online (Sandbox Code Playgroud)

使obj.a === 1,obj.b === 3等等.


Mar*_*ahn 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
}
Run Code Online (Sandbox Code Playgroud)

这将允许您传入要继承的多个对象.你将失去instanceOf这里的能力,但如果你想要多重继承,那就是给定的.


我可以在https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js上找到上述相当复杂的例子.

请注意,该文件中有一些死代码,但如果您想查看,它允许多重继承.


如果你想要链式继承(不是多重继承,但是对于大多数人来说它是同样的事情),它可以通过类来实现:

var newClass = Class.extend( cls1 ).extend( cls2 ).extend( cls3 )
Run Code Online (Sandbox Code Playgroud)

这将保留原始的原型链,但你也会运行很多无意义的代码.

  • 这创建了一个合并的浅层克隆.向"继承的"对象添加新属性不会导致新属性出现在派生对象上,就像在真正的原型继承中一样. (7认同)

Ger*_*ica 5

我提供了一个函数来允许使用多重继承来定义类。它允许使用如下代码:

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();
Run Code Online (Sandbox Code Playgroud)

产生这样的输出:

human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
Run Code Online (Sandbox Code Playgroud)

类定义如下:

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();
  }
}));
Run Code Online (Sandbox Code Playgroud)

我们可以看到使用该makeClass函数的每个类定义都接受Object映射到父类的父类名称。它还接受一个函数,该函数返回Object所定义的类的包含属性。该函数有一个参数protos,其中包含足够的信息来访问任何父类定义的任何属性。

最后需要的部分是makeClass函数本身,它做了相当多的工作。这是它以及其余代码。我已经评论makeClass得很重了:

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();
Run Code Online (Sandbox Code Playgroud)

makeClass函数还支持类属性;这些是通过在属性名称前添加$符号来定义的(请注意,结果的最终属性名称将被$删除)。考虑到这一点,我们可以编写一个专门的Dragon类来模拟 Dragon 的“类型”,其中可用 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 });

Run Code Online (Sandbox Code Playgroud)

多重继承的挑战

任何密切关注代码的人makeClass都会注意到,当上面的代码运行时,一个相当显着的不良现象悄然发生:实例化 aRunningFlying将导致对Named构造函数的两次调用!

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

 (^^ More Specialized ^^)

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

  (vv More Abstract vv)
Run Code Online (Sandbox Code Playgroud)

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

与此作斗争并非易事。让我们看一些带有简化类名的示例。我们将考虑 class A,最抽象的父类,类BC,它们都继承自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');
  }
}));
Run Code Online (Sandbox Code Playgroud)

如果我们想阻止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 ... */
  
};
Run Code Online (Sandbox Code Playgroud)

上述更改的全部目的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');
  }
}));
Run Code Online (Sandbox Code Playgroud)

这种新样式实际上成功地确保在初始化 的"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.定义util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(其中parentName其专用逻辑将紧随子类的专用逻辑)
  • C.defineutil.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(在这种情况下 ,testFn将接收名为 的父级的专用逻辑的结果parentName,并返回一个true/false指示是否应该发生短路的值
  • D.定义util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(在这种情况下blackList将是一个Array父名称,其专门逻辑应完全跳过)

这些解决方案都是可用的,但这完全是混乱!对于继承函数调用可以采用的每个独特结构,我们需要在下面定义一个专门的方法util. 真是一场绝对的灾难。

考虑到这一点,我们可以开始看到实现良好的多重继承的挑战。全面落实makeClass甚至没有考虑多重调用问题,或者与多重继承有关的许多其他问题。

这个答案变得很长。我希望makeClass我包含的实现仍然有用,即使它并不完美。我也希望任何对这个主题感兴趣的人在进一步阅读时能够记住更多的背景信息!