JavaScript中的组合、继承和聚合

Composition, Inheritance, and Aggregation in JavaScript

本文关键字:继承 组合 JavaScript      更新时间:2023-09-26

网上有很多关于组合与继承的信息,但我还没有找到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合成的信息,只有其他语言的。

谁能给我一个例子,使用上面的代码来演示组合和聚合?

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

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

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

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

当然,根据语言的不同,你可能无法从两个类扩展一个类(例如,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的每个实例都是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方法(由框架提供)完成了设置对象原型的所有繁重工作。

编辑——在你的评论中,你提出了一个很好的问题,即"如果你使用组合,你如何协调主对象的范围和组成主对象的对象的范围"。

有很多方法。第一种方法是简单地传递参数。所以

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

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

小提琴。

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

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

可以做的是解耦这些方法的实现从构造器原型,并提供他们正好在那里但在另一种形式的代码重用-混合…

的例子:

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"应该会进入A没那么坏的方向。

…如果组合在面向对象的很多情况下都受到青睐,那么如何呢大多数使用JavaScript的人似乎只使用原型和继承?

因为他们中的大多数人使用他们所学到的和/或他们所知道的是熟悉的。也许应该有更多的知识传播关于JavaScript也是基于委托的语言,以及什么可以

附录:

这是相关的线程,最近更新,希望能帮助…

    如何在Javascript中正确使用mixins stackoverflow.com:: Traits in javascript
  • stackoverflow.com::区分组合与继承