JavaScript 中的多重继承/原型

我已经到了需要在 JavaScript 中实现某种基本多重继承的地步。(我不是来讨论这是不是一个好主意的,所以请不要把这些评论告诉别人。)

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

总而言之,我真正需要的是一个对象能够从多个原型 链子继承一个属性(也就是说,每个原型可以有自己的正确链) ,但在给定的优先顺序(它将搜索链为第一个定义)。

为了证明这在理论上是可行的,它可以通过将次级链接到主链的末端来实现,但这会影响到以前的原型的所有实例,这不是我想要的。

有什么想法吗?

75693 次浏览

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

它将允许您传入多个要继承的对象。在这里你将失去 ABc0的能力,但是如果你想要多重继承的话,这是必然的。


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

请注意,这个文件中有一些死代码,但是如果你想查看的话,它允许多重继承。


如果你想要链式继承(不是多重继承继承,但对大多数人来说是一回事) ,可以通过类来实现,比如:

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

这将保留原始的原型链,但是也会有许多无意义的代码运行。

这一个使用 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等等。

我绝对不是 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]());
}
}

在 JavaScript 中实现多重继承是可能的,尽管很少有库这样做。

我可以指出 Ring.js,我所知道的唯一的例子。

看看包 小队

在 IeUnit 中实现的概念同化似乎以一种相当动态的方式提供了您正在寻找的内容。

下面是 使用构造函数的原型链接的一个例子:

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“类”只是一个函数对象,作为一个构造函数加上一个附加的原型对象。(来源: 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);

我最初用 这个答案写了一些这样的文章。

现场的后来者是 简单声明。但是,在处理多重继承时,仍然会得到原始构造函数的副本。这在 Javascript 中是必需的..。

默克。

更新(2019) : 原帖已经过时了。这篇文章(现在是互联网档案馆链接,因为域已经消失了)及其相关的 GitHub 库是一个很好的现代方法。

原文: 如果你使用构造的原型而不是泛型对象的原型,Javascript 中的多重继承[编辑,不是正确的类型继承,而是属性继承;。这里有两个要继承的父类:

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

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

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

在 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查找包含该属性的第一个原型,并在适当的接收器上调用它的 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"

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

您所需要做的就是每次使用 Create ()创建一个带有指定原型对象和属性的新对象,然后如果您计划在将来实例化 B,请确保每一步都更改 Object.Prototype.structor

要继承实例属性 thisAthisB,我们在每个对象函数的末尾使用 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();
  • BA继承原型
  • CB继承原型
  • objCC的一个实例

这是对上述步骤的一个很好的解释:

JavaScript 中的 OOP: 你需要知道的

我今天做了很多这方面的工作,并试图在 ES6中实现这一点。我的方法是使用 Browserify,Babel,然后我用 Wallaby 测试它,它似乎工作。我的目标是扩展当前的 Array,包括 ES6、 ES7,并在原型中添加一些处理音频数据所需的额外自定义特性。

袋鼠通过了我的四项测试。Js 文件可以粘贴到控制台中,您可以看到‘ include’属性位于类的原型中。我明天还想再测试一下。

下面是我的方法: (我很可能会在睡一会儿之后重构和重新打包为一个模块!)

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

我觉得简单得离谱。这里的问题是,对于您调用的第一个类,子类将只引用 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

我会用 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();

检查下面显示支持多重继承的代码

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

这个怎么样,它在 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();

下面是 specalize _ 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

我只是在别人的属性中分配我需要的类,然后添加一个代理来自动指向我喜欢的类:

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

我提供了一个函数,允许用多重继承定义类,它允许像下面这样的代码:

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 类型列表存储在类本身上,而不是实例上:

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代码的人都会注意到,当上面的代码运行时,会无声地出现一个相当重要的不良现象: 实例化一个 ABC1将导致对 Named构造函数的两次调用!

这是因为继承图像是这样的:

 (^^ More Specialized ^^)


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


(vv More Abstract vv)

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

与之抗争是非同小可的。让我们来看一些具有简化类名的示例。我们将考虑类 A,最抽象的父类,类 BC,它们都从 A继承,类 BCBC继承(因此从概念上来说是从 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,我们可能需要放弃直接调用继承构造函数的风格。我们将需要某种程度的间接检查是否重复调用正在发生,并在它们发生之前短路。

我们可以考虑改变提供给 properties 函数的参数: 除了 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进行更改的全部目的是为了在调用 makeClass时为 propertiesFn提供一个额外的参数。我们还应该意识到,在任何类中定义的每个函数现在都可能在其他所有函数之后接收到一个名为 dup的参数,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');
}
}));

这种新样式实际上成功地确保了在初始化 BC实例时仅记录 "Construct A"一次。但有三个缺点,其中第三个是 非常关键:

  1. 这段代码的可读性和可维护性已经降低。util.invokeNoDuplicates函数背后隐藏着许多复杂性,考虑这种风格如何避免多次调用是不直观的,也是令人头疼的。我们还有那个讨厌的 dups参数,它确实需要在 类中的每一个函数上定义。哎哟。
  2. 这个代码比较慢——需要更多的间接和计算才能达到理想的多重继承。不幸的是,我们的多调用问题的 任何解决方案可能就是这种情况。
  3. 最重要的是,依赖于遗传的功能结构已经变得 niftyFunction1 。如果一个子类 NiftyClass覆盖了一个函数 niftyFunction,并且使用 util.invokeNoDuplicates(this, 'niftyFunction', ...)来运行它而不进行重复调用,那么 NiftyClass.prototype.niftyFunction将调用定义它的每个父类的名为 niftyFunction的函数,忽略来自这些类的任何返回值,并最终执行 NiftyClass.prototype.niftyFunction的专用逻辑。这是 niftyFunction2。如果 NiftyClass继承了 CoolClassGoodClass,并且这两个父类都提供了它们自己的 niftyFunction定义,那么 NiftyClass.prototype.niftyFunction将永远不能(在不冒多重调用风险的情况下) :
  • 先运行 NiftyClass的专用逻辑,那么的父类的专用逻辑
  • 在除 之后以外的任何点运行 NiftyClass的专用逻辑,所有的专用父逻辑都已完成
  • 根据父逻辑的特殊逻辑的返回值有条件地执行
  • 避免运行特定父母的专用 niftyFunction

当然,我们可以通过在 util下定义专门的函数来解决上面每个字母的问题:

  • 定义 util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • 定义 util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(其中 parentName是父类的名称,其专用逻辑后面紧跟着子类的专用逻辑)
  • 定义 util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(在这种情况下,testFn将接收名为 parentName的父节点的专用逻辑的结果,并返回一个 true/false值,指示是否应该发生短路)
  • D. 定义 util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(在这种情况下,blackList是父名称的 Array,其专门逻辑应该完全跳过)

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

考虑到这一点,我们可以开始看到实施良好多重继承所面临的挑战。我在这个答案中提供的 makeClass的完整实现甚至没有考虑到多重调用问题,或者许多其他与多重继承有关的问题。

这个答案越来越长了。我希望我包含的 makeClass实现仍然有用,即使它并不完美。我也希望任何对这个话题感兴趣的人在进一步阅读的时候能够获得更多的上下文信息!