写在前面

我们知道面向对象的编程语言中都会有继承,而且在各种语言中充当着至关重要的角色,但是继承是什么?又有多少种继承的方式?常见的又有哪些呢?对于js继承有更深层次的理解,能够在开发中应对自如。

所谓继承,继承是面向对象的,使用这种方式能够更好的对代码的复用,能够缩短开发周期、提升开发效率。

那么我们带着两个问题阅读文章,在文章解决这些疑惑:

  • JS继承到底有多少种继承方式?
  • ES6中的extends关键字是使用哪种继承方式实现的?

一、继承的概念

我们知道一个人继承祖业,可以将父辈所有的物质基础继承过来,但是自己作为主体又有自己的其它能力和特性。同样的汽车作为一个大类,生产轿车、跑车、面包车等,都具有汽车四个轮子加发动机的特性,但各自具有自己独特的特性,比如五菱宏光可以在秋名山飙车成为车神。

因此,继承可以使得子类具有父类的各种方法和属性。

常见的继承方式有:

  • 原型链继承
  • 构造函数继承
  • 组合继承
  • 原型链继承
  • 寄生式继承
  • 寄生组合式继承

二、继承

2.1 原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例:

  • 每个构造函数都有一个原型对象
  • 原型对象又包含一个指向构造函数的指针
  • 实例则包含一个原型对象的指针
  1. function Person(){
  2. this.name = "person";
  3. this.abilities = ["吃饭","睡觉","打豆豆"];
  4. }
  5. function Student(){
  6. this.study = ["语文","数学","英语"];
  7. }
  8. Student.prototype = new Person();
  9. console.log(new Student());

我们可以看到Student类已经继承了Person类的所有特性:
image.png
但是,我们注意到:当使用同一个对象创建实例的时候,内存空间是共享的,当一个发生变化的时候,另外一个也会随之变化。

  1. function Person(){
  2. this.name = "person";
  3. this.abilities = ["吃饭","睡觉","打豆豆"];
  4. }
  5. function Student(){
  6. this.study = ["语文","数学","英语"];
  7. }
  8. Student.prototype = new Person();
  9. const stu1 = new Student();
  10. const stu2 = new Student();
  11. stu1.abilities.push("走路");
  12. console.log(stu1.abilities,stu2.abilities);

我们看到stu1和stu2都是由Student对象进行创建的两个实例,当改变stu1的值,stu2的值也随之改变了。
image.png

2.2 构造函数继承(借助call)

构造函数继承可以很好的解决原型链继承的共享内存的弊端,但是父类原型对象中存在父类之前自己定义的方法,那么子类将无法继承这些方法,此时去使用父类方法就会报错。

构造函数继承只能继承父类实例、属性和方法,不能继承原型属性和方法。

  1. function Person(){
  2. this.name = "person";
  3. }
  4. Person.prototype.getName = function(){
  5. return this.name;
  6. }
  7. function Student(){
  8. Person.call(this);
  9. this.study = ["语文","数学","英语"];
  10. }
  11. const stu = new Student();
  12. console.log(stu);
  13. console.log(stu.getName())

我们可以看到,打印的stu实例包含了Student对象以及父类Person所有的属性,但是要使用父类原型的方法就会报错。getName是父类Person的引用方法,不会共享内存。
image.png

2.3 组合继承(原型链继承+构造函数继承)

  1. function Person(){
  2. this.name = "person";
  3. this.abilities = ["吃饭","睡觉","打豆豆"];
  4. }
  5. //第一次调用Person()
  6. Person.prototype.getName = function(){
  7. return this.name;
  8. }
  9. function Student(){
  10. //第二次调用Person()
  11. Person.call(this);
  12. this.study = ["语文","数学","英语"];
  13. }
  14. Student.prototype = new Person();
  15. //手动挂载构造器,指向自己的构造函数
  16. Student.prototype.constructor = Student;
  17. const stu1 = new Student();
  18. const stu2 = new Student();
  19. stu1.abilities.push("走路");
  20. console.log(stu1.abilities,stu2.abilities);//不会互相影响
  21. console.log(stu1.getName());//正常输出"person"
  22. console.log(stu2.getName());//正常输出"person"

运行得到:我们看到在stu1实例的abilities数组中追加元素,并不会影响到stu2实例的值。
image.png
我们发现使用组合式继承时,可以有效解决原型链继承和构造函数继承的缺点,但是,调用两次Person(),这就造成了性能开销,那么我们还有没有优化空间呢?

2.4 原型式继承

我们可以利用es5中的Object.create()方法进行原型式继承,从而实现对组合式继承的优化。Object.create()接收两个参数:

  • 用作新对象原型的对象
  • 为新对象定义额外属性的对象(可选参数) ```javascript const person = { name:”person”, abilities:[“吃饭”,”睡觉”,”打豆豆”], getName(){
    1. return this.name;
    } }

const person1 = Object.create(person); person1.name = “human”; person1.abilities.push(“走路”);

const person2 = Object.create(person); person2.abilities.push(“跑步”);

console.log(person1.name); console.log(person1.name === person1.getName()); console.log(person2.name); console.log(person1.abilities); console.log(person2.abilities);

  1. 我们可以看到使用Object.create()可以实现普通对象继承,不仅可以继承属性,还能继承方法。但是也有缺点:包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22745085/1638549928417-d8665359-83a6-4f86-9fdc-b761a3dc640d.png#clientId=u4a9bf4da-9d11-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=119&id=u8c7b1f76&margin=%5Bobject%20Object%5D&name=image.png&originHeight=238&originWidth=870&originalType=binary&ratio=1&rotation=0&showTitle=false&size=37039&status=done&style=none&taskId=uf2c97a0b-ef5c-4cab-9cad-f9e038da1ec&title=&width=435)<br />修改`person1.name`的值,person2.name的值并未发生改变,并不是因为person1和person2有独立的 name 值,而是因为person1.name = 'human',给person1添加了 name 值,并非修改了原型上的 name 值。
  2. <a name="cZP1z"></a>
  3. ### 2.5 寄生式继承
  4. 寄生式继承:首先使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法。
  5. 寄生式继承相比于原型式继承,还是在父类基础上添加了更多方法。
  6. ```javascript
  7. function clone(original){
  8. const clone = Object.create(original);
  9. clone.getAbilities = function(){
  10. return this.abilities;
  11. }
  12. return clone;
  13. }
  14. const person = {
  15. name:"person",
  16. abilities:["吃饭","睡觉","打豆豆"],
  17. getName(){
  18. return this.name;
  19. }
  20. }
  21. const person1 = clone(person);
  22. console.log(person1.getName());
  23. console.log(person1.getAbilities());

运行得到:
image.png

2.6 寄生组合式继承

前面分析了五种常见的继承方法,现在综合所有方式的优缺点,可以进行优化改造得到寄生组合式的继承方式,这也是所有继承方式中相对最优的。

  1. function clone(parent,child){
  2. //这里使用Object.create()可以减少组合继承中多进行一次构造函数的过程
  3. child.prototype = Object.create(parent.prototype);
  4. child.prototype.constructor = child;
  5. }
  6. function Parent(){
  7. this.name = "parent";
  8. this.abilities = ["吃饭","睡觉","打豆豆"];
  9. }
  10. Parent.prototype.getName = function(){
  11. return this.name;
  12. }
  13. function Child(){
  14. Parent.call(this);
  15. this.study = ["语文","数学","英语"];
  16. }
  17. clone(Parent,Child);
  18. Child.prototype.getStudy =function(){
  19. return this.study;
  20. }
  21. const child = new Child();
  22. console.log(child);
  23. console.log(child.getName());
  24. console.log(child.getStudy());

运行得到:
image.png

extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法,使用例子如下。

  1. class Rectangle {
  2. // constructor
  3. constructor(height, width) {
  4. this.height = height;
  5. this.width = width;
  6. }
  7. // Getter
  8. get area() {
  9. return this.calcArea()
  10. }
  11. // Method
  12. calcArea() {
  13. return this.height * this.width;
  14. }
  15. }
  16. const rectangle = new Rectangle(10, 20);
  17. console.log(rectangle.area);
  18. // 输出 200
  19. -----------------------------------------------------------------
  20. // 继承
  21. class Square extends Rectangle {
  22. constructor(length) {
  23. super(length, length);
  24. // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
  25. this.name = 'Square';
  26. }
  27. get area() {
  28. return this.height * this.width;
  29. }
  30. }
  31. const square = new Square(10);
  32. console.log(square.area);
  33. // 输出 100

extends继承的核心代码如下,其实现和上述的寄生组合式继承方式一样。

  1. function _inherits(subType, superType) {
  2. // 创建对象,创建父类原型的一个副本
  3. // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  4. // 指定对象,将新创建的对象赋值给子类的原型
  5. subType.prototype = Object.create(superType && superType.prototype, {
  6. constructor: {
  7. value: subType,
  8. enumerable: false,
  9. writable: true,
  10. configurable: true
  11. }
  12. });
  13. if (superType) {
  14. Object.setPrototypeOf
  15. ? Object.setPrototypeOf(subType, superType)
  16. : subType.__proto__ = superType;
  17. }
  18. }

参考文章