【问题标题】:Composition, Inheritance, and Aggregation in JavaScriptJavaScript 中的组合、继承和聚合
【发布时间】:2012-01-31 13:45:52
【问题描述】:

网上有很多关于组合与继承的信息,但我还没有找到合适的 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(){ /* code */ } 我只是把它们放在那里是为了花哨)。如果组合在 OOP 中的很多情况下都受到青睐,为什么大多数使用 JavaScript 的人似乎只使用原型和继承?我在网上没有找到很多关于 JavaScript 组合的信息,只有其他语言的。

有人可以给我一个使用上述代码的例子来演示组合和聚合吗?

【问题讨论】:

  • 这个问题太模糊了。可能更适合programmers.se
  • 如果它太模糊,那么 IMO 它不适合任何网站。
  • 我怎么能更具体?我使用继承呈现代码,现在我有兴趣看到它使用组合构建。对于其他语言,还有其他问题是相同的。
  • 所有组合都是另一个实例中的某个实例,可能委托给组合类。你想要什么样的例子?
  • Phaser 游戏框架 (phaser.io) 就是一个很好的组合示例

标签: javascript oop inheritance composition


【解决方案1】:

在处理组合与继承时,语言是无关紧要的。如果您了解什么是类以及类的实例是什么,那么您就拥有了所需的一切。

组合只是一个类由其他类组合;或者换一种说法,一个对象的实例引用了其他对象的实例。

继承是指一个类从另一个类继承方法和属性。

假设您有两个功能,A 和 B。您想定义第三个功能 C,它同时具有 A 和 B 的部分或全部。您可以让 C 从 B 和 A 扩展,在这种情况下 C拥有 B 和 A 的所有内容,因为 C isA B 和 A,或者您可以使 C 的每个实例都具有 A 的实例和 B 的实例,并在这些功能上调用项目。在后一种情况下,每个实例 C 实际上包装了一个 B 的实例和一个 A 的实例。

当然,根据语言,您可能无法从 2 个类扩展一个类(例如 Java 不支持多重继承),但这是与概念无关的语言特定细节。

现在,关于特定语言的详细信息...

我使用了 class 这个词,但 javascript 并没有 Class 这样的概念。它有对象,就是这样(除了简单类型)。 Javascript 使用原型继承,这意味着它可以有效地定义对象和这些对象上的方法(这是另一个问题的主题;您可以搜索 SO,因为已经有答案。)

按照我们上面的例子,你有 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 的每个实例都有 someMethod 方法,因为 C 的每个实例 "isA" A。

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 方法(由框架提供)完成了设置对象原型的所有繁重工作。

编辑——在你的 cmets 中,你提出了一个很好的问题,即“如果你使用组合,你如何协调主要对象的范围与构成主要对象的对象的范围”。

有很多方法。首先是简单地传递参数。所以

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的实例。

【讨论】:

  • this.b.someMethod 在您的示例中对this 一无所知。它将如何计算总数或列出股票?
  • katspaugh,正确的——我更正了。在没有实际运行的情况下全力以赴......
  • katspaugh,b 上的 someMethod 不知道 A 上的 this,这是真的。但是,您可以编写方法来接受一般参数。如果你想真正进阶,你可以使用 call and apply;我会更新我的答案
  • katspaugh,是的,这是真的,你需要根据你正在处理的实际实现来决定什么是适合你的。这就是为什么软件设计不是微不足道的;)
  • Brian,但是当你使用组合时你并没有扩展一个类。继承时,子类与超类有isA 关系。当你作曲时,情况并非如此。它对强类型语言有影响......
【解决方案2】:

... 有人可以用上面的代码给我一个例子来演示 组合和聚合?

乍一看,提供的示例似乎不是最好的 选择以演示 JavaScript 中的组合。 prototype Stock 构造函数的属性仍然是理想的 totallist 两种方法都可以访问任何股票 对象自己的属性。

可以做的是解耦这些方法的实现 从构造函数原型并将它们准确地提供回那里 - 还以另一种形式的代码重用 - Mixins ...

示例:

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”应该会导致 一个不错的方向。

... 如果 OOP 中的很多情况都支持组合,那么如何 大多数使用 JavaScript 的人似乎只使用原型 和继承?

因为他们中的大多数人使用,他们得到什么和/或他们什么 都熟悉。也许应该有更多的知识传播 关于 JavaScript 作为基于委托的语言,还有什么可以 用它来实现。

附录:

这是相关的线程,最近更新并希望对您有所帮助...

【讨论】:

    【解决方案3】:

    我想我可以向您展示如何使用纯 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);
      };
    };
    

    小提琴here.

    【讨论】:

      猜你喜欢
      • 2017-11-23
      • 2012-12-08
      • 2014-08-09
      • 1970-01-01
      • 1970-01-01
      • 2017-01-20
      • 1970-01-01
      • 2015-08-21
      • 2014-05-07
      相关资源
      最近更新 更多