本文详细解析JavaScript中的组合继承模式,结合原型链与构造函数继承的优势。通过对比原型链继承和构造函数继承的局限性,阐述组合继承如何实现属性独立与方法复用,并指出其调用两次父类构造函数及内存占用等缺点,帮助开发者深入理解ES5面向对象编程的核心机制。
1、前言
在深入探讨继承机制之前,读者需具备对原型链的基本认知。
若对原型链尚不熟悉,建议先阅读《JavaScript 原型链详解》一文,其中对原型链机制有较为详尽的阐述。
若已掌握相关基础知识,请继续阅读后续内容。
此前曾有一篇博文列举了所有继承方式,但篇幅过长不利于知识吸收。因此,本文将组合继承部分单独拆分出来进行详解,后续章节将补充寄生继承的相关内容。
2、原型链继承
该模式的核心在于将父类的实例作为子类的原型。具体而言,子类创建的两个实例的隐式原型__proto__指向父类的一个实例,而该父类实例的隐式原型__proto__又指向父类的原型father.prototype。基于原型链的查找机制,所有子类实例均可访问并继承父类原型上定义的属性。
可参考以下图示,配合代码进行理解:

//父类
function father() {
this.fatherAttr = ["fatherAttr"];
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
//子类
function child() {}
// 将father实例作为child这个构造函数的原型
child.prototype = new father();
child.prototype.constructor = child;
//两个子类实例
const test1 = new child();
const test2 = new child();
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);

特点分析:
- 尽管两个实例对象自身均没有
fatherAttr属性,但由于父类实例拥有fatherAttr属性,且该父类实例被用作child的原型,根据原型链查找规则,实例可通过构造函数child的原型访问到该属性(见测试1)。 - 由于所有实例共享同一个作为原型的父类实例,因此它们共享原型上的属性
fatherAttr。当该属性为引用类型(如数组)时,若test1添加新内容,将导致test2上的fatherAttr属性也随之改变(见测试2)。这是原型链继承的主要缺点之一。 - 此外,
child构造函数在创建实例时无法直接传递参数(见测试2的隐含限制),这是另一个缺点。 - 不过,实例可以访问到父类原型上的属性,因此可将可复用的方法定义在父类原型上(见测试3)。
3、构造函数继承
该模式通过将父类上的this绑定到子类来实现。即在子类创建实例时,在子类内部调用父类的构造函数,从而将父类的属性拷贝到子类实例中,使实例继承这些属性。
//父类
function father(params) {
this.fatherAttr = ["fatherAttr"];
this.params = params;
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
//子类
function child(params) {
father.call(this, params);
}
//两个子类实例
const test1 = new child("params1");
const test2 = new child("params2");
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);

特点分析:
- 两个实例对象均拥有拷贝来的
fatherAttr属性,因此不存在属性共享问题。然而,每创建一个实例都需要拷贝一次父类的所有属性,且由于无法继承父类原型,方法无法复用,被迫进行方法拷贝(见测试1)。这是构造函数继承的主要缺点。 - 若
test1添加新内容,仅改变test1自身的属性,不会影响test2(见测试2)。 child构造函数支持传递参数,允许定制实例属性(见测试1)。- 然而,实例无法继承父类原型上的属性(见测试3),这是其另一大缺点。
4、组合继承
组合继承结合了原型链继承和构造函数继承的优点,旨在利用两种模式的特点进行互补使用。
//父类
function father(params) {
this.fatherAttr = ["fatherAttr"];
this.params = params;
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
//子类
function child(params) {
//第二次调用了父类构造函数
father.call(this, params);
}
// 将father实例作为child构造函数的原型
child.prototype = new father();//第一次调用了父类构造函数
child.prototype.constructor = child;
//两个实例
const test1 = new child("params1");//从这里跳转去子类构造函数第二次调用了父类构造函数
const test2 = new child("params2");
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);
console.log("测试4:");
delete test1.fatherAttr
console.log("test1:", test1);
console.log("test1.fatherAttr:", test1.fatherAttr);

特点分析:
- 两个实例对象均拥有拷贝来的
fatherAttr属性,这意味着每创建一个实例需拷贝一次父类所有属性(构造函数继承特点,见测试1)。同时,实例能访问父类原型,可将复用方法定义在父类原型上(原型链继承特点,见测试1)。 - 若
test1添加新内容,仅改变test1自身的属性,不会影响test2(构造函数继承特点,见测试2)。 child构造函数支持传递参数,允许定制实例属性(构造函数继承特点,见测试1)。- 实例能够继承父类原型上的属性(原型链继承特点,见测试3)。
- 然而,该模式调用了两次父类构造函数,生成了两份实例:一次用于创建子类原型链,另一次在通过子类创建具体实例时执行。第二次调用覆盖了第一次,造成资源浪费(见缺点)。
- 由于调用了两次父类构造函数,若使用delete删除实例上拷贝来的
fatherAttr属性,实例仍会通过隐式原型访问到父类实例上的fatherAttr属性(见测试4)。这是原型链继承带来的副作用,也是组合继承的一个缺点。
至此,关于JavaScript组合继承的详解介绍完毕。如需了解更多相关JavaScript组合继承内容,请搜索本站历史文章或继续浏览后续相关文章。感谢大家的支持!