重学 JS 系列:聊聊继承

这是重学 JS 系列的第二篇文章,写这个系列的初衷也是为了夯实本身的 JS 基础。既然是重学,确定不会从零开始介绍一个知识点,若有遇到不会的内容请自行查找资料。前端

原型

继承得靠原型来实现,固然原型不是这篇文章的重点,咱们来复习一下便可。git

其实原型的概念很简单:github

  • 全部对象都有一个属性 __proto__ 指向一个对象,也就是原型
  • 每一个对象的原型均可以经过 constructor 找到构造函数,构造函数也能够经过 prototype 找到原型
  • 全部函数均可以经过 __proto__ 找到 Function 对象
  • 全部对象均可以经过 __proto__ 找到 Object 对象
  • 对象之间经过 __proto__ 链接起来,这样称之为原型链。当前对象上不存在的属性能够经过原型链一层层往上查找,直到顶层 Object 对象

其实原型中最重要的内容就是这些了,彻底没有必要去看那些长篇大论什么是原型的文章,初学者会越看越迷糊。面试

固然若是你想了解更多原型的深刻内容,能够阅读我 以前写的文章数组

ES5 实现继承

ES5 实现继承总的来讲就两种办法,以前写过这方面的内容,就直接复制来用了。浏览器

总的来讲这部分的内容我以为在当下更多的是为了应付面试吧。架构

组合继承

组合继承是最经常使用的继承方式,app

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}
function Child(value) {
  Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
复制代码

以上继承的方式核心是在子类的构造函数中经过 Parent.call(this) 继承父类的属性,而后改变子类的原型为 new Parent() 来继承父类的函数。函数

这种继承方式优势在于构造函数能够传参,不会与父类引用属性共享,能够复用父类的函数,可是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,致使子类的原型上多了不须要的父类属性,存在内存上的浪费。优化

寄生组合继承

这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,咱们只须要优化掉这点就好了。

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
复制代码

以上继承实现的核心就是将父类的原型赋值给了子类,而且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

Babel 如何编译 ES6 Class 的

为何在前文说 ES5 实现继承更多的是应付面试呢,由于咱们如今能够直接使用 class 来实现继承。

可是 class 毕竟是 ES6 的东西,为了能更好地兼容浏览器,咱们一般都会经过 Babel 去编译 ES6 的代码。接下来咱们就来了解下经过 Babel 编译后的代码是怎么样的。

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),其实这句代码的做用是为了继承到父类的静态方法,以前咱们实现的两种继承方法都是没有这个功能的。

而后 Child 构造函数这块的代码也基本和以前的实现方式相似。因此总的来讲 Babel 实现继承的方式仍是寄生组合继承,无非多实现了一步继承父类的静态方法。

继承存在的问题

讲了这么些如何实现继承,如今咱们来考虑下继承是不是一个好的选择?

总的来讲,我我的不怎么喜欢继承,缘由呢就一个个来讲。

咱们先看代码。假如说咱们如今要描述几辆不一样品牌的车,车必然是一个父类,而后各个品牌的车都分别是一个子类。

class Car {
    constructor (brand) {
        this.brand = brand
    }
    wheel () {
        return '4 个轮子'
    }
    drvie () {
        return '车能够开驾驶'
    }
    addOil () {
        return '车能够加油'
    }
}
Class OtherCar extends Car {}
复制代码

这部分代码在当下看着没啥毛病,实现了车的几个基本功能,咱们也能够经过子类去扩展出各类车。

可是如今出现了新能源车,新能源车是不须要加油的。固然除了加油这个功能不须要,其余几个车的基本功能仍是须要的。

若是新能源车直接继承车这个父类的话,就出现了第一个问题 ,大猩猩与香蕉问题。这个问题的意思是咱们如今只须要一根香蕉,可是却获得了握着香蕉的大猩猩,大猩猩其实咱们是不须要的,可是父类仍是强塞给了子类。继承虽然能够重写父类的方法,可是并不能选择须要继承什么东西。

另外单个父类很难描述清楚全部场景,这就致使咱们可能又须要新增几个不一样的父类去描述更多的场景。随着不断的扩展,代码势必会存在重复,这也是继承存在的问题之一。

除了以上两个问题,继承还存在强耦合的状况,无论怎么样子类都会和它的父类耦合在一块儿。

既然出现了强耦合,那么这个架构一定是脆弱的。一旦咱们的父类设计的有问题,就会对维护形成很大的影响。由于全部的子类都和父类耦合在一块儿了,假如更改父类中的任何东西,均可能会致使须要更改全部的子类。

如何解决继承的问题

继承更多的是去描述一个东西是什么,描述的很差就会出现各类各样的问题,那么咱们是否有办法去解决这些问题呢?答案是组合。

什么是组合呢?你能够把这个概念想成是,你拥有各类各样的零件,能够经过这些零件去造出各类各样的产品,组合更多的是去描述一个东西能干什么。

如今咱们把以前那个车的案例经过组合的方式来实现。

function wheel() {
  return "4 个轮子";
}
function drvie() {
  return "车能够开驾驶";
}
function addOil() {
  return "车能够加油";
}
// 油车
const car = compose(wheel, drvie, addOil)
// 新能源车
const energyCar = compose(wheel, drive)
复制代码

从上述伪代码中想必你也发现了组合比继承好的地方。不管你想描述任何东西,均可以经过几个函数组合起来的方式去实现。代码很干净,也很利于复用。

最后

其实这篇文章的主旨仍是后面两小节的内容,若是你还有什么疑问欢迎在评论区与我互动。

我全部的系列文章都会在个人 Github 中最早更新,有兴趣的能够关注下。今年主要会着重写如下三个专栏

  • 重学 JS
  • React 进阶
  • 重写组件

最后,以为内容有帮助能够关注下个人公众号 「前端真好玩」咯,会有不少好东西等着你。

相关文章
相关标签/搜索