本文深入解析 JavaScript 的继承机制。首先回顾原型与原型链基础,详细对比 ES5 中的组合继承与寄生组合继承实现及优劣。接着分析 ES6 Class 经 Babel 编译后的代码逻辑,揭示其基于寄生组合继承并增强静态方法继承的原理。最后探讨继承带来的耦合与大猩猩香蕉问题,并推荐通过组合模式实现更灵活、低耦合的代码复用。
原型基础回顾
原型
继承机制依赖于原型来实现。虽然原型并非本文核心,但有必要进行简要复习。 原型的概念相对直观:
- 所有对象均具备一个 __proto__ 属性,该属性指向另一个对象,即原型。
- 每个对象的原型可通过 constructor 属性追溯至构造函数,反之,构造函数也可通过 prototype 属性找到对应的原型。
- 所有函数均可通过 __proto__ 属性关联到 Function 对象。
- 所有对象均可通过 __proto__ 属性关联到 Object 对象。
- 对象之间通过 __proto__ 属性相互连接,形成原型链。当当前对象中不存在某属性时,JavaScript 引擎会沿原型链向上查找,直至到达顶层的 Object 对象。
原型的核心内容即上述几点,无需阅读冗长的原型理论文章,这反而容易让初学者困惑。
若需深入了解原型细节,可参考我之前撰写的相关文章。
ES5 中的继承实现
ES5 实现继承
在 ES5 中实现继承主要包含两种方法。此前已有相关论述,此处直接引用。
这部分内容在当下语境中,更多用于应对面试场景。
组合继承
组合继承是应用最为广泛的继承方式。
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
上述继承方式的核心在于:在子类构造函数中通过 Parent.call(this) 继承父类的属性,随后将子类的原型设置为 new Parent() 以继承父类的函数。
该继承方式的优点在于构造函数支持传参,避免了子类与父类引用属性的共享,并能复用父类函数。然而,其缺点在于继承父类函数时调用了父类构造函数,导致子类原型上出现了不必要的父类属性,造成内存浪费。

寄生组合继承
此方式对组合继承进行了优化。组合继承的缺陷在于继承父类函数时调用了构造函数,我们只需消除这一调用即可。
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
上述实现的核心在于将父类的原型赋值给子类,并将构造函数设置为子类。这样既解决了无用父类属性的问题,又能正确定位子类的构造函数。

ES6 Class 的 Babel 编译原理
Babel 如何编译 ES6 Class 的
前文提到 ES5 实现继承多用于面试,原因在于现在可直接使用 class 语法实现继承。
然而,class 属于 ES6 特性,为确保浏览器兼容性,通常需通过 Babel 编译 ES6 代码。接下来我们将分析 Babel 编译后的代码形态。
function _possibleConstructorReturn (self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
function _inherits (subClass, superClass) {
// ...
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Parent = function Parent () {
// 验证是否是 Parent 构造出来的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child () {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
}(Parent));
以上代码展示了编译后的部分结果,隐去了非核心代码。我们先来解析 _inherits 函数。
设置子类原型的代码与寄生组合继承完全一致,这侧面印证了该实现方式的优越性。但此处多了一句 Object.setPrototypeOf(subClass, superClass),其作用在于继承父类的静态方法,此前实现的两种继承方式均不具备此功能。
随后,Child 构造函数的代码与之前的实现方式基本类似。总体而言,Babel 实现继承的方式仍基于寄生组合继承,仅额外增加了继承父类静态方法的步骤。
继承的局限性与组合模式替代方案
继承存在的问题
在探讨了继承的实现方式后,我们需要思考:继承是否总是最佳选择?
总体而言,我个人对继承持保留态度,原因如下。
首先看代码示例。假设我们要描述不同品牌的汽车,汽车作为父类,各品牌汽车作为子类。
class Car {
constructor (brand) {
this.brand = brand
}
wheel () {
return '4 个轮子'
}
drvie () {
return '车可以开驾驶'
}
addOil () {
return '车可以加油'
}
}
Class OtherCar extends Car {}
这段代码在当前视角下看似无误,实现了汽车的基本功能,且可通过子类扩展出各种车型。
然而,随着新能源车的出现,新能源车无需加油功能。尽管其他基本功能仍需保留,但加油功能不再需要。
若新能源车直接继承汽车父类,便会出现第一个问题:大猩猩与香蕉问题。该问题意指我们仅需一根香蕉,却得到了一只握着香蕉的大猩猩。大猩猩本身并非所需,但父类强行将其赋予子类。继承虽允许重写父类方法,却无法选择性继承所需内容。
此外,单个父类难以清晰描述所有场景,这可能导致需要新增多个不同父类以覆盖更多场景。随着扩展进行,代码必然出现重复,这也是继承存在的另一问题。
除上述两点外,继承还存在强耦合问题,无论子类如何变化,均会与父类紧密耦合。
既然存在强耦合,架构必然脆弱。一旦父类设计存在缺陷,将极大影响维护工作。由于所有子类均与父类耦合,修改父类中的任何内容都可能导致需更改所有子类。
如何解决继承的问题
继承主要用于描述“是什么”,若描述不当便会引发各种问题。我们是否有办法解决这些问题?答案是组合。
什么是组合?可将其概念理解为:你拥有各种零件,可通过这些零件组装出各种产品。组合更多用于描述“能做什么”。
现在,我们通过组合方式重新实现之前的汽车案例。
function wheel() {
return "4 个轮子";
}
function drvie() {
return "车可以开驾驶";
}
function addOil() {
return "车可以加油";
}
// 油车
const car = compose(wheel, drvie, addOil)
// 新能源车
const energyCar = compose(wheel, drive)
从上述伪代码中,想必你也发现了组合优于继承之处。无论描述何种对象,均可通过几个函数组合实现。代码简洁且利于复用。
最后
其实这篇文章的主旨还是后面两小节的内容,如果你还有什么疑问欢迎在评论区与我互动。
我所有的系列文章都会在我的 Github 中最先更新,有兴趣的可以关注下。今年主要会着重写以下三个专栏
- 重学 JS
- React 进阶
- 重写组件
以上所述是小编给大家介绍的JS继承详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!