三、JavaScript常见继承方式

139 阅读4分钟

继承的概念:继承是面向对象的,使用这种方式可以更好的复用之前的代码,提高效率,在js中,继承多用于前端工程基础库的搭建

问题:

  1. js的继承有多少种实现方式

  2. ES6的extends 关键字是用那种继承?

继承概念的探究

说到继承的概念,首先要说一个经典的例子。 

先定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等,由汽车这个类可以派生出“轿车”和“货车”两个类,那么可以在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱。这样轿车和货车就是不一样的,但是二者都属于汽车这个类,这样从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系。 

 继承可以使得子类别具有父类的各种方法和属性,比如上面的例子中“轿车” 和 “货车” 分别继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性。在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法。

JS 实现继承的几种方式

第一种:原型链继承 

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。 

 代码演示

  function Parent1() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
  }
  function Child1() {
    this.type = 'child2';
  }
  Child1.prototype = new Parent1();
  console.log(new Child1()); 

上面的代码看似没有问题,虽然父类的方法和属性都能够访问,但其实有一个潜在的问题,我再举个例子来说明这个问题。 

var s1 = new Child1();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play); 

这段代码在控制台执行之后,可以看到结果如下:

[1,2,3,4]
[1,2,3,4]

 明明我只改变了 s1 的 play 属性,为什么 s2 也跟着变了呢?原因很简单,因为两个实例使用的是同一个原型对象。

它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。 

 那么要解决这个问题的话,就得再看看其他的继承方式,下面我们看看能解决原型属性共享问题的第二种方法。

第二种:构造函数继承(借助 call) 

  function Parent1(){

    this.name = 'parent1';

  }



  Parent1.prototype.getName = function () {

    return this.name;

  }



  function Child1(){

    Parent1.call(this);

    this.type = 'child1'

  }



  let child = new Child1();

  console.log(child);  // 没问题

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

执行上面的这段代码,可以得到这样的结果。

打印的 child 在控制台显示,除了 Child1 的属性 type 之外,也继承了 Parent1 的属性 name。这样写的时候子类虽然能够拿到父类的属性值,解决了第一种继承方式的弊端,但问题是,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法。这种情况的控制台执行结果如下图所示。

Drawing 2.png

因此,从上面的结果就可以看到构造函数实现继承的优缺点,它使父类的引用属性不会被共享,优化了第一种继承方式的弊端;但是随之而来的缺点也比较明显——只能继承父类的实例属性和方法,不能继承原型属性或者方法。

第三种:组合继承(前两种组合)

这种方式结合了前两种继承方式的优缺点,结合起来的继承,代码如下。

  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'

执行上面的代码,可以看到控制台的输出结果,之前方法一和方法二的问题都得以解决。

Drawing 3.png

但是这里又增加了一个新问题:通过注释我们可以看到 Parent3 执行了两次,第一次是改变Child3 的 prototype 的时候,第二次是通过 call 方法调用 Parent3 的时候,那么 Parent3 多构造一次就多进行了一次性能开销,这是我们不愿看到的。