6 继承

6.1 方式1: 借助call

     function Parent1(){
        this.name = 'parent1';
      }
      function Child1(){
        Parent1.call(this);
        this.type = 'child1'
      }
      console.log(new Child1);

这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。

6.2 方式2: 借助原型链

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

看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:

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

可以看到控制台:

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

那么还有更好的方式么?

6.3 方式3:将前两种组合

      function Parent3 () {
        this.name = 'parent3';
        this.play = [1, 2, 3];
      }
      function Child3() {
        Parent3.call(this);
        this.type = 'child3';
      }
      Child3.prototype = new Parent3();
      var s3 = new Child3();
      var s4 = new Child3();
      s3.play.push(4);
      console.log(s3.play, s4.play);

可以看到控制台:

之前的问题都得以解决。但是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?

6.4 方式4: 组合继承的优化1

      function Parent4 () {
        this.name = 'parent4';
        this.play = [1, 2, 3];
      }
      function Child4() {
        Parent4.call(this);
        this.type = 'child4';
      }
      Child4.prototype = Parent4.prototype;

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下:

    var s3 = new Child4();
    var s4 = new Child4();
    console.log(s3)

子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。

6.5 方式5(最推荐使用): 组合继承的优化2

     function Parent5 () {
        this.name = 'parent5';
        this.play = [1, 2, 3];
      }
      function Child5() {
        Parent5.call(this);
        this.type = 'child5';
      }
      Child5.prototype = Object.create(Parent5.prototype);
      Child5.prototype.constructor = Child5;

这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承。

6.6 ES6的extends被编译后的JavaScript代码

ES6的代码最后都是要在浏览器上能够跑起来的,这中间就利用了babel这个编译工具,将ES6的代码编译成ES5让一些不支持新语法的浏览器也能运行。

那最后编译成了什么样子呢?

    function _possibleConstructorReturn(self, call) {
        // ...
        return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
    }
    
    function _inherits(subClass, superClass) {
        // ...
        //看到没有
        subClass.prototype = Object.create(superClass && superClass.prototype, {
            constructor: {
                value: subClass,
                enumerable: false,
                writable: true,
                configurable: true
            }
        });
        if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
    }
    
    
    var Parent = function Parent() {
        // 验证是否是 Parent 构造出来的 this
        _classCallCheck(this, Parent);
    };
    
    var Child = (function (_Parent) {
        _inherits(Child, _Parent);
    
        function Child() {
            _classCallCheck(this, Child);
    
            return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
        }
    
        return Child;
    }(Parent));

核心是_inherits函数,可以看到它采用的依然也是第五种方式————寄生组合继承方式,同时证明了这种方式的成功。不过这里加了一个Object.setPrototypeOf(subClass, superClass),这是用来干啥的呢?

答案是用来继承父类的静态方法。这也是原来的继承方式疏忽掉的地方。

追问: 面向对象的设计一定是好的设计吗?

不一定。从继承的角度说,这一设计是存在巨大隐患的。

6.7 从设计思想上谈谈继承本身的问题

假如现在有不同品牌的车,每辆车都有drive、music、addOil这三个方法。

    class Car{
      constructor(id) {
        this.id = id;
      }
      drive(){
        console.log("wuwuwu!");
      }
      music(){
        console.log("lalala!")
      }
      addOil(){
        console.log("哦哟!")
      }
    }
    class otherCar extends Car{}

现在可以实现车的功能,并且以此去扩展不同的车。

但是问题来了,新能源汽车也是车,但是它并不需要addOil(加油)。

如果让新能源汽车的类继承Car的话,也是有问题的,俗称"大猩猩和香蕉"的问题。大猩猩手里有香蕉,但是我现在明明只需要香蕉,却拿到了一只大猩猩。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了。

继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。

当然你可能会说,可以再创建一个父类啊,把加油的方法给去掉,但是这也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好。

那如何来解决继承的诸多问题呢?

用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。

顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。

    function drive(){
      console.log("wuwuwu!");
    }
    function music(){
      console.log("lalala!")
    }
    function addOil(){
      console.log("哦哟!")
    }
    
    let car = compose(drive, music, addOil);
    let newEnergyCar = compose(drive, music);

代码干净,复用性也很好。这就是面向组合的设计方式。

6.8 继承-简版

在 ES5 中,我们可以使用如下方式解决继承的问题

    function Super() {}
    Super.prototype.getNumber = function() {
      return 1
    }
    
    function Sub() {}
    let s = new Sub()
    Sub.prototype = Object.create(Super.prototype, {
      constructor: {
        value: Sub,
        enumerable: false,
        writable: true,
        configurable: true
      }
    })
  • 以上继承实现思路就是将子类的原型设置为父类的原型
  • ES6 中,我们可以通过 class 语法轻松解决这个问题
    class MyDate extends Date {
      test() {
        return this.getTime()
      }
    }
    let myDate = new MyDate()
    myDate.test()
  • 但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。
  • 如果你使用编译过得代码调用 myDate.test()你会惊奇地发现出现了报错

因为在 JS 底层有限制,如果不是由 Date构造出来的实例的话,是不能调用 Date 里的函数的。所以这也侧面的说明了:ES6 中的 class 继承与 ES5 中的一般继承写法是不同的。

  • 既然底层限制了实例必须由 Date 构造出来,那么我们可以改变下思路实现继承
    function MyData() {
    
    }
    MyData.prototype.test = function () {
      return this.getTime()
    }
    let d = new Date()
    Object.setPrototypeOf(d, MyData.prototype)
    Object.setPrototypeOf(MyData.prototype, Date.prototype)
  • 以上继承实现思路:先创建父类实例 => 改变实例原先的 _proto__转而连接到子类的 prototype=> 子类的 prototype__proto__ 改为父类的 prototype
  • 通过以上方法实现的继承就可以完美解决 JS 底层的这个限制
Last Updated:
Contributors: leeguooooo