[JavaScript] JavaScript 面向对象设计 (2): 继承篇

身为一个面向对象的程序开发人员,应该不会不知道继承 (inheritance) 是什么吧,它可以让子类拥有父类的完整功能,并透过 private/protected/internal 等修饰子 (modifier) 做封装的保护,子类也可以存取父类的资源,子类也可以选择允许或不允许给其他对象继承等等,若是想要在不修改原本对象的情况下扩充原有功能,继承是一个好方法。


身为一个面向对象的程序开发人员,应该不会不知道继承 (inheritance) 是什么吧,它可以让子类拥有父类的完整功能,并透过 private/protected/internal 等修饰子 (modifier) 做封装的保护,子类也可以存取父类的资源,子类也可以选择允许或不允许给其他对象继承等等,若是想要在不修改原本对象的情况下扩充原有功能,继承是一个好方法。

例如,现在我手上有一个 MathBase 对象 (Mathbase.js),它的对象声明是这样的:

   1:  function MathBase(a, b) {
   2:   
   3:      var _a = a;
   4:      var _b = b;
   5:   
   6:      this.a = _a;
   7:      this.b = _b;
   8:   
   9:      this.add = function () { return this.a + this.b; };
  10:      this.minus = function () { return this.a - this.b; };
  11:   
  12:  }

其调用方式为:

   1:  function displayAdd() {
   2:   
   3:      var math = new MathBase(1, 2);
   4:      document.write("1 + 2 = " + math.add());
   5:      document.write("
");
   6:   
   7:  }
   8:   
   9:   
  10:  function displayMinus() {
  11:   
  12:      var math = new MathBase(1, 2);
  13:      document.write("1 - 2 = " + math.minus());
  14:      document.write("
");
  15:   
  16:  }

执行结果为:

image

今天,我想要在不改变 Math.js 的情况下扩充它的功能,这时除了用 CP 大法 (copy/paste) 以外,我们还可以多用一样东西:继承。只是在 JavaScript 中,继承的作法和以往的方式完全不同。原有的 MathBase 只有加和减,现在要加上乘和除,我们新增一个 MathV1,声明如下:

   1:  MathV1.prototype = new MathBase();
   2:   
   3:  function MathV1(a, b) {
   4:   
   5:      MathV1.prototype.a = a;
   6:      MathV1.prototype.b = b;
   7:   
   8:      console.log(this);
   9:   
  10:      this.multiply = function () {
  11:          return MathV1.prototype.a * MathV1.prototype.b;
  12:      };
  13:   
  14:      this.divide = function () {
  15:          return MathV1.prototype.a / MathV1.prototype.b;
  16:      };
  17:   
  18:  }

由于 MathV1 要继承 MathBase 的功能,在 JavaScript 中,我们可以使用 [Object].prototype 属性来做这件事,它的意思是这个对象的原型是什么,所以在第一行中,先定义出 MathV1 的原型是来自 MathBase,让它们有父子关系,这时 Math.prototype 会是 MathBase 的执行个体 (若没有明确设定,默认值会是对象本身),那么我们就可以透过 MathV1.prototype 来操作父类中的对象,包括修改其属性,或是调用方法等等都可以。由于要共用在 MathBase 中声明的属性,所以我们在函数中使用的是 MathV1.prototype 来存取声明在父类的 a 和 b 属性。

这时,我们将原本的调用程序由 MathBase 改成 MathV1,如下:

   1:   
   2:  function displayAdd() {
   3:   
   4:      var math = new MathV1(1, 2);
   5:      document.write("1 + 2 = " + math.add());
   6:      document.write("
");
   7:   
   8:  }
   9:   
  10:   
  11:  function displayMinus() {
  12:   
  13:      var math = new MathV1(1, 2);
  14:      document.write("1 - 2 = " + math.minus());
  15:      document.write("
");
  16:   
  17:  }
  18:   
  19:  function displayMultiply() {
  20:   
  21:      var math = new MathV1(1, 2);
  22:      document.write("1 * 2 = " + math.multiply());
  23:      document.write("
");
  24:   
  25:  }
  26:   
  27:   
  28:  function displayDivide() {
  29:   
  30:      var math = new MathV1(1, 2);
  31:      document.write("1 / 2 = " + math.divide());
  32:      document.write("
");
  33:   
  34:  } 
  35:      
  36:      

然后在浏览器中使用:

image

image

你会发现结果一和结果二都可以正常运行,而且变量是共用的。

Object.prototype 还有几个不同的用法,像是:

1. 调用对象内指定的方法和属性,但仅能针对公开属性和方法调用,对于私有成员不行。
2. 更改构造函数内容 (Object.prototype.constructor)。
3. 扩充现有对象,在不使用本文手法之下,这部分可参考:http://www.javascriptkit.com/javatutors/oopjs2.shtml。

经过以上讨论,你会发现 JavaScript 的继承不像是 C# 或 Java 这样的,由对象本体 (object context) 去继承,而是透过函数的方式产生的效果,也就是使用 prototype 属性取代 base (JavaScript 中没有 base 属性),同时 prototype 又允许 JavaScript 自由扩充对象,我们后面的几个特殊功能会利用到这个特性。

Reference:

http://www2.stat.unibo.it/palareti/studenti/linguaggi/jsobj/index.htm

http://webreflection.blogspot.com/2009/06/wait-moment-javascript-does-support.html