JavaScript 的继承机制演进:从原型到 class
前言
JS 的继承核心机制基于原型,而非传统意义上的“类”。传统意义上的类(如 Java 和 C++)是一个静态的“模板”,类需要被实例化才能使用,继承时extends这样的关键字获得其他类的属性和方法。在 JS 中,没有完整的类,所谓的类是被用作构造函数的对象。对象通过原型链直接连接另一个实例对象实现继承,当访问属性时,JS 引擎会沿着原型链逐层向上查找。
每个对象都有隐藏的[[prototype]]属性,它指向对象的原型,可以通过__proto__或者Object.getPrototypeOf()访问。对象的构造函数的prototype属性定义了new操作符创建对象时的默认原型。JS 中的class语法仅仅是原型机制的语法糖。独特的原型机制也影响了开发者进行面向对象编程时继承的方式。
这篇博客主要记录 JS 中常见继承实现方式,了解它们出现的缘由。这篇博客也算是一个笔记,在《JavaScript 高级程序设计》中,详细的介绍了相关内容。
原型链继承
原型链继承(Prototype Chaining)是基于 JS 本身原型机制实现继承的方式。
js- function SuperType () {
- this.superProperty = []
- }
- function SubType () {
- this.subProperty = 'subProperty'
- }
- // 设置原型
- SubType.prototype = new SuperType()
- SubType.prototype.constructor = SubType
- // 创建子类
- const subTypeImpl1 = new SubType()
- const subTypeImpl2 = new SubType()
对象间的关系:

这种方式只需要基于原型链本身的功能,但是这样子的继承方式是存在缺点的:
- 子类实例对象共享原型对象,操作原型上的属性会污染其他子类:
js- subTypeImpl1.superProperty.push(1)
- console.log(subTypeImpl2.superProperty) // [1]
- 原型对象从一开始就被设定了,无法向超类的构造函数传参。
这是一种有明显缺陷的继承方式,一般不会单独使用。
借用构造函数继承
借用构造函数继承(Constructor Stealing)是当时社区中提出的,在子类构造函数中调用超类构造函数实现继承超类属性的方法:
js- function SuperType (id) {
- this.id = id
- }
- function SubType (id, name) {
- // 继承属性
- SuperType.call(this, id)
- // 增强属性
- this.name = name
- this.sayHello = () => console.log('Hello')
- }
- // 创建子类
- const subTypeImpl = new SubType(1, 'test')
对象间的关系:

这种方式可以在调用超类构造函数时传递参数。但是也有明显的缺陷:
- 方法是定义在实例对象上的,每次实例化都需要重新创建,无法重用。
- 只调用了超类构造函数,实例无法访问超类原型:
js- console.log(subTypeImpl.__proto__.__proto__ === Object.prototype) // true
- console.log(subTypeImpl.__proto__.__proto__ === SuperType.prototype) // false
这也是一种有明显缺陷的继承方式,一般不会单独使用。
组合继承
组合继承(Combination Inheritance)或者说伪经典继承(Pseudo-Classical Inheritance)是结合原型链继承和借用构造函数继承的继承方式。它利用借用构造函数继承子对象上继承超类的属性,再利用原型链继承正确设置超类原型。
js- function SuperType (id) {
- this.id = id
- }
- SuperType.prototype.sayHi = () => console.log('Hi')
- function SubType (id, name) {
- // 继承属性
- SuperType.call(this, id)
- // 增强属性
- this.name = name
- }
- // 增强方法
- SubType.prototype.sayHello = () => console.log('Hello')
- // 设置原型
- SubType.prototype = new SuperType()
- SubType.prototype.constructor = SubType
- // 创建子类
- const subTypeImpl = new SubType(1, 'test')
对象间的关系:

这种方式很好地维护了 JS 原型链instanceof和__proto__的特性,又避免了共享原型对象造成数据污染的问题。但是它也存在缺点:
- 超类构造函数被调用 2 次,存在效率问题。
- 超类的属性在子类实例对象和原型链上重复出现,不够优雅。
原型式继承
2006年,Douglas Crockford 在博客文章 Prototypal Inheritance in JavaScript,提出了原型式继承(Prototypal Inheritance)。它是一种利用工厂函数,从一个已有的超类实例对象中创建子类实例的方法。
js- function object(o) {
- function F() {}
- F.prototype = o
- return new F()
- }
- function SuperType (id) {
- this.id = id
- }
- const superTypeImpl = new SuperType(1)
- const subTypeImpl = object(superTypeImpl)
对象间的关系:

原型式继承适合不需要单独创建构造函数,但是需要在对象之间共享数据的场景。它有着原型链继承共享原型对象的缺点。
原型式继承由 ES5 原生支持,可以通过Object.create(proto)实现。
寄生式继承
寄生式继承同样由 Douglas Crockford 提出,详见 Classical Inheritance in JavaScript。它是和原型式继承类似的方式,思路类似寄生构造函数和工厂模式,基本思想是实现一个继承的函数,以某种方法增强对象,最后返回这个对象。这是《JavaScript 高级程序设计》提到的寄生式继承的模式:
js- function inherit (obj) {
- // 继承属性
- const instance = object(obj)
- // 增强对象
- instance.name = name
- instance.sayHello = () => console.log('Hello')
- }
这里的原型式继承的object函数不是必需的,只需要是一个返回对象的函数就可以了。它和原型式继承一样,适用于只关注对象,无需构造函数的情况。这种方式也存在借用构造函数继承在对象上定义方法的缺点。
原生 JS 中Object.create(proto, propertiesObject)中第二个参数可以做到对象增强。
寄生式组合继承
组合式继承存在效率问题,它超类的构造函数需要调用两次。为了解决这个问题,寄生式组合继承使用借用构造函数继承的方式继承超类的属性,使用原型式继承的方式,让子类原型继承超类原型。这样子无需调用两次超类构造函数。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
js- function inheritPrototype (SubType, SuperType) {
- const prototype = object(SuperType.prototype)
- prototype.constructor = SubType
- SubType.prototype = prototype
- }
- function SuperType (id) {
- this.id = id
- }
- SuperType.prototype.sayHi = () => console.log('Hi')
- function SubType (id, name) {
- // 继承属性
- SuperType.call(this, id)
- // 增强属性
- this.name = name
- }
- // 子类原型继承超类原型
- inheritPrototype(SubType, SuperType)
- // 增强方法
- SubType.prototype.sayHello = () => console.log('Hello')
对象间的关系:

这里超类构造函数SuperType只调用了一次,效率更高。而且,像组合继承,它维护了原型链,使得instanceof、Object.getPrototypeOf()这些 API 可以正常使用。
ES6 的继承
ES6 新增了class关键字用于定义类型,用extends进行继承。ES6 的继承机制和寄生式组合继承非常相似。
js- class SuperType {
- constructor (id) {
- this.id = id
- }
- sayHi () {
- console.log('Hi')
- }
- }
- class SubType extends SuperType {
- constructor (id, name) {
- super(id)
- this.name = name
- }
- sayHello () {
- console.log('Hello')
- }
- }
我们来稍微利用 Babel 的转写来窥视一下 ES6 继承的实现原理,这是上面这段代码被 Babel 转写后的结果,省略掉了和继承无关的代码。
js- function _inherits(subClass, superClass) {
- if (typeof superClass !== "function" && superClass !== null) {
- throw new TypeError("Super expression must either be null or a function");
- }
- subClass.prototype = Object.create(
- superClass && superClass.prototype,
- { constructor: { value: subClass, writable: true, configurable: true } }
- );
- Object.defineProperty(subClass, "prototype", { writable: false });
- if (superClass) _setPrototypeOf(subClass, superClass);
- }
- function _setPrototypeOf(o, p) {
- _setPrototypeOf = Object.setPrototypeOf
- ? Object.setPrototypeOf.bind()
- : function _setPrototypeOf(o, p) {
- o.__proto__ = p;
- return o;
- };
- return _setPrototypeOf(o, p);
- }
- // SuperType...
- var SubType = /*#__PURE__*/function (_SuperType) {
- _inherits(SubType, _SuperType);
- // 这里省略掉代码做了一下几件事情:
- // 1. 在实例化时把超类属性加入到子类构造函数中
- // 2. 在子类 prototype 原型中定义方法
- // 3. 返回一个返回实例化对象的函数...
- return SubType;
- }(SuperType);
我们先来梳理一下这段代码的逻辑。
subClass.prototype = Object.create(...),和寄生式组合继承相似,子类原型subClass.prototype继承了超类原型superClass.prototype。_setPrototypeOf(subClass, superClass)子类构造函数的__proto__指向超类构造函数。
我们来看一下这些对象间的关系:

和寄生式组合继承相比,我们可以发现,增加了SubType.__proto__指向SuperType的继承关系。这很容易猜到,这是用来继承静态属性和静态方法的。
此外,在省略掉的代码里,ES6 继承时先设置子类原型上的方法,再对子类进行实例化,和上文中的寄生式组合继承顺序略有差别。
ES6 的继承很大程度上使用了寄生式组合继承的思想,在这之上完善了对静态属性、静态方法的继承。
结语
继承方式的演进历程:
- 原型链继承:最基础的继承方式,但存在共享原型对象和无法传参的问题;
- 借用构造函数继承:解决了属性共享问题,但方法无法复用,无法继承原型方法;
- 组合继承:结合前两者优点,但存在效率问题和属性重复;
- 原型式继承:轻量级方案,适合对象间数据共享,存在共享原型对象的问题;
- 寄生式继承:工厂模式增强版,有方法无法复用的问题;
- 寄生组合式继承:接近最完美的方案,解决了组合继承的效率问题;
- ES6
class继承:语法糖下的寄生组合继承,加入继承静态属性。
JavaScript 的继承机制经历了从探索到成熟的演进过程。在这篇博客,我们复习了多种继承实现方式,每种方法都试图解决前者的缺陷,最终形成了今天广泛使用的模式。
