JavaScript 继承机制详解:从原型链到组合模式

作者:代码拾荒时间:2026-10-23 06:23:39浏览:594

本文深入解析 JavaScript 的继承机制。首先回顾原型与原型链基础,详细对比 ES5 中的组合继承与寄生组合继承实现及优劣。接着分析 ES6 Class 经 Babel 编译后的代码逻辑,揭示其基于寄生组合继承并增强静态方法继承的原理。最后探讨继承带来的耦合与大猩猩香蕉问题,并推荐通过组合模式实现更灵活、低耦合的代码复用。

原型基础回顾

原型

继承机制依赖于原型来实现。虽然原型并非本文核心,但有必要进行简要复习。 原型的概念相对直观:

  1. 所有对象均具备一个 __proto__ 属性,该属性指向另一个对象,即原型。
  2. 每个对象的原型可通过 constructor 属性追溯至构造函数,反之,构造函数也可通过 prototype 属性找到对应的原型。
  3. 所有函数均可通过 __proto__ 属性关联到 Function 对象。
  4. 所有对象均可通过 __proto__ 属性关联到 Object 对象。
  5. 对象之间通过 __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 中最先更新,有兴趣的可以关注下。今年主要会着重写以下三个专栏

  1. 重学 JS
  2. React 进阶
  3. 重写组件

以上所述是小编给大家介绍的JS继承详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!

作者最新文章