继承几种实现方式

614 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第19天,点击查看活动详情

继承

继承可以使得子类具有父类别的各种属性和方法,开发过程中不需要重复的去书写一些已经存在的代码,在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。

JavaScripy常见的继承方式有以下几种:

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

原型链继承

原型链继承是比较常用的继承方式,利用了JS的对象拥有原型的特性,每个构造函数都有一个原型对象,其中原型链主要包括三部分,构造函数、原型和实例,构造函数能通过prototype访问原型对象,原型对象通过construtor访问构造函数,构造函数通过new生成实例。让我们来看下原型链如何实现继承

 function Parent() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
  }
  function Child() {
    this.type = 'child2';
  }
  Child1.prototype = new Parent();
  console.log(new Child())
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]

原型链继承会有一个比较明显的问题,由于继承的是父级的属性,所以s1和s2中的play属性是从父级继承过来的,相当于s1和s2都有play属性,虽然是已经继承到了,但公用的是同一个父级的属性,当修改其中一个play的时候,更改的是父级的play属性,这样又被继承下来,s1和s2中的play属性都会一起修改

构造函数继承

function Parent(){
    this.name = 'parent1';
    this.play = [1, 2, 3]
}
Parent.prototype.getName = function () {
    return this.name;
}
function Child(){
    Parent.call(this);
    this.type = 'child'
}
let child = new Child();
let child1 = new Child();
child1.name = 'parent2'
child1.play.push(4);
console.log(child);  // Child { name: 'parent1', play: [ 1, 2, 3 ], type: 'child' }
console.log(child1);  //  Child { name: 'parent2', play: [ 1, 2, 3, 4 ], type: 'child' }
// console.log(child.getName());  // 会报错

从上面代码可以看出,构造函数继承可以解决原型链继承子类共享父类属性的问题,但是构造函数继承也存在自己的问题,比如父类通过原型类设置的方法,子类无法进行继承使用,虽然优化了第一种原型链继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

组合继承

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

Parent3.prototype.getName = function () {
    return this.name;
}
function Child3() {
    // 第二次调用 Parent3()
    Parent3.call(this);
    this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
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'

组合继承是将前面俩种原型链继承和构造函数继承的方法结合起来,优化了原型链和构造函数的缺陷,虽然能够优秀的解决的问题,但是也存在自己的小问题,比如上面的代码中,Parent3 执行了两次,造成了多构造一次的性能开销。但也是一个较为合理的继承方式

原型式继承

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"]

我们知道Object.create方法实现的是浅拷贝,所以这种方式会有浅拷贝的缺点,我们可以看到parent4这个对象中有原始数据类型和引用数据类型,对我们多个拷贝出来的子类来说,多个子类的引用数据类型会指向同一个内存地址,就会出现同原型链继承方式相似的问题。

寄生式继承

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 person5 = clone(parent5);

console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]

寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法 其优缺点也很明显,跟上面讲的原型式继承一样

寄生组合式继承

function clone (parent, child) {
    // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
}

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

clone(Parent6, Child6);

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

let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5

寄生组合式继结合了前面几种继承方式的优缺点,其中组合继承较为完善,只是多次调用了方法,寄生继承的缺点和原型继承相似,寄生组合方式结合了俩种方式的优缺点进行改造,可以说是继承方式中相对较优的继承方式

Class语法糖继承

class Person {
  constructor(name) {
    this.name = name
  }
  // 原型方法
  // 即 Person.prototype.getName = function() { }
  // 下面可以简写为 getName() {...}
  getName = function () {
    console.log('Person:', this.name)
  }
}
class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    super(name)
    this.age = age
  }
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

Class继承是通过ES6 中的extends关键字直接实现的继承,但是我们可以用babel工具转换进行查看,extends的继承方法其实也是寄生组合的继承方式,但是从书写方面上看,使用Class的书写方式可以让我们整个代码的过程更加清晰,继承的流程也比较流畅