这下能把继承看懂了吧!!!

185 阅读8分钟

继承

继承(inheritance)是面向对象语言中最为津津乐道的概念,在许多面向对象语言中都支持两种继承方式,接口继承和实现继承。

  • 接口继承:只继承方法签名
  • 实现继承:继承实际的方法

在 JavaScript 中只支持实现继承,并且实现继承的实现主要依靠原型链实现

继承的优点:子类可以继承并使用父类的属性和方法。以此可以提高代码的复用

继承方式

  • 原型链继承
  • 构造函数继承(借助 call)
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

原型链继承

原型链继承是比较常见的继承方式之一,将子类的原型指向父类 其中涉及 构造函数、原型和实例,三者之间存在着一定的关联, image.png

每一个构造函数都有一个原型对象;

原型对象又包含一个指向构造函数的指针;

而实例则包含一个指向原型对象的指针。

注意: 所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个指向 Object.prototype 的指针 关于原型链可以参考我之前出的这篇文章 从构造函数到原型链 实现原型链的基本模式:

 function Parent() {
  this.name = 'parent1';
  this.play = [1, 2, 3]
}
function Child() {
  this.type = 'child2';
}
Child.prototype = new Parent();



var s1 = new Child();
console.log(s1)
var s2 = new Child();
s1.play.push(4);
console.log(s1); // [1,2,3,4]
console.log(s2); // [1,2,3,4]

将 Children 的原型指向 Parent 的实例,原型指向了另一个类型的实例,通过重写原型对象的方式实现原型链的继承,使原本存在于 Parent 实例中的方法和属性也存在于 Children.prototype 中。 image.png

但是原型链继承的引用类型属性会被所有实例共享:

子类的原型对象指向父类的实例,因此子类实例共享父类实例的属性和方法,而且子类实例对引用类型属性的修改会影响到其他子类实例和父类实例 优点:

  1. 子类通过prototype指向父类的实例,实现原型链继承。

缺点:

  1. 不能向父类构造函数传递参数
  2. 父类上的引用类型属性会被所有实例属性共享,其中一个实例改变时,会影响其他实例,所以在实践中很少会单独使用原型链继承。

借助构造函数继承

在解决原型中包含引用类型值所带来的问题时,可以选择借助构造函数来完成,在子类构造函数的内部调用父类的构造函数,因此可以借助 call、apply 调用 Parent 函数。

 function ParentConstructor(){
  // 父类的 方法和属性
  this.name = 'parent1';
  this.sayName =function() {
    return this.name
  }
}

// 父类原型上的方法
ParentConstructor.prototype.getName = function () {
  return this.name;
}

function ChildConstructor(){
  ParentConstructor.call(this);
  this.type = 'child'
}

let child = new ChildConstructor();
console.log(new ParentConstructor());  // 没问题
console.log(child);  // 没问题

console.log(child.sayName());  // 'parent1'
console.log(child.getName());  // 会报错

在新创建的对象上执行父类构造函数,使得 child 的每个实例都会具有自己属性副本 image.png

父类原型对象中一旦存在父类自己定义的原型方法,那么子类将无法继承这些方法 相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端, 优点:

  1. 可以向父类传参
  2. 复杂类型的父类属性不会被共享

缺点:只能继承父类的实例属性和方法,不能继承父类原型上的属性和方法

组合继承

原型链继承无法传参,但可以获取原型对象上的属性,借助构造函数无法获取原型对象上的属性,但是可以传参赋值构造函数内属性和方法。 那么如果将 原型链继承 与 借助构造函数继承相结合会发生什么样的事情呢?

function Parent3(){
  this.name = 'parent3'
  this.play = [1, 2, 3];
}

// 原型链上添加方法
Parent3.prototype.getName = function () {
  return this.name;
}

function Child3(){
   // 此时子类的原型已经指向 父类 即 Child3.prototype ==> Parent3 继承 父类原型上的属性和方法
   console.log('children', Child3.prototype)  
  // 第二次在 子类型构造函数内部调用 父类构造函数 Parent3
  Parent3.call(this)               // 使得 child 的每个实例都有自己的属性副本 不会相互影响
  this.type = 'child3'
}

//  将原型对象指向 父类时 是 第一次调用 Parent3()
Child3.prototype = new Parent3();  // 第一次调用 父类构造函数  改变子类的原型指向

var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'

将原型链和借助构造函数两者相结合,发挥二者之长这样的方式被称为组合继承,也被称为伪经典继承。

既能够继承原型上的属性和方法又能实现对实例属性的隔离继承保证实例都有自己的属性。 image.png 优点:

  1. 子类实例化可向父类传参。
  2. 实现对构造函数和原型对象上的属性和方法的复用。

缺点:父类构造函数被调用多次,可能会影响性能。一次是时创建子类原型时,另一次在子类构造函数内部。

组合式继承避免了原型链和构造函数的缺陷,融合了他们的优点成为了 javascript 中最常用的继承模式

原型式继承

原生式继承必须要求有一个对象作为另一个对象的基础,es5 中新增的 Object.create 对原型式继承进行了规范。

let parent4 = {
  name: "parent4",
  friends: ["p1", "p2", "p3"],
  getName: function() {
    return this.name;
  }
};

let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");

let person5 = Object.create(parent4);
person5.friends.push("lucy");

console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]

执行结果 image.png Object.create() 与 object() 的行为相同,对传入其中的对象执行了一次浅复制

function object(o) {
	function F() {}
  F.prototype = o
  return new F()
}

先创建一个临时的构造函数,将传入的对象作为这个构造函数的原型,最后返回临时类型的实例。 因为 Object.create 方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能 优点:简单,单纯的通过原型链访问父类属性 缺点:与原型链继承相似,在各实例中引用类型的属性会被共享

寄生式继承

寄生式继承在原型式继承的基础上进行优化,利用浅拷贝的能力创建一个新对象,再以某种方式对这个对象进行增强,并最终返回该对象。

let parent5 = {
  name: "parent5",
  friends: ["p1", "p2", "p3"],
  getName: function() {
      return this.name;
  }
};


function clone(original) {
  let clone = Object.create(original);
  clone.getFriends = function() {
      return this.friends;
  };
  return clone;
}
let person6 = clone(parent5);
let person7 = clone(parent5);


person6.name = 'person6'
person7.name = 'person7'


person6.friends.push('hahaha')
console.log(person6);
console.log(person7);

image.png

由于寄生式继承本质上是在原型式继承的基础上增加了一些新的属性或者方法,

因此子类实例共享父类实例的引用类型和方法,而且子类实例对引用类型属性的修改会影响到父类实例及其他子类实例。

使用寄生式继承来为对象添加函数,但是缺点也很明显,容易造成属性共享和引用类型属性修改的问题。

✨ 寄生组合式继承

寄生组合继承是在组合继承的基础上,采用 Object.create() 方法改造实现减少对父类的调用次数,从而提高性能

寄生组合式继承,将组合式和寄生式结合起来,借用构造函数来继承属性,通过原型链的形式来继承方法。

在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式

   function clone (parent, child) {
      // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
      child.prototype = Object.create(parent.prototype);
      child.prototype.constructor = child;  //  修正原型副本的构造函数为子类构造函数
      console.log(parent.prototype.constructor)  // 父类构造函数指向 Parent6
      console.log(child.prototype.constructor)  // 修正后的子类构造函数指向 Child6

    }

    function Parent6() {
      this.name = 'parent6';
      this.play = [1, 2, 3];
    }
    Parent6.prototype.getName = function () {
      return this.name;
    }
    function Child6(name) {
      Parent6.call(this);
      this.name = name;
    }

    clone(Parent6, Child6);

    Child6.prototype.getFriends = function () {
      return this.friends;
    }

    let person8 = new Child6('haha'); 
    let person9 = new Child6('lala'); 
    person9.play.push(7,8,9)
    console.log(person8);
    console.log(person9); 

image.png

寄生组合式继承通过借用构造函数来继承属性,结合原型链的混成形式来继承方法。使用寄生式继承来继承父类的原型,在将结果指定给子类型的原型。

寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方式。

extends

ES6 中引入了 class 关键字,使得 JavaScript 中的继承更加符合面向对象编程的习惯。通过使用 class 关键字和 extends 关键字,可以轻松地实现对父类属性和方法的继承。

extends 基于原型链中的寄生组合继承方式实现,用 extends 继承父类,在子类的 construtor 调用 super()。

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // 调用父类的构造函数
  }
  speak() {
    console.log(`${this.name} barks.`);
  }
}

let d = new Dog('Mitzie');
d.speak(); // 输出 "Mitzie barks."

Dog 类继承了 Animal 类,并且在 Dog 类的构造函数中调用了 super 方法来调用父类的构造函数。这样做可以实现子类实例化时对父类实例属性的继承,同时也可以继承父类原型上的方法和属性。

小结

JavaScript 中有多种继承方式可供选择,每种方式都有其优缺点。 而寄生组合式继承,是集寄生式继承和组合继承的优点于一身,

而 extends 的语法糖实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式。

参考

  1. 面试官:Javascript如何实现继承?
  2. javascript的几种继承方式