本文深入解析 JavaScript 中的继承机制,涵盖对象冒充、原型链及混合继承三种核心实现方式。详细阐述如何利用构造函数、this 关键字、call 和 apply 方法实现属性继承,以及如何通过原型链实现方法共享。同时对比各方式的优劣,如多重继承支持与 instanceof 运算符的使用,帮助开发者根据实际需求选择最佳继承方案。
JavaScript 继承概述与抽象类
确定基类之后,即可着手创建其子类。是否使用基类完全取决于具体需求。在某些场景下,开发者可能希望构建一个无法直接实例化的基类,仅作为子类通用函数的提供者。此时,该基类被视为抽象类。
尽管 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()。
call() 方法的应用
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 对两个类来说都是唯一的参数。
apply() 方法的应用
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 的构造函数时,没有给它传递参数。这在原型链中是标准做法。要确保构造函数没有任何参数。
与对象冒充相似,子类的所有属性和方法都必须出现在 prototype 属性被赋值之后,因为在它之前赋值的所有方法都会被删除。为什么呢?因为 prototype 属性被替换成了新对象,添加了新方法的原始对象将被销毁。所以,为 ClassB 类添加 name 属性和 sayName() 方法的代码如下:
![]()
![]()
![]()
可以通过运行以下示例来测试这段代码:

此外,在原型链中,instanceof 运算符的运行方式也很独特。对于 ClassB 的所有实例,instanceof 对 ClassA 和 ClassB 都返回 true。例如:
![]()
在 ECMAScript 的弱类型世界中,这是一个极其有用的工具,不过在使用对象冒充时不能使用它。
原型链的弊端是不支持多重继承。请记住,原型链会用另一类型的对象重写类的 prototype 属性。
混合继承模式
5. 混合方式
这种继承方式使用构造函数定义类,并未使用任何原型。对象冒充的主要问题是必须使用构造函数方式(如上一章中学到的),这不是最好的选择。不过如果使用原型链,就无法使用带参构造函数了。开发者该如何选择呢?答案很简单,两者都用。
在前一章中,你学过创建类的最好方式是用构造函数方式定义属性,用原型方式定义方法。这种方式同样适用于继承机制,用对象冒充继承构造函数的属性,用原型链继承 prototype 对象的方法。用这两种方式重写前面的例子,代码如下:
![]()


在此例子中,继承机制由两行突出显示的代码实现。在第一行突出显示的代码中,在 ClassB 构造函数中,用对象冒充继承 ClassA 类的 sColor 属性。在第二行突出显示的代码中,用原型链继承 ClassA 类的方法。由于这种混合方式使用了原型链,所以 instanceof 运算符仍能正确运行。
下面的例子测试了这段代码:
