JavaScript 原型链与构造函数继承的缺陷及组合继承方案

作者:闭包旅人

时间:2026-10-25

浏览:213

本文分析 JavaScript 中两种主要继承模式的缺陷:原型链继承导致引用值在实例间共享,构造函数继承则无法复用原型方法。通过对比指出两者局限,并引出结合两者的组合继承模式,该模式利用原型链继承方法、构造函数继承属性,是目前最流行的继承方案。

文章目录

前言

从面向对象编程的角度来看,其核心价值在于代码的复用能力。

通常所说的面向对象三大特性包括封装、继承和多态。

其中,继承是核心所在。通过封装形成类,进而实现继承,并在继承基础上进行重写以体现多态,其根本目的均指向复用。

在 JavaScript 中实现面向对象编程,主要依赖的是基于原型的机制。

这是由该语言的设计决定的,JavaScript 的设计主要受到 Self(一种基于原型的语言)和 Scheme(函数式语言)的影响。

因此,其复用能力源自原型机制。

基于这一认知基础,接下来探讨原型继承的具体实现。

原型链继承

原型继承最直接的实现方式即为原型链继承。

ECMA-262 标准将原型链定义为 ECMAScript 的主要继承方式,其核心思想是通过原型来继承多个引用类型的属性和方法。

以下是原型链继承的代码实现示例:

function SuperType() {
	 this.property = true;
}
function SubType() {
	 this.subproperty = false;
}
SuperType.prototype.getSuperValue = function() {
 return this.property;
};
SubType.prototype.getSubValue = function () {
 return this.subproperty;
};
SubType.prototype = new SuperType(); // 对 SubType 得原型链重新指定,是原型链继承
let instance = new SubType();
console.log(instance.getSuperValue()); // true

关于查找关系的详细说明,若读者不理解,可参考文章《歪理解?原型链中的函数和对象》。

此处通过代码展示具体的指向关系:

上述示例包含一个对象 instance 和两个函数 SuperType 与 SubType。在原型链中,函数可视为“上帝”,而对象则是基础物质。

这种继承关系来源于两个方面:

  • 一是继承自祖先(即遗产);
  • 二是继承自上帝(即天赋)。
//  继承自祖先(遗产)
instance.__proto__ === SubType.prototype // true
SubType.prototype.__proto__ === SuperType.prototype // true
// 继承自上帝(天赋)
SuperType.__proto__  === Function.prototype // true
SubType.__proto__  === Function.prototype // true
SuperType.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true

本文重点不在于讲解原型链本身,而在于指出原型链继承存在的问题。

其主要问题在于:当原型中包含引用值时,这些引用值会在所有实例之间共享。

function SuperType() {
 this.colors = ["red", "blue", "green"];
}
function SubType() {}
SubType.prototype = new SuperType() // 原型链继承
let s1 = new SubType()
let s2 = new SubType()
s1.colors.push("yellow")
console.log(s1.colors) // ['red', 'blue', 'green', 'yellow']
console.log(s2.colors) // ['red', 'blue', 'green', 'yellow']

例如 colors 是一个数组(引用值),当它被 SubType 继承时,使用的是引用传递。如果在实例化后,其中一个实例修改了该数组,将影响其他所有实例。

实际上,业务代码中很少直接这样编写继承:SubType.prototype = new SuperType()。因为原型链继承会导致复用混乱,所以它通常不会单独使用。

构造函数继承

构造函数继承也被称为“盗用构造函数”、“对象伪装”或“经典继承”。

其基本思路是在子类构造函数中,通过 apply() 和 call() 方法调用父类构造函数。

将上一节的例子改造为构造函数继承形式如下:

function SuperType() {
 this.colors = ["red", "blue", "green"];
}
function SubType() {
	SuperType.call(this) // 构造函数继承
}
let s1 = new SubType()
let s2 = new SubType()
s1.colors.push("yellow")
console.log(s1.colors) // ['red', 'blue', 'green', 'yellow']
console.log(s2.colors) // ['red', 'blue', 'green']

这种方式完美解决了原型链继承中引用值共享的问题,但它也存在自身的问题,即使用构造函数模式自定义类型时产生的问题。

具体表现为:必须在构造函数中定义方法(若在原型上定义,子类无法访问),导致函数无法重用。

function SuperType() {
}
function SubType() {
	SuperType.call(this) // 构造函数继承
}
SuperType.prototype.fn = ()=>{}
let s1 = new SubType()
console.log(s1.fn) // undefined
function SuperType() {
	 this.fn=()=>{}
}
function SubType() {
	SuperType.call(this) // 构造函数继承
}
let s1 = new SubType()
let s2 = new SubType()
console.log(s1.fn === s2.fn) // false

而在原型链继承中,方法是可以重用的。

function SuperType() {}
function SubType() {}
SuperType.prototype.fn = ()=>{}
SubType.prototype = new SuperType() // 原型链继承
let s1 = new SubType()
console.log(s1.fn) // ()=>{}
function SuperType() {
	 this.fn=()=>{}
}
function SubType() {}
SubType.prototype = new SuperType() // 原型链继承
let s1 = new SubType()
let s2 = new SubType()
console.log(s1.fn === s2.fn) // true

综上所述,原型链继承和构造函数继承的“毛病”分别是:

  • 原型链继承:所有继承的属性和方法都会在对象实例间共享,无法实现实例私有化。
  • 构造函数继承:子类无法访问父类原型上的方法。

这种机制确实显得较为别扭。

因此,一种能够规避两者缺陷的继承方式应运而生:组合继承。

组合继承

目前,组合继承是最流行的继承模式。

其思路是:使用原型链继承原型上的属性和方法,同时通过构造函数继承实例属性。

function SuperType(name){
	 this.name = name;
	 this.colors = ["red", "blue", "green"];
}
function SubType(name, age){
	 SuperType.call(this, name) // 构造函数继承
	 this.age = age;
}
SuperType.prototype.sayName = function() {
	 console.log(this.name);
}
SubType.prototype = new SuperType() // 原型链继承
SubType.prototype.sayAge = function() {
	 console.log(this.age);
}
let s1 = new SubType("Nicholas", 29)
let s2= new SubType("Greg", 27)
s1.colors.push("yellow")
console.log(s1.colors) // ['red', 'blue', 'green', 'yellow']
console.log(s2.colors) // ['red', 'blue', 'green']
s1.sayName() // Nicholas
s2.sayName() // Greg
s1.sayAge() // 29
s2.sayAge() // 27

总结来说,组合继承即通过构造函数继承属性(尤其是引用值),而通过原型链继承需要复用的公共方法。

JavaScript 的继承机制确实有些特殊,它并非纯粹的面向对象语言,却通过原型链模拟面向对象特性,其中有许多细节需要注意。(相比之下,函数式编程更为清晰。)

以上就是 JavaScript 原型链继承及构造函数继承问题纠正的详细内容,更多关于 JavaScript 原型链继承构造函数继承的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章