本节书摘来自异步社区《JavaScript设计模式》一书中的第9章,第9.11节, 作者: 【美】Addy Osmani 译者: 徐涛 更多章节内容可以访问云栖社区“异步社区”公众号查看。
9.11 Mixin模式
在C++和Lisp等传统编程语言中,Mixin是可以轻松被一个子类或一组子类继承功能的类,目的是函数复用。
9.11.1 子类化
对于不熟悉子类化的开发人员来说,在深入研究Mixin和Decorator之前,将阅读初学者内容。
子类化这个术语是指针对一个新对象,从一个基础或超类对象中继承相关的属性。在传统的面向对象编程中,类B是从另外一个类A扩展得来。这里我们认为A是一个超类,B是A的一个子类。因此,B的所有实例从A处继承了相关方法。但是B仍然能够定义自己的方法,包括那些A最初所定义方法的重写。
A中的一个方法,在B里已经被重写了,那么B还需要调用A中的这个方法吗,我们称此为方法链。B需要调用构造函数A(超类)吗,我们称此为构造函数链。
为了演示子类化,首先需要一个可以创建自己新实例的基本对象。让我们围绕一个人的概念来模拟子类化。
var Person = function (firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.gender = "male";
};
下一步,指定一个新类(对象),它是现有Person对象的一个子类。想象一下,在继承Person超类上的属性同时,我们需要在SuperHero上添加另外不同的属性。由于超级英雄与平常人具有很多共同的特征(如姓名、性别),希望这应该能够充分说明子类化是如何工作的。
// Person的新实例很容易像如下这样创建:
var clark = new Person("Clark", "Kent");
// 为超人(Superhero)定义一个子类构造函数
var Superhero = function (firstName, lastName, powers) {
// 调用超类的构造函数,然后使用.call()方法进行调用从而进行初始化
Person.call(this, firstName, lastName);
// 最后,保存powers,在正常Person里找不到的特性数组
this.powers = powers;
};
SuperHero.prototype = Object.create(Person.prototype);
var superman = new Superhero("Clark", "Kent", ["flight", "heat-vision"]);
console.log(superman);
// 输出Person属性和powers
Superhero构造函数创建一个源于Person的对象。这种类型的对象拥有在链中比它们靠上对象的属性,如果我们已经在Person对象中设置默认值,Superhero就能够重写所有继承的值,并且其对象本身也可以拥有特定的值。
9.11.2 Mixin(混入)
在JavaScript中,我们可以将继承Mixin看作为一种通过扩展收集功能的方式。我们定义的每个新对象都有一个原型,可以从中继承更多属性。原型可以继承于其他对象的原型,但更重要的是,它可以为任意数量的对象实例定义属性。可以利用这一点来促进函数复用(见图9-10)。
Mixin允许对象通过较低的复杂性借用(或继承)功能。由于该模式非常适用于JavaScript的对象原型,它为我们提供了一种相当灵活的方式,从不只一个Mixin中分享功能,但实际上很多功能是通过多重继承获得的。
它们可以被视为具有可以在很多其他对象原型中轻松共享属性和方法的对象。想象一下,我们在标准对象字面量中定义一个包含实用函数的Mixin,如下所示:
var myMixins = {
moveUp: function () {
console.log("move up");
},
moveDown: function () {
console.log("move down");
},
stop: function () {
console.log("stop! in the name of love!");
}
};
然后我们可以使用Underscore.js的_.extend()方法等辅助器轻松地扩展现有构造器函数的原型,以将上述行为包含进来:
// carAnimator构造函数的大体代码
function carAnimator() {
this.moveLeft = function () {
console.log("move left");
};
}
// personAnimator构造函数的大体代码
function personAnimator() {
this.moveRandomly = function () { /*..*/ };
}
// 使用Mixin扩展2个构造函数
_.extend(carAnimator.prototype, myMixins);
_.extend(personAnimator.prototype, myMixins);
// 创建carAnimator的新实例
var myAnimator = new carAnimator();
myAnimator.moveLeft();
myAnimator.moveDown();
myAnimator.stop();
// 输出:
// move left
// move down
// stop! in the name of love!
正如我们所看到的,这允许我们以通用方式轻松“混入”对象构造函数。
在下一个示例中,我们有两个构造函数:Car和Mixin。我们要做的是扩充(扩展的另一种说法)Car,以便它可以继承Mixin中定义的特定方法,即driveForward()和driveBackward()。这次,我们不会使用Underscore.js。
本示例将演示如何扩展构造函数,不需要对我们可能拥有的每个构造函数都重复这个过程而将功能包含进来。
// 定义简单的Car构造函数
var Car = function (settings) {
this.model = settings.model || "no model provided";
this.color = settings.color || "no colour provided";
};
// Mixin
var Mixin = function () { };
Mixin.prototype = {
driveForward: function () {
console.log("drive forward");
},
driveBackward: function () {
console.log("drive backward");
},
driveSideways: function () {
console.log("drive sideways");
}
};
// 通过一个方法将现有对象扩展到另外一个对象上
function augment(receivingClass, givingClass) {
// 只提供特定的方法
if (arguments[2]) {
for (var i = 2, len = arguments.length; i < len; i++) {
receivingClass.prototype[arguments[i]] = givingClass. prototype [arguments[i]];
}
}
// 提供所有方法
else {
for (var methodName in givingClass.prototype) {
// 确保接收类不包含所处理方法的同名方法
if (!Object.hasOwnProperty(receivingClass.prototype, methodName)) {
receivingClass.prototype[methodName] = givingClass. prototype[methodName];
}
// 另一方式:
// if ( !receivingClass.prototype[methodName] ) {
// receivingClass.prototype[methodName] = givingClass.prototype[methodName];
// }
}
}
}
// 给Car构造函数增加"driveForward"和"driveBackward"两个方法
augment(Car, Mixin, "driveForward", "driveBackward");
// 创建一个新Car
var myCar = new Car({
model: "Ford Escort",
color: "blue"
});
// 测试确保新增方法可用
myCar.driveForward();
myCar.driveBackward();
// 输出:
// drive forward
// drive backward
// 也可以通过不声明特定方法名的形式,将Mixin的所有方法都添加到Car里
augment(Car, Mixin);
var mySportsCar = new Car({
model: "Porsche",
color: "red"
});
mySportsCar.driveSideways();
// 输出:
// drive sideways
优点和缺点
Mixin有助于减少系统中的重复功能及增加函数复用。当一个应用程序可能需要在各种对象实例中共享行为时,我们可以通过在Mixin中维持这种共享功能并专注于仅实现系统中真正不同的功能,来轻松避免任何重复。
也就是说,有关Mixin的缺点是稍有争议的。有些开发人员认为将功能注入对象原型中是一种很糟糕的想法,因为它会导致原型污染和函数起源方面的不确定性。在大型系统中,可能就会有这种情况。
我认为,强大的文档有助于将与混入函数来源有关的困惑减至最低,但对于每一种模式,如果在实现期间多加注意,一切应该会很顺利。