原文:https://addyosmani.com/resources/essentialjsdesignpatterns/book/#mixinpatternjavascript

在像 C++ 和 Lisp 这样的传统的编程语言中,Mixin 是为单个或一组子类提供可轻松继承的功能,以便复用这些功能。

子类化

对于不熟悉子类化的开发者,我们将在深入 Mixins 和 Decorators 前先做一下简短的初级入门介绍。

子类化这个术语指的是从一个基础的或者超类对象上继承属性给一个新的对象。在传统的面向对象编程中,一个类 B 是可以继承另一个类 A 。这种情况下我们把 A 看作是超类,而 B 则是 A 的子类。因此,所有 B 的实例都继承了 A 的方法。然而 B 仍然可以定义它自己的方法,包括重写在 A 已经定义的方法。

B 需要执行 A 中被覆盖的方法的时,我们将其称为方法链。当 B 需要执行调用构造函数 A 时,我们称之为构造器链。

为了示范子类化,我们首先需要一个基础对象,它可以创建自己的新实例。让我们围绕一个“人”的概念来建模。

  1. var Person = function(firstName, lastName) {
  2. this.firstName = firstName;
  3. this.lastName = lastName;
  4. this.gender = 'male';
  5. };

接下来,我们会要创建一个新的类(对象),它是当前 Person 对象的子类。我们希望添加不同的属性来将 PersonSuperhero 区分开来,并且从“超类” Person 继承它的属性。由于超级英雄和普通人有很多共同的特征(如,姓名、性别),这应该能够充分的说明子类的作用。

  1. // Person 的新实例可以按照下面的方式轻松的创建:
  2. var clark = new Person('Clark', 'Kent');
  3. // 为 “Superhero” 创建子类构造器:
  4. var Superhero = function(firstName, lastName, powers) {
  5. // 在新对象上调用超类的构造函数,然后使用 .call() 来调用构造函数
  6. // 并把它当作初始化对象的方法
  7. Person.call(this, firstName, lastName);
  8. // 最终,存储他们的特权,一组普通人没有的特性
  9. this.powers = powers;
  10. };
  11. Superhero.prototype = Object.create(Person.prototype);
  12. var superman = new Superhero('Clark', 'Kent', ['flight', 'heat-vision']);
  13. console.log(superman);
  14. // 输出 Person 的属性以及能力

Superhero 构造器创建了一个“起源”自于 Person 的对象。此类对象拥有位于其链上方对象的属性,如果我们在 Person 对象上设置了默认值, Superhero 是有能力在它的对象中用特定的值来覆盖继承来的值。

混入

在 JavaScript 中,我们可以把 Mixins 的继承看作是是通过拓展来获取功能的方式。我们定义的每个对象都有一个原型,我们可以从中继承其他属性。

原型还可以继承其他原型,但是,更重要的是,它可以为任意数量的对象实例定义属性。我们可以利用这个原理来加强功能复用。

Mixins 允许对象从它们这儿以极少的复杂性来借(或者继承)功能。因为这个模式能和 JavaScript 对象原型很好协作,它就赋予我们相对便捷的方式来通过多重继承来共享功能,而不是单单从一个 Mixin。

Mixins 可以被看作是拥有属性和方法的对象,这些方法和属性可以轻易的通过一组其他的对象原型来实现共享。假设我们按照下面的方式定义了一个 Mixin,它是一个包含一些功能函数的标准的对象字面量:

  1. var myMixins = {
  2. moveUp: function() {
  3. console.log('move up');
  4. },
  5. moveDown: function() {
  6. console.log('move down');
  7. },
  8. stop: function() {
  9. console.log('stop! in the name of love!');
  10. }
  11. };

然后我们可以通过使用 Underscore.js 的 _.extend() 这类的辅助方法来轻松的拓展已存在的构造器函数的原型,使其拥有 Mixin 的能力:

  1. // 一个 carAnimator 构造器的骨架
  2. function CarAnimator(){
  3. this.moveLeft = function(){
  4. console.log( "move left" );
  5. };
  6. }
  7. // 一个 personAnimator 构造器的骨架
  8. function PersonAnimator(){
  9. this.moveRandomly = function(){ /*..*/ };
  10. }
  11. // 使用我们的 Mixin 来拓展这两个构造器
  12. _.extend( CarAnimator.prototype, myMixins );
  13. _.extend( PersonAnimator.prototype, myMixins );
  14. // 创建一个新 carAnimator 的实例
  15. var myAnimator = new CarAnimator();
  16. myAnimator.moveLeft();
  17. myAnimator.moveDown();
  18. myAnimator.stop();
  19. // 输出:
  20. // move left
  21. // move down
  22. // stop! in the name of love!

正如我们所看到的,它让我们可以轻松的将通用的行为“混”入到对象构造器中。在接下来的示例中,我们会有两个构造器:一个 Car 和一个 Mixin。我们要做的是加强(换句话说是拓展)Car,这样它就可以继承 Mixin 中定义的特定的方法,即 driveForward()driveBackward() 。这次我们不用 Underscore.js 。

相反,在这个例子中,我们将示范在不用为重复处理每个构造器的情况下如何来增强构造器。

  1. // 定义一个简单的 Car 构造器
  2. var Car = function(settings) {
  3. this.model = settings.model || 'no model provided';
  4. this.color = settings.color || 'no colour provided';
  5. };
  6. // Mixin
  7. var Mixin = function() {};
  8. Mixin.prototype = {
  9. driveForward: function() {
  10. console.log('drive forward');
  11. },
  12. driveBackward: function() {
  13. console.log('drive backward');
  14. },
  15. driveSideways: function() {
  16. console.log('drive sideways');
  17. }
  18. };
  19. // 将另一个对象的方法拓展到已存在对象上
  20. function augment(receivingClass, givingClass) {
  21. // 只提供特定的方法
  22. if (arguments[2]) {
  23. for (var i = 2, len = arguments.length; i < len; i++) {
  24. receivingClass.prototype[arguments[i]] =
  25. givingClass.prototype[arguments[i]];
  26. }
  27. }
  28. // 提供所有的方法
  29. else {
  30. for (var methodName in givingClass.prototype) {
  31. // 检查接收方类是否有与当前正在处理的同名方法
  32. if (!Object.hasOwnProperty.call(receivingClass.prototype, methodName)) {
  33. receivingClass.prototype[methodName] =
  34. givingClass.prototype[methodName];
  35. }
  36. // 可选的(同时检查原型链)
  37. // if ( !receivingClass.prototype[methodName] ) {
  38. // receivingClass.prototype[methodName] = givingClass.prototype[methodName];
  39. // }
  40. }
  41. }
  42. }
  43. // 拓展 Car 构造器以包含 "driceForward" 和 "driveBackward"
  44. augment(Car, Mixin, 'driveForward', 'driveBackward');
  45. // 创建一个新的 Car
  46. var myCar = new Car({
  47. model: 'Ford Escort',
  48. color: 'blue'
  49. });
  50. // 测试我们是否能够访问对应的方法
  51. myCar.driveForward();
  52. myCar.driveBackward();
  53. // 输出:
  54. // drive forward
  55. // drive backward
  56. // 我们可以通过传入第三个参数来拓展 Car,使其包含 mixin 所有的方法
  57. augment(Car, Mixin);
  58. var mySportsCar = new Car({
  59. model: 'Porsche',
  60. color: 'red'
  61. });
  62. mySportsCar.driveSideways();
  63. // 输出:
  64. // drive sideways

优缺点

Mixins 有助于减少系统中重复性的功能,提升功能的可复用性。当系统中存在需要跨对象实例共享行为时,我们可以通过将这些共享的功能维护在一个 Mixin 中,避免他们重复定义,从而只需关注我们系统中那些真正不同的功能。

那就是说,Mixins 的缺点是有争议的。有些开发者认为将这些功能注入到对象的原型是个坏主意,因为它会导致原型污染和功能来源的不确定性。在大型系统中尤为如此。

我认为详尽的文档可以帮助解决减少混入的功能来源的困惑性,但是和每一种模式一样,如果在实现的过程中小心谨慎,应该没就没太大的问题。