选定基类后,就可以创建它的子类了。是否使用基类完全由你决定。有时,你可能想创建一个不能直接使用的基类,它只是用于给子类提供通用的函数。在这种情况下,基类被看作抽象类。
尽管ECMAScript并没有像其他语言那样严格地定义抽象类,但有时它的确会创建一些不允许使用的类。通常,我们称这种类为抽象类。
创建的子类将继承超类的所有属性和方法,包括构造函数及方法的实现。记住,所有属性和方法都是公用的,因此子类可直接访问这些方法。子类还可添加超类中没有的新属性和方法,也可以覆盖超类中的属性和方法。
4.2.1 继承的方式
和其他功能一样,ECMAScript中实现继承的方式不止一种。这是因为JavaScript中的继承机制并不是明确规定的,而是通过模仿实现的。这意味着所有的继承细节并非由解释程序处理。作为开发者,你有权决定最适用的继承方式。
1. 对象冒充
构想原始的ECMAScript时,根本没打算设计对象冒充(object masquerading)。它是在开发者开始理解函数的工作方式,尤其是如何在函数环境中使用this关键字后才发展出来的。
其原理如下:构造函数使用this关键字给所有属性和方法赋值(即采用类声明的构造函数方式)。因为构造函数只是一个函数,所以可使ClassA的构造函数成为ClassB的方法,然后调用它。ClassB就会收到ClassA的构造函数中定义的属性和方法。例如,用下面的方式定义ClassA和ClassB:
还记得吗?关键字this引用的是构造函数当前创建的对象。不过在这个方法中,this指向的是所属的对象。这个原理是把ClassA作为常规函数来建立继承机制,而不是作为构造函数。如下使用构造函数ClassB可以实现继承机制:
在这段代码中,为ClassA赋予了方法newMethod(记住,函数名只是指向它的指针)。然后调用该方法,传递给它的是ClassB构造函数的参数sColor。最后一行代码删除了对ClassA的引用,这样以后就不能再调用它。
所有的新属性和新方法都必须在删除了新方法的代码行后定义。否则,可能会覆盖超类的相关属性和方法:
为证明前面的代码有效,可以运行下面的例子:
有趣的是,对象冒充可以支持多重继承。也就是说,一个类可以继承多个超类。用UML表示的多继承机制如图4-2所示。
图 4-2
例如,如果存在两个类ClassX和ClassY,ClassZ想继承这两个类,可以使用下面的代码:
这里存在一个弊端,如果ClassX和ClassY具有同名的属性或方法,ClassY具有高优先级,因为继承的是最后的类。除这点小问题之外,用对象冒充实现多继承机制轻而易举。
由于这种继承方法的流行,ECMAScript的第三版为Function对象加入了两个新方法,即call()和apply()。
2. call()方法
call()方法是与经典的对象冒充方法最相似的方法。它的第一个参数用作this的对象。其他参数都直接传递给函数自身。例如:
在这个例子中,函数sayColor()在对象外定义,即使它不属于任何对象,也可以引用关键字this。对象obj的color属性等于"red"。调用call()方法时,第一个参数是obj,说明应该赋予sayColor()函数中的this关键字值是obj。第二个和第三个参数是字符串。它们与sayColor()函数中的参数prefix和suffix匹配,最后生成的消息"The color is red, a very nice color indeed"将被显示出来。
要与继承机制的对象冒充方法一起使用该方法,只需将前三行的赋值、调用和删除代码替换即可:
这里,想让ClassA中的关键字this等于新创建的ClassB对象,因此this是第一个参数。第二个参数sColor对两个类来说都是唯一的参数。
3. apply()方法
apply()方法有两个参数,用作this的对象和要传递给函数的参数的数组。例如:
这个例子与前面的例子相同,只是现在调用的是apply()方法。调用apply()方法时,第一个参数仍是obj,说明应该赋予sayColor()中的this关键字值是obj。第二个参数是由两个字符串构成的数组,与sayColor()的参数prefix和suffix匹配。生成的消息仍是"The color is red, a very nice color indeed",将被显示出来。
该方法也用于替换前三行的赋值、调用和删除新方法的代码:
同样的,第一个参数仍是this。第二个参数是只有一个值color的数组。可以把ClassB的整个arguments对象作为第二个参数传递给apply()方法:
当然,只有超类中的参数顺序与子类中的参数顺序完全一致时才可以传递参数对象。如果不是,就必须创建一个单独的数组,按照正确的顺序放置参数。此外,还可使用call()方法。
4. 原型链
继承这种形式在ECMAScript中原本是用于原型链的。上一章介绍了定义类的原型方式。原型链扩展了这种方式,以一种有趣的方式实现继承机制。
在上一章中学过,prototype对象是个模板,要实例化的对象都以这个模板为基础。总而言之,prototype对象的任何属性和方法都被传递给那个类的所有实例。原型链利用这种功能来实现继承机制。
如果用原型方式重定义前面例子中的类,它们将变为下列形式:
原型链的神奇之处在于突出显示的代码行。这里,把ClassB的prototype属性设置成ClassA的实例。这很有意义,因为想要ClassA的所有属性和方法,但又不想逐个将它们赋予ClassB的prototype属性。还有比把ClassA的实例赋予prototype属性更好的方法吗?
注意,调用ClassA的构造函数时,没有给它传递参数。这在原型链中是标准做法。要确保构造函数没有任何参数。
オブジェクトの偽装と同様に、サブクラスのすべての属性とメソッドは、プロトタイプ属性が割り当てられた後に表示される必要があります。これは、プロトタイプ属性の前に割り当てられたすべてのメソッドが削除されるためです。なぜ?プロトタイプ属性が新しいオブジェクトに置き換えられるため、新しいメソッドが追加された元のオブジェクトは破棄されます。したがって、name 属性と SayName() メソッドを ClassB クラスに追加するコードは次のようになります。
次の例を実行して、このコードをテストできます:
さらに、instanceof 演算子はプロトタイプ チェーン内で独自の方法で動作します。 ClassB のすべてのインスタンスについて、instanceof は ClassA と ClassB の両方に true を返します。例:
これは ECMAScript の弱い型指定の世界では非常に便利なツールですが、オブジェクトの偽装を使用する場合には使用できません。
プロトタイプ チェーンの欠点は、多重継承をサポートしていないことです。プロトタイプ チェーンは、クラスのプロトタイプ プロパティを別の型のオブジェクトでオーバーライドすることに注意してください。
5.混合方法
この継承メソッドはコンストラクターを使用してクラスを定義し、プロトタイプを使用しません。オブジェクトの偽装に関する主な問題は、(前の章で学習したように) コンストラクター アプローチを使用する必要があることですが、これは最良のオプションではありません。ただし、プロトタイプ チェーンを使用する場合は、パラメーター化されたコンストラクターを使用できません。開発者はどのように選択すればよいでしょうか?答えは簡単です。両方を使用してください。
前の章では、クラスを作成する最良の方法は、コンストラクターを使用してプロパティを定義し、プロトタイプを使用してメソッドを定義することであることを学びました。このメソッドは継承メカニズムにも適用され、オブジェクトを使用してコンストラクターのプロパティを継承するふりをし、プロトタイプ チェーンを使用してプロトタイプ オブジェクトのメソッドを継承します。これら 2 つのメソッドを使用して前の例を書き直すと、コードは次のようになります:
この例では、継承メカニズムは強調表示されている 2 行のコードによって実装されています。最初の強調表示されたコード行の ClassB コンストラクターでは、オブジェクトが ClassA クラスの sColor プロパティを継承するふりをするために使用されています。 2 番目の強調表示されたコード行では、ClassA クラスのメソッドがプロトタイプ チェーンを使用して継承されます。このハイブリッド アプローチではプロトタイプ チェーンが使用されるため、instanceof 演算子は引き続き正しく動作します。
次の例はこのコードをテストします: