🔥 extends和寄生式组合继承原型之间的区别?

1,954

在面向对象编程中讨论最多的就是继承,大部分的面向对象语言都支持两种继承,一种是接口继承,一种是实现继承。前者只继承方法签名(比如Java中继承接口的抽象类),后者继承实际的方法。在ECMAScript中接口继承是不存在的。实现继承是ECMAScript的唯一继承方式,而这主要是通过原型链实现的。(在ECMAScript中我们所说的继承其实并不是真正意义上的继承,类意味着是复制。但是JS中的类是共享。我们只是在模仿真正的类。所以JS中的类class基本上只是现有原型[[prototype]]机制的一种语法糖!)

如果您不清楚ES5的各种继承,我推荐您阅读这篇文章,之后再来阅读这篇文章。

寄生式组合继承

在ES5中存在原型链继承,盗用构造函数继承,原型式继承,寄生式继承,寄生式组合继承。 寄生式组合继承无疑是最完美的一种继承方式。(其实就是原型共享)在JS中所有的继承都是原型的共享,那么它的原型共享是怎样的一个关系呢?

下面是我们的示例代码,你可以先来看看,在你的脑海里有没有这个继承的关系图:

  function implementInheritance(fFunc, sFunc) {
    const prototype = Object(fFunc.prototype); // 创建一个与父原型对象引用一致的原型对象
    // 如果传进去的是引用类型的值,仍然会返回这个值,经他们复制的变量保有和源对象相同的引用地址
    prototype.constructor = sFunc; // 将构造函数的指引从父构造函数变为子构造函数
    sFunc.prototype = prototype; // 赋值原型对象
  }

  function Father(identity) {
    this.identity = identity;
    this.sayFather = function () {
      console.log('我是Father')
    }
  }
  Father.prototype.sayFatherIdentity = function () {
    console.log("Father -> sayFatherIdentity -> this.identity", this.identity)
  }

  function Son(fIdentity, selfIdentity) {
    Father.call(this, fIdentity);
    this.selfIdentity = selfIdentity;
    this.saySon = function () {
      console.log('我是Son')
    }
  }
  implementInheritance(Father, Son);
  Son.prototype.saySonIdentity = function () {
    console.log("Son -> saySonIdentity -> this.selfIdentity", this.selfIdentity)
  }
  const newF = new Father('父');
  const newS = new Son('父--s','子');

阅读完代码了,原型的关系图有没有在你的脑海中浮现呢?没有的话请看下面的图片:

寄生组合继承原型.png

解读:

在使用完 implementInheritance()这个函数之后,因为我们创建了一个和父构造函数引用一致的对象,所以我们为这个原型对象上添加方法时,其实也添加到了父构造函数的原型上。因为经过执行 sFunc.prototype = prototype;这句代码,所以子构造函数的原型也是父构造函数的原型。所以我们才看到了两个构造函数和两个实例对象的原型都是一个原型,prototype.constructor = sFunc; 这句代码改变了父构造函数的constructor属性的指引,我们实例化完Father之后会发现构造函数其实指向了Son构造函数。 这个就是寄生式组合继承的原型共享的结构图。

class extends

ES6的继承其实只是ES5继承的语法糖 (class继承使用的任然是原型和构造函数的概念,还是基于原型的共享),但是它的原型共享关系图和ES5的一点也不一样。

请看下面的示例代码:

  // Father 类
  class Father {
    constructor(identity) {
      this.identity = identity
    }
    sayFatherIdentity() {
      console.log("Father -> sayFatherIdentity -> this.identity", this.identity)
    }
    sayFather = () => {
      console.log('我是Father')
    }
  }

  // Son 类
  class Son extends Father {
    constructor(fatherIdentity, selfIdentity) {
      super(fatherIdentity)
      this.sonIdentity = selfIdentity
    }
    saySonIdentity() {
      console.log("Son -> saySonIdentity -> this.sonIdentity", this.sonIdentity)
    }
    saySon = () => {
      console.log('我是Son')
    }
  }
  const newF = new Father('父');
  const newS = new Son('父-son', '子');

请看下方的原型共享关系图:

Class继承原型链.png

解读:

从图中我们可以看出,子构造函数是有自己的原型对象,但是子原型对象的__proto__指向了父构造函数的原型对象, 再看构造函数,子类和父类都有自己的构造函数,这一点还是nice的!子类的prototype的__proto__是指向父类的的原型对象。对于实例化的两个对象来说,子实例化对象的原型对象的__proto__ === 父实例化对象的原型对象。

区别

寄生式组合继承与class 继承 其实都是基于原型对象共享实现的。

  1. 对于原型对象的共享来说, 寄生式组合继承的原型共享都是直接基于父构造函数的原型对象。而class 继承则是每个类都有自己的原型对象,都是基于原型链进行链接共享的原型对象。

  2. 对于构造函数来说,主要的区别就是,类构造函数必须使用new操作符调用。而普通的构造函数如果不使用new调用,那么就会以全局的this作为内部的对象。

往期精彩