我有相当的功能允许使用多个继承定义类。它允许如下代码。总体而言,您会注意到完全不同于javascript中的本机分类技术(例如,您永远不会看到class
关键字):
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类型的列表存储在Class本身而不是实例上:
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
密切关注该代码的任何人都将注意到,在上述代码运行时,一个非常重要的不希望现象会无声地发生:实例化a 将导致对构造函数的两次调用!RunningFlying
Named
这是因为继承图如下所示:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
当子类的继承图中存在指向同一父类的多个路径时,子类的实例化将多次调用该父类的构造函数。
与之抗争并非易事。让我们看一些使用简化的类名的示例。我们将考虑class A
,最抽象的父类,class B
和C
,它们都从继承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');
}
}));
如果要防止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 ... */
};
上面更改为的全部目的makeClass
是使我们propertiesFn
在调用时提供一个附加的参数makeClass
。我们也应该知道,在任何类中定义的每个功能现在可以接受所有的人后一个参数,命名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');
}
}));
这种新样式实际上可以成功确保"Construct A"
仅在BC
初始化的实例时记录一次。但是存在三个缺点,其中第三个非常关键:
- 此代码变得不太易读和难以维护。该
util.invokeNoDuplicates
函数背后隐藏了许多复杂性,而思考这种样式如何避免多次调用是非直觉的和令人头疼的。我们也有那个讨厌的dups
参数,它确实需要在类中的每个函数上定义。哎哟。
- 这段代码比较慢-间接性要多得多,并且需要进行计算才能获得具有多重继承的理想结果。不幸的是,对我们的多次调用问题的任何解决方案都可能是这种情况。
- 最重要的是,依赖继承的功能结构变得非常僵化。如果子类
NiftyClass
重写了一个函数niftyFunction
,并且用于util.invokeNoDuplicates(this, 'niftyFunction', ...)
运行该函数而没有重复调用,则NiftyClass.prototype.niftyFunction
它将调用定义该函数niftyFunction
的每个父类的命名函数,忽略这些类的所有返回值,最后执行的特殊逻辑NiftyClass.prototype.niftyFunction
。这是唯一可能的结构。如果NiftyClass
继承了CoolClass
和GoodClass
,并且这两个父类都提供niftyFunction
了它们自己的定义,NiftyClass.prototype.niftyFunction
则决不能(不会冒着多次调用的危险)进行以下操作:
- A.首先运行专用逻辑
NiftyClass
,然后运行父类的专用逻辑
- B. 在所有专用父逻辑都完成之后的
NiftyClass
任何时间运行专用逻辑
- C.有条件地取决于其父级的特殊逻辑的返回值
- D.避免
niftyFunction
完全运行特定父母的专门知识
当然,我们可以通过在util
以下代码中定义专用功能来解决上述每个字母问题:
- A.定义
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B. define
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)
(parentName
父级的名称在哪里,其专用逻辑将紧随其后的是子类的专用逻辑)
- C. define
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)
(在这种情况下,testFn
将接收名为的父级的专用逻辑的结果parentName
,并将返回一个true/false
指示是否应该发生短路的值)
- D. define
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)
(在这种情况下blackList
将是Array
父名称中的一个,其专用逻辑应完全跳过)
这些解决方案都可用,但这完全是混乱!对于继承的函数调用可以采用的每个唯一结构,我们需要在下定义一个专门的方法util
。真是一场灾难。
考虑到这一点,我们可以开始看到实现良好的多重继承的挑战。makeClass
我在此答案中提供的完整实现甚至都没有考虑多重调用问题,也没有考虑与多重继承有关的许多其他问题。
这个答案越来越长。我希望makeClass
我所包含的实现仍然有用,即使它不是完美的。我也希望任何对这个主题感兴趣的人在继续阅读时,都要牢记更多的内容!