组合继承:JavaScript 中最常用的继承模式解析

🤍 前端开发工程师、技术日更博主、已过CET6
🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1
🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》、《前端求职突破计划》
🍚 蓝桥云课签约作者、上架课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入门到实战全面掌握 uni-app》
文章目录
一、组合继承的定义与核心思路
在 JavaScript 面向对象编程中,组合继承(Combination Inheritance)是一种融合了 原型链继承 和 构造函数继承 的继承模式,旨在解决传统原型链继承的缺陷(如引用类型属性共享、无法在子类实例化时向父类传参等问题),同时保留原型链继承的优势(方法复用)。其核心思路是:
- 通过原型链继承父类原型上的属性和方法(实现方法复用)。
- 通过构造函数继承父类构造函数中的属性(避免引用类型共享问题,支持传参初始化)。
二、传统原型链继承的缺陷回顾
在理解组合继承之前,先回顾原型链继承的两大痛点:
2.1 引用类型属性共享问题
function Parent() {
this.friends = ['Alice', 'Bob']; // 引用类型属性
}
function Child() {}
Child.prototype = new Parent(); // 原型链继承
const child1 = new Child();
child1.friends.push('Charlie'); // 修改引用类型属性
const child2 = new Child();
console.log(child2.friends); // 输出 ["Alice", "Bob", "Charlie"](共享导致副作用)
问题本质:子类原型是父类实例,原型中的引用类型属性会被所有子类实例共享。
2.2 无法在子类实例化时向父类传参
原型链继承中,子类原型一旦绑定父类实例(如 Child.prototype = new Parent()),父类构造函数的参数就被固定,无法在创建子类实例时动态传递参数。
三、组合继承的实现步骤与代码示例
组合继承通过 两次调用父类构造函数 解决上述问题:
- 在子类原型链绑定父类原型之前,先在子类构造函数中通过
call/apply调用父类构造函数,初始化父类属性(构造函数继承)。 - 通过原型链继承父类原型上的方法(原型链继承)。
3.1 实现代码
// 父类
function Parent(name, age) {
this.name = name;
this.age = age;
this.friends = ['Alice', 'Bob']; // 实例属性(非共享)
}
// 父类原型方法(复用)
Parent.prototype.sayName = function() {
console.log(`姓名:${this.name}`);
};
// 子类
function Child(name, age, grade) {
// 第一步:构造函数继承(解决属性共享和传参问题)
Parent.call(this, name, age); // 调用父类构造函数,传递参数
this.grade = grade; // 子类独有属性
}
// 第二步:原型链继承(继承父类原型方法)
Child.prototype = Object.create(Parent.prototype); // 创建原型链
Child.prototype.constructor = Child; // 修正 constructor 指向
// 子类原型方法(扩展或重写)
Child.prototype.sayGrade = function() {
console.log(`年级:${this.grade}`);
};
// 使用示例
const child = new Child('小明', 12, '六年级');
child.friends.push('Charlie'); // 操作子类实例的属性,不影响其他实例
console.log(child.friends); // 输出 ["Alice", "Bob", "Charlie"]
const anotherChild = new Child('小红', 11, '五年级');
console.log(anotherChild.friends); // 输出 ["Alice", "Bob"](属性独立)
child.sayName(); // 姓名:小明(继承父类方法)
child.sayGrade(); // 年级:六年级(子类方法)
3.2 核心原理分析
-
构造函数继承的作用:
- 通过
Parent.call(this, name, age),在子类实例化时,父类构造函数会在子类实例的作用域中执行,将父类的属性(如name、age、friends)直接挂载到子类实例上,确保每个子类实例都有独立的属性副本,避免引用类型共享问题。 - 支持在创建子类实例时向父类传递参数(如
name、age),灵活初始化属性。
- 通过
-
原型链继承的作用:
- 通过
Object.create(Parent.prototype),将子类原型指向父类原型的一个副本,子类实例可以通过原型链访问父类原型上的方法(如sayName),实现方法复用,避免重复定义。
- 通过
四、组合继承的优缺点
4.1 优点
- 完美解决原型链继承的缺陷:
- 实例属性独立(非共享),避免引用类型冲突。
- 支持子类实例化时向父类传参,灵活初始化属性。
- 保留原型链继承的优势:
- 父类原型方法可被所有子类实例共享,节省内存空间。
- 代码结构清晰:通过构造函数和原型链的分工,清晰分离了属性初始化和方法复用的逻辑。
4.2 缺点
- 父类构造函数被调用两次:
- 第一次在
Child.prototype = Object.create(Parent.prototype)时,创建父类实例作为子类原型(此时会调用Parent构造函数,生成一个“无用”的父类实例)。 - 第二次在子类构造函数中通过
Parent.call(this)初始化子类实例的属性。 - 虽然会生成一个无用的父类实例,但该实例仅用于原型链绑定,不会被外部引用,对性能影响较小,属于可接受的权衡。
- 第一次在
五、组合继承与 ES6 类继承的关系
ES6 的 class 语法虽然使用更简洁的 extends 和 super 实现继承,但底层原理本质上是 组合继承的语法糖:
- 构造函数继承:在子类
constructor中必须通过super()调用父类构造函数,确保父类属性正确初始化(类似组合继承中的Parent.call(this))。 - 原型链继承:子类原型通过
Object.setPrototypeOf(Child.prototype, Parent.prototype)绑定父类原型(类似组合继承中的Object.create)。
// ES6 类继承的底层模拟(组合继承思路)
class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() { ... }
}
class Child extends Parent {
constructor(name, age, grade) {
super(name, age); // 构造函数继承
this.grade = grade;
}
sayGrade() { ... }
}
// 等价于组合继承实现
function Child(name, age, grade) {
Parent.call(this, name, age); // 构造函数继承
this.grade = grade;
}
Child.prototype = Object.create(Parent.prototype); // 原型链继承
Child.prototype.constructor = Child;
六、组合继承的适用场景
组合继承是 JavaScript 中最常用的继承模式,适用于:
- 需要复用父类方法(通过原型链)。
- 需要子类实例拥有独立的属性副本(避免共享问题)。
- 需要在子类实例化时向父类传递参数。
典型应用场景:
- 开发框架中的类层次结构(如 UI 组件库中基类与子类的继承)。
- 封装具有复杂属性和方法的对象(如游戏中的角色继承、业务模型继承等)。
七、总结
组合继承通过融合构造函数继承和原型链继承的优势,在 JavaScript 中实现了一种相对完美的继承方案。尽管存在父类构造函数被调用两次的“冗余”,但在实际开发中,其带来的代码可维护性和性能平衡使其成为首选。理解组合继承的底层逻辑,能帮助开发者更好地掌握 JavaScript 面向对象编程的核心机制,尤其是在处理 ES5 传统继承或深入理解 ES6 类继承原理时具有重要意义。
更多推荐


所有评论(0)