4

假设我有以下“类树”:

              Element
               /   \
              /     \
             /       \
  Positionnable     Sizeable
             \        /
              \      /
               \    /
             Rectangle

现在说元素构造函数做了一些事情:

var Element = function() {
    this.traits = [ ];
};

并且说子类构造函数需要在做自己的工作之前调用它们的父构造函数(元素构造函数):

var Positionnable = function() {
    Element.call( this );
    this.traits.position = { x : 0, y : 0 }; // Requires this.traits to be set.
};

var Sizable = function() {
    Element.call( this );
    this.traits.size = { w : 0, h : 0 }; // Requires this.traits to be set.
};

问题是,当我让 Rectangle 从 Positionnable 和 Sizable 继承时(通过合并原型),来自 Element 的构造函数将被调用两次,这可能是一个问题,具体取决于它的作用:

var Rectangle = function() {
    Positionnable.call( this ); // Calls Element constructor
    Sizeable.call( this );      // Calls Element constructor
};

所以我想到了两种可能性:在某个地方添加布尔值,当调用构造函数时将其设置为 true 以避免多次执行此操作,但这看起来很脏。

或者我可以调用 Rectangle 中的所有直接或间接父构造函数:

var Positionnable = function() {
    this.traits.position = { x : 0, y : 0 }; // Assumes parent constructor has been called
};

var Sizable = function() {
    this.traits.size = { w : 0, h : 0 }; // Assumes parent constructor has been called
};

var Rectangle = function() {
    Element.call( this );
    Positionnable.call( this ); // Does no longer call Element constructor
    Sizeable.call( this );      // Does no longer call Element constructor
};

但这会假设 Element 构造函数在 Positionnable 和 Sizable 构造函数之前调用(这意味着这两个在单独调用时会失败),这也将涉及(对于编码器)递归查找所有直接或间接父类来调用它们的构造函数(如果继承树比这更复杂,可能会很烦人),如果我需要为 Rectangle 创建一个子类,我会遇到和现在一样的问题。

那么我怎么能只调用一次构造函数呢?

4

2 回答 2

4

传统上,在 JavaScript 中,最好完全避免多重继承,并在必要时使用mixin来共享功能。多重继承会导致各种问题,包括臭名昭著的可怕的厄运钻石问题(这是您面临的问题)。

不同的语言使用不同的方法来解决钻石问题。例如,Java 使用接口而不是多重继承,C++ 使用虚拟继承来解决歧义,而 Python 和 Dylan 等语言使用方法解析顺序来线性化异构

尽管如此,上述所有技术都不足以解决您面临的钻石构造问题。接口在 JavaScript 这样的动态语言中是无用的:鸭子类型扮演着更重要的角色。虚拟继承不必要地使事情复杂化。线性化解决了继承方法的顺序,但不能解决您的问题。

您可以使用两种技术来解决 JavaScript 中的这个问题:

  1. 完全放弃多重继承,并像大多数 JavaScript 程序员一样使用 mixins 来共享功能。
  2. 根据是否this是派生类构造函数的实例,有条件地从派生类构造函数调用基类构造函数。

我建议你使用 mixins。然而,最终的决定是你自己做的。我将代表我简单地介绍每种技术的优点和缺点。

混合

Mixin 是在 JavaScript 中实现多重继承的传统方式,大多数时候传统方法被证明是最好的。一个 mixin 就像一个带有实现的 Java 接口。您可以(并且应该以我的拙见)重构您的代码,如下所示:

function Element() {
    // constructor logic
}

function positionable(that, x, y) {
    that.x = x;
    that.y = y;
}

function sizable(that, w, h) {
    that.w = w;
    that.h = h;
}

function Rectangle(x, y, w, h) {
    Element.call(this);
    positionable(this, x, y);
    sizable(this, w, h);
}

Rectangle.prototype = Object.create(Element.prototype);

正如您在上面的代码中看到的那样positionablesizable它们不是构造函数。它们是 mixins - 你不应该使用它们来创建实例。您可以使用它们来增加实例。

在 JavaScript 中,最好的做法是创建从单个基类继承的类,并根据需要使用 mixin 来共享附加功能。

奖励:编程很像英语。名词就像类,动词就像方法,形容词就像混入。以“able”结尾的词通常是形容词。因此positionable,并且sizable应该作为 mixins 来实现。

课程

如果您仍然一心想要实施PositionableSizable作为课程,那么恐怕我无法改变您的想法。因此,我将向您展示如何使用类来解决您的问题,并说明为什么这种技术不如使用 mixins:

function Element() {
    this.traits = [];
}

function Position() {
    if (this instanceof Position) Element.call(this);
    this.traits.position = { x: 0, y: 0 };
}

Position.prototype = Object.create(Element.prototype);


function Sizable() {
    if (this instanceof Sizable) Element.call(this);
    this.traits.size = { w: 0, h: 0 };
}

Sizable.prototype = Object.create(Element.prototype);

function Rectangle() {
    Element.call(this);
    Positionable.call(this);
    Sizable.call(this);
}

Rectangle.prototype = Object.create(Element.prototype);
_.extend(Rectangle.prototype, Positionable.prototype, Sizable.prototype);

正如你所看到的,这段代码肯定比使用 mixins 编写的代码更丑。在PositionableSizable构造函数中,我们在调用构造函数之前检查是否this是各自构造函数的实例Element,解决了菱形构造函数问题。

我们使用 Underscore.jsextend函数将Positionable.prototype和的属性复制Sizable.prototypeRectangle.prototype. 我会让你弄清楚为什么这不是实现多重继承的推荐方法。

于 2013-09-03T13:22:07.873 回答
0

在我看来,第一种方法比第二种方法更好。

但是,多重继承通常不是一个好主意。

随着时间的推移,它会使您的代码复杂化。

如果我们在 Java 中思考一下,更有意义的是

Rectangle extends Element implements Positionable, Sizable

我认为您应该重新考虑代码的类层次结构。

编辑:

如果你正在这样做,那么这就是我将如何去做。它没有经过测试。您应该尝试使用它,并添加更多功能。

function extend(subClass, superClass) {
    var superclasses = subClass._superclasses || (subClass._superclasses = []);
    addSuperClasses(superclasses, superClass._superclasses);
    superclasses.push(superClass);
    subClass.prototype.initialize = initialize;
}

function addSuperClasses(superclasses, list) {
    if (list) {
        for (var i = 0, l = list.length, superclass; i < l; i++) {
            superclass = list[i];
            if (superclasses.indexOf(superclass) === -1) {
                superclasses.push(superclass);
            }
        }
    }
}

function initialize() {
    if (!this._initialized) {
        this._initialized = true;
        var superclasses = this.constructor._superclasses;
        if (superclasses) {
            for (var i = 0, l = superclasses.length; i < l; i++) {
                superclasses[i].call(this);
            }
        }
    }
}

function Element() {}

function Positionable() {
    this.initialize();
    this.traits.position = {
        x: 0,
        y: 0
    };
}
extend(Positionable, Element);

function Sizable() {
    this.initialize();
    this.traits.size = {
        w: 0,
        h: 0
    };
}
extend(Sizable, Element);

function Rectangle() {}
extend(Rectangle, Positionable);
extend(Rectangle, Sizable);
于 2013-09-03T12:24:53.703 回答