一、开篇前谈
这一段时间不小心中招🐏了,身体抱恙啊每天都是软弱无力,估摸着已经有一周没有学习、复习知识了。这会儿身体好些了,真的不能再摆下去了,连忙爬起来更新这一篇想写很久的有关继承的内容,本文简单理顺一下四种继承的演变过程,其中的原型链相关知识在下一篇原型、原型文章再展开聊吧。
二、什么是继承
我们简单聊一聊什么是继承。继承这个词一般处于面向对象开发的语言中,在JavaScript中的继承其实是对于继承的一种模仿,因为并不完全具备面向对象的特性。继承是对一个通用数据类型的复用,通俗而言便是子类拥有父类开放的方法、属性。
三、实现继承
在JavaScript中实现继承的方式一般为四种:原型链继承、借用构造函数继承、组合继承以及寄生组合继承,前三种继承方式都有各自存在的问题,寄生组合继承是在ES6的extends、class关键字出现之前的最佳继承解决方案。
1. 原型链继承
原型链简单而言便是一个由原型所组成的链条,实例对象可以通过原型链寻找到一层层父类原型上的属性、方法,原型链的终点为null。因此我们可以利用原型链的特性,将子类构造函数的原型设置为所要继承父类实例对象,便可继承到父类的方法属性以及原型链。
得到执行结果:
从上述例子,我们可以看到继承了父类属性和方法的Henry子类实例对象可以调用父类的方法以及属性,但当子类实例上出现与父类同名属性时优先使用子类属性(原型链知识)。
这种继承方式是存在弊端的,当我们修改了原型属性时将会作用到所有的实例对象上,同时子类原型修改为一个父类实例对象,若这个实例对象的属性方法发生了修改,所有的子类实例对象也会受到影响:
得到执行结果:
从上述例子中不论是通过直接修改原型、亦或是对所依赖的父类实例对象进行修改都会影响到子类实例对象。同时父类构造器实例化过程是独立于子类构造器执行的,因此子类无法复用父类构造函数,因此这不是一个非常有效的继承方案,
2. 借用构造器继承
因此有了利用new关键字实质是创建一个this对象并对其进行赋值最后返回的一个特性,我们可以使用call、apply等显示绑定的方式修改父类构造函数的this指向,使其指向当前构造函数的this从而实现复用父类构造函数的目的:
得到执行结果:
从上述运行结果中我们可以看到,子类成功的利用apply方法复用了父类的函数构造器,但因为我们并没有关联父类构造函数的原型属性。因此我们无法使用到父类原型链上的相关属性、方法,这也是借用构造器实现继承存在的弊端。
3. 组合继承
那么将上述两种继承方式相结合,是否能达成既能使用父类原型链相关属性、方法又能复用父类构造函数的一个继承方案呢?这便是Js继承中的组合继承:
得到执行结果:
那么我们从上述执行结果可以看到既实现了复用父类构造函数又能获取到父类原型链上的属性及方法,这便是相对理想的继承实现方案了。但实际上他仍然存在一个小问题,在上述实现中我们可以看到调用了两次的父类构造函数,一次是在复用、一次是创建父类实例对象,但是根据原型链的知识我们可以知道,子类实例身上是包含父类实例身上由构造函数初始化的所有属性,由此用做原型的父类实例身上的属性是无用属性。其次,当父类构造函数逻辑足够复杂时,还会造成一定的性能浪费。
4. 寄生组合继承
基于组合继承的基础上对第二次使用到父类构造函数进行优化。在ES6之后我们会使用Object.create方法创建一个以父类原型对象为原型的实例。在没有该方法之前,我们也可以使用圣杯模式来实现这一步骤的优化:
上述是使用圣杯模式(雅虎做法)的方式实现的寄生组合继承,避免了父类构造器多余的创建父类实例,让我们看看执行结果:
而是用Object.create方法能完美替换较为复杂的圣杯模式:
让我们看看执行结果以及当前的子类实例原型链的结构:
我们可以清晰的看见Sub实例对象上复用父类构造函数创建了hobby属性以及原型链继承了父类的相关原型链。对于Object.create这个Api的具体用法以及两个参数详情可以查看MDN上对于Object.create的介绍。
四、总结
以上便是对于JavaScript中四种继承方案的介绍,当然在ES6之后提出了class以及完美继承extends关键字的继承方案,大家有兴趣的可以使用Babel将相关es6代码转换为es5以查看实现的底层逻辑。
在这里要提一嘴,对于子类直接操作原型链上的引用类型属性导致作用于所有相关子类实例的问题是相对无解的问题。因此在配置原型时尽量只将通用方法挂载到原型对象上,如果真的存在引用值而又不希望被后续操作所修改,可以给对应引用值配置属性描述符以及使用Object.preventExtensions
等相关对象封闭Api。
一篇简单的随手写文章结束啦,下次见。