JavaScript中的组合、继承和聚合

61

在网上有很多关于组合与继承的信息,但我没有找到适用于JavaScript的好例子。使用以下代码来演示继承:

function Stock( /* object with stock names and prices */ ) {
    for (var company_name in arguments[0]) {
        // copy the passed object into the new object created by the constructor
        this[company_name] = arguments[0][company_name]; 
    }
}

// example methods in prototype, their implementation is probably redundant for
// this question, but list() returns an array with toString() invoked; total()
// adds up the stock prices and returns them. Using ES5 feature to make
// inherited properties non-enumerable 

Stock.prototype =  {
    list: function () {
        var company_list = [];
        for (var company_name in this)
            company_list.push(company_name);
        return company_list.toString();
    },
    total: function () {
        var price_total = 0;
        for (var company_name in this)
            price_total += this[company_name];
        return '$' + price_total;
    }
};

Object.defineProperties(Stock.prototype, {
    list: { enumerable: false },
    total: { enumerable:false }
});

var portfolio = new Stock({ MSFT: 25.96, YHOO: 16.13, AMZN: 173.10 });
portfolio.list();  // MSFT,YHOO,AMZN
portfolio.total(); // $215.19

(为了让代码更简洁,您可以省略方法实现,例如:Stock.total = function(){ /* 代码 */ }。我只是在这里放置它们以增加可读性)。如果在面向对象编程中,组合方式被广泛采用,那么为什么大多数使用JavaScript的人似乎只使用原型和继承?我在网上没有找到关于JavaScript中组合的很多信息,只有其他语言。

有人能给我举个例子来演示组合和聚合吗?


1
这个问题太模糊了,可能更适合发布在程序员问答社区 - Sergio Tulentsev
1
如果太模糊不清,那么我认为它都不适合任何网站。 - Peter Olson
4
我怎样才能更加具体?我用继承来展示代码,现在我想看看它被组合构建的样子。对于其他语言也有类似的问题。 - Brian
所有的组合都是一个实例嵌套在另一个实例中,可能会委托给组合类。你想要什么样的例子? - Dave Newton
一个很好的组合示例是Phaser游戏框架(http://phaser.io)。 - bren
3个回答

78
语言无关于组合与继承的区别。如果你了解类是什么以及类的实例是什么,那么你已经拥有了所需知识。
组合是指一个类由其他类组成,或者说对象的实例引用了其他对象的实例。
继承是指一个类继承另一个类的方法和属性。
假设你有两种功能A和B。你想定义第三个功能C,它包含A和B的部分或全部内容。你可以让C继承B和A,这样C就拥有了B和A的所有内容,因为C是B和A的子类;或者你可以让每个C的实例都有一个A的实例和一个B的实例,并调用这些功能中的项目。在后一种情况下,每个C实例实际上包装了一个B和一个A的实例。
当然,根据不同的编程语言,可能无法让一个类同时继承自两个类(例如,Java不支持多重继承),但这只是与具体编程语言相关的细节,与概念本身无关。
现在,针对编程语言的具体细节...
我使用了“class”这个词,但 JavaScript 没有类的概念。它只有对象(除了简单类型)。JavaScript 使用原型继承,这意味着它有一种有效地定义对象和这些对象上的方法的方式。关于原型继承的详细信息,这是另一个问题的主题;你可以在 Stack Overflow 上搜索,因为已经有人回答过这个问题。
现在回到我们上面的例子,你有A、B和C。
对于继承,你可以:
// define an object (which can be viewed as a "class")
function A(){}

// define some functionality
A.prototype.someMethod = function(){}

如果你想让C扩展A,你应该这样做:

C.prototype = new A();
C.prototype.constructor = A;

现在,因为每个C的实例“isA” A, 所以每个C的实例都具有该方法someMethod

Javascript不支持多继承*(稍后会详细说明),因此您无法使C同时扩展A和B。但是,您可以使用组合来给它提供功能。实际上,这是一些人更喜欢组合而不是继承的原因之一;没有限制将功能组合在一起(但这不是唯一的原因)。

function C(){
   this.a = new A();
   this.b = new B();
}

// someMethod on C invokes the someMethod on B.
C.someMethod = function(){
    this.a.someMethod()
}

所以这是继承和组合两种简单示例。但是,这还不是故事的结尾。之前我说过Javascript不支持多重继承,从某种意义上来说确实如此,因为你不能基于多个对象的原型来构建一个对象的原型;也就是说你不能做到:

C.prototype = new B();
C.prototype.constructor = B;
C.prototype.constructor = A;

因为在执行第三行代码时,你刚好撤销了第二行代码的操作。这对于instanceof运算符有影响。

但是,这并不重要,因为虽然你不能重复定义对象的构造函数,但你仍然可以向对象的原型中添加任何方法。所以即使不能像上面的例子那样做,你仍然可以向C.prototype添加任何想要的内容,包括A和B的原型上的所有方法。

许多框架都支持这种方式,并且使其易于操作。我经常使用Sproutcore框架进行开发,在该框架中,你可以执行以下操作:

A = {
   method1: function(){}
}

B = {
   method2: function(){}
}

C = SC.Object.extend(A, B, {
   method3: function(){}
}
在这里,我使用对象字面量定义了功能AB,然后将两者的功能添加到C中,所以每个C的实例都有方法1、2和3。在这种特殊情况下,extend方法(由框架提供)完成了设置对象原型的大部分工作。
编辑-在您的评论中,您提出了一个好问题,即“如果使用组合,如何协调主对象的范围与组成主对象的对象的范围”。
有很多方法。首先是简单地传递参数。 因此
C.someMethod = function(){
    this.a.someMethod(arg1, arg2...);
}

这里你不需要操作作用域,只需要简单地在函数之间传递参数即可。这是一种简单且非常可行的方法。(这些参数可以来自 this 或被传入,无论哪种方式...)

另一种方法是使用 JavaScript 的 call(或 apply)方法,它基本上允许您设置函数的作用域。

C.someMethod = function(){
    this.a.someMethod.call(this, arg1, arg2...);
}

为了更清晰一些,以下内容是等价的

C.someMethod = function(){
    var someMethodOnA = this.a.someMethod;
    someMethodOnA.call(this, arg1, arg2...);
}
在Javascript中,函数是对象,因此你可以将它们赋值给变量。
这里的`call`调用设置了`someMethodOnA`的作用域为`this`,即C的实例。

1
katspaugh,是的,这是真的,你需要根据你所处理的实际实现来决定什么对你是正确的。这就是为什么软件设计并不是微不足道的原因 ;) - hvgotcodes
1
Brian,但当你使用组合时,你并没有扩展一个类。当你继承时,子类与父类存在"是一个"的关系。但当你组合时,情况并非如此。这在强类型语言中有一定的影响... - hvgotcodes
令人惊叹的回复帮助我更好地理解组合。我刚刚注意到,在这种情况下,聚合是否存在任何差异或良好实践都没有参考。 - Shane
我看过的最好的关于组合与继承的解释。谢谢。 - Alex Hill
另外,在执行C.prototype = new A();之前,您需要声明一个function C() {},因为您不能设置尚未定义的对象的属性。 - imrek
显示剩余10条评论

4
我可以用纯JavaScript(ES5)向您展示如何以“对象组合”方式重写您的代码。我使用工厂函数而不是构造函数来创建对象实例,因此不需要使用new关键字。这样,我可以更喜欢对象增强(组合)而非传统/伪类/原型继承,因此不需要调用Object.create函数。
生成的对象是一个漂亮的平面组合对象:
/*
 * Factory function for creating "abstract stock" object. 
 */
var AbstractStock = function (options) {

  /**
   * Private properties :)
   * @see http://javascript.crockford.com/private.html
   */
  var companyList = [],
      priceTotal = 0;

  for (var companyName in options) {

    if (options.hasOwnProperty(companyName)) {
      companyList.push(companyName);
      priceTotal = priceTotal + options[companyName];
    }
  }

  return {
    /**
     * Privileged methods; methods that use private properties by using closure. ;)
     * @see http://javascript.crockford.com/private.html
     */
    getCompanyList: function () {
      return companyList;
    },
    getPriceTotal: function () {
      return priceTotal;
    },
    /*
     * Abstract methods
     */
    list: function () {
      throw new Error('list() method not implemented.');
    },
    total: function () {
      throw new Error('total() method not implemented.');
    }
  };
};

/*
 * Factory function for creating "stock" object.
 * Here, since the stock object is composed from abstract stock
 * object, you can make use of properties/methods exposed by the 
 * abstract stock object.
 */
var Stock = compose(AbstractStock, function (options) {

  return {
    /*
     * More concrete methods
     */
    list: function () {
      console.log(this.getCompanyList().toString());
    },
    total: function () {
      console.log('$' + this.getPriceTotal());
    }
  };
});

// Create an instance of stock object. No `new`! (!)
var portofolio = Stock({MSFT: 25.96, YHOO: 16.13, AMZN: 173.10});
portofolio.list(); // MSFT,YHOO,AMZN
portofolio.total(); // $215.19

/*
 * No deep level of prototypal (or whatsoever) inheritance hierarchy;
 * just a flat object inherited directly from the `Object` prototype.
 * "What could be more object-oriented than that?" –Douglas Crockford
 */ 
console.log(portofolio); 



/*
 * Here is the magic potion:
 * Create a composed factory function for creating a composed object.
 * Factory that creates more abstract object should come first. 
 */
function compose(factory0, factoryN) {
  var factories = arguments;

  /*
   * Note that the `options` passed earlier to the composed factory
   * will be passed to each factory when creating object.
   */
  return function (options) {

    // Collect objects after creating them from each factory.
    var objects = [].map.call(factories, function(factory) {
      return factory(options);
    });

    // ...and then, compose the objects.
    return Object.assign.apply(this, objects);
  };
};

点击这里进行Fiddle操作。


2
“有人能否举个例子,用上述代码演示组合和聚合?”
乍一看,提供的示例似乎不是展示JavaScript中组合的最佳选择。对于访问任何库存对象自身属性的方法totallistStock构造函数的prototype属性仍然是理想的位置。
可以做的是将这些方法的实现从构造函数的原型中解耦,并在那里提供它们 - 这是一种额外的代码重用形式 - Mixin...
例如:
var Iterable_listAllKeys = (function () {

    var
        Mixin,

        object_keys = Object.keys,

        listAllKeys = function () {
            return object_keys(this).join(", ");
        }
    ;

    Mixin = function () {
        this.list = listAllKeys;
    };

    return Mixin;

}());


var Iterable_computeTotal = (function (global) {

  var
      Mixin,

      currencyFlag,

      object_keys = global.Object.keys,
      parse_float = global.parseFloat,

      aggregateNumberValue = function (collector, key) {
          collector.value = (
              collector.value
              + parse_float(collector.target[key], 10)
          );
          return collector;
      },
      computeTotal = function () {
          return [

              currencyFlag,
              object_keys(this)
                  .reduce(aggregateNumberValue, {value: 0, target: this})
                  .value
                  .toFixed(2)

          ].join(" ");
      }
    ;

    Mixin = function (config) {
        currencyFlag = (config && config.currencyFlag) || "";

        this.total = computeTotal;
    };

    return Mixin;

}(this));


var Stock = (function () {

  var
      Stock,

      object_keys = Object.keys,

      createKeyValueForTarget = function (collector, key) {
          collector.target[key] = collector.config[key];
          return collector;
      },
      createStock = function (config) { // Factory
          return (new Stock(config));
      },
      isStock = function (type) {
          return (type instanceof Stock);
      }
  ;

  Stock = function (config) { // Constructor
      var stock = this;
      object_keys(config).reduce(createKeyValueForTarget, {

          config: config,
          target: stock
      });
      return stock;
  };

  /**
   *  composition:
   *  - apply both mixins to the constructor's prototype
   *  - by delegating them explicitly via [call].
   */
  Iterable_listAllKeys.call(Stock.prototype);
  Iterable_computeTotal.call(Stock.prototype, {currencyFlag: "$"});

  /**
   *  [[Stock]] factory module
   */
  return {
      isStock : isStock,
      create  : createStock
  };

}());


var stock = Stock.create({MSFT: 25.96, YHOO: 16.13, AMZN: 173.10});

/**
 *  both methods are available due to JavaScript's
 *  - prototypal delegation automatism that covers inheritance.
 */
console.log(stock.list());
console.log(stock.total());

console.log(stock);
console.dir(stock);

在网上有很多关于组合与继承的信息,但我没有找到JavaScript的好例子。...我没有在网上找到很多关于JavaScript中组合的信息,只有其他语言的信息。...
也许搜索查询不够具体,但即使在2012年搜索“JavaScript Mixin composition”也应该引导人们朝着一个不错的方向前进。
如果在面向对象编程中,组合在许多情况下都更受青睐,那么为什么大多数使用JavaScript的人似乎只使用原型和继承?
因为他们大多使用他们所学和/或熟悉的内容。也许应该更广泛地传播关于JavaScript作为基于委托的语言以及它可以实现什么的知识。
附录:
这些是相关的线程,最近更新,希望有所帮助...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接