javaScript-继承的多种方式
2021-08-29·7min
type
Post
summary
status
Published
category
tags
slug
date
Aug 29, 2021
password
icon
在面向对象编程中,为了能够复用方法,在现有的对象基础上扩展新的对象,继承是关键。
JS中没有实际意义上的类,所以JS中的继承是基于原型链来完成的。
虽然ES6中引入了class这个定义类的关键字,但定义出来的类本质上只是一种语法糖结构,背后使用的还是原型链与构造函数的概念。
下面就让我们来了解一下JS中继承的几种方式以及存在的问题。
原型链继承
当查找一个对象中的属性或方法时,如果本身没有,就会沿着原型链一直向上查找。
原型链继承就是基于这个原理。
回想一下构造函数、实例对象以及原型之间的关系:

设想一下,如果原型是另一个类型的实例对象呢?那不就能沿着原型链来获取到想要的属性和方法了吗?
实现原型链继承:
在上方的代码中,将子类SubType的的原型设置成了父类SuperType的一个实例对象。
因此,子类的实例不仅能够从父类的实例中继承属性,还与父类的原型挂上了钩。
原型链继承的关键点就是将原型设置成了新的对象,而这个新的对象恰好是另一个类型的实例。
原型链继承存在的问题
- 引用类型的属性被所有实例共享
我们都知道一个类型的原型是由所有该类型的实例共享的。
当我们使用原型链继承时,类型的原型会被设置成了另一个类型的实例对象。
当这另一个类型的实例对象中含有引用类型的属性时,理所当然的,这个引用类型的属性会被所有实例共享。
看代码:
- 实例化对象时,不能给父类构造函数传参。
- 子类实例对象的constructor属性指向错误。 我们都知道,实例的原型中会有一个constructor属性指回该实例的构造函数。 但由于原型被改写成了父类的实例对象,所以原本原型上的constructor属性会丢失。 所以当想获取这个constructor时,会一直到父类的原型中才能找到,这就导致了constructor属性指向错误。
借用构造函数(经典继承)
原理很简单,就是在子类的构造函数中,调用父类的构造函数。
看代码:
借用构造函数的方式相对于原型链继承来说:
- 解决了引用类型的属性被多个实例共享的问题。
- 解决了不能给父类构造函数传参的问题。
- 解决了constructor属性指向错误的问题。
但是也产生了新的问题:
1.由于只是借用了父类的构造函数,并没有跟父类的原型挂上钩,所以没办法继承父类原型上的属性和方法。
2.如果想让子类继承父类的方法,就必须将方法定义在父类的构造函数中。这么做的后果就是,每一个实例对象的方法都是独立的,无法复用。
组合继承
组合继承其实就是结合了原型链继承与借用构造函数继承。
组合继承融合了原型链继承与借用构造函数继承的优点。
- 解决了借用构造函数继承无法继承父类原型,无法复用方法的缺点。
- 解决了原型链继承中引用类型的属性被多个实例共享的问题。
- 解决了不能给父类构造函数传参的问题。
- 解决了constructor属性指向错误的问题
但是组合继承同样也存在问题:
可以在上方代码中看到,父类构造函数其实是被调用了两次,一个在子类构造函数中被调用,一次被调用作为子类的原型。
这样做产生的问题就是,在子类的实例和原型中,会存在两组相同的属性,这会导致一些效率问题。
原型继承
原型继承的原理就是将一个对象作为一个构造函数的原型,这样新构建出来的实例就能够访问到该对象上的属性和方法。
封装一个函数来实现这个过程:
其实就是ES5中 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
原型继承与原型链继承的问题类似,传入的对象中如果包含引用类型的属性,这个属性会被所有实例共享。
寄生式继承
寄生式继承与原型继承很类似。
即基于一个对象创建新对象,然后再增强这个新对象,最后返回新对象。
通过寄生式继承给对象添加函数会导致函数难以复用,这点与借用构造函数模式类似。
寄生组合式继承
在前面说到,组合式继承的问题就是父类的构造函数被调用了两次,在实例与原型中会有相同的两组数据。
那么如果不调用两次父类构造函数,直接将子类的原型与父类的原型相关联,问题不就解决了吗?
封装一个函数,使子类的原型间接继承父类的原型,并修正constructor的指向。
将上面封装好的函数结合组合继承:
相比组合式继承,寄生组合式继承只调用了一次父类构造函数,避免了SubType.prototype上不必要也用不到的属性,因此效率更高。
而且原型链仍然保持不变,还能够正常使用 instanceof 和 isPrototypeOf。
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
ES6的继承
在ES6的class中,原生支持了继承的机制,虽然使用的是新语法,但本质上,还是基于原型链实现的继承。
class使用extends关键字,就可以继承任何拥有原型以及[[Construct]]的对象,这意味着,class不仅可以继承class,还可以继承构造函数。
使用了extends关键字的类,可以称之为派生类。
在派生类的方法中,可以使用super关键字来引用它们的原型。
需要注意的是,super关键字只可以在派生类中使用,并且只限于类构造函数,类静态方法。
1.在类构造函数中调用super关键字
在类构造函数中调用super关键字相当于调用了父类的构造函数。
这类似于盗用构造函数模式。
2.在类静态方法中调用super关键字
在静态方法中可以通过调用super关键字来调用继承的父类上的静态方法。
使用super时要注意的问题
- super关键字只能够在派生类的类构造函数以及类静态方法中调用。
- 不能够单独引用super关键字,要么让它在类构造函数中执行,要么让它引用父类静态方法。
- 当派生类中没有定义类构造函数,实例化化派生类时,会自动在派生类中调用super(),并传入参数。
- 在派生类构造函数中,不能在调用super关键字之前引用this
5.如果在派生类中显示定义了类构造函数,则要么在类构造函数中调用super关键字,要么返回一个对象。
总结
以上就是关于JS继承的几种方式,本文从简单的继承模式开始进行分析,并总结各种模式的不足以及后续的改进。
虽然很多继承模式在实际的开发工作中很少能够用到,但是想成为一个合格的JS开发者是必须要了解的。
参考
JS高级程序设计(第四版)第8章