详解Javascript的继承实现

我最先掌握的在js中实现继承的方法是在w3school学到的混合原型链和对象冒充的方法,在工做中,只要用到继承的时候,我都是用这个方法实现。它的实现简单,思路清晰:用对象冒充继承父类构造函数的属性,用原型链继承父类prototype 对象的方法,知足我遇到过的全部继承的场景。正因如此,我从没想过下次写继承的时候,我要换一种方式来写,才发如今js里面,继承机制也能够写的如此贴近java这种后端语言的实现,确实很妙!因此我想在充分理解他博客的思路下,实现一个本身从此用获得的一个继承库。javascript

1. 混合方式实现及问题

了解问题以前,先看看它的具体实现:html

从结果上来讲,这种继承实现方式没有问题,Manager的实例同时继承到了Employee类的实例属性和实例方法,而且经过instanceOf运算的结果也都正确。可是从代码组织和实现细节层面,这种方法还有如下几个问题:java

1)代码组织不够优雅,继承实现的关键部分的逻辑是通用的,都是以下结构:git

这段代码缺少封装。另外在添加子类的实例方法时,不能经过SubClass.prototype = { method1: function() {} }这种方式去设置,不然就把子类的原型整个又修改了,继承就没法实现了,这样每次都得按SubClass.prototype.method1 = function() {} 的结构去写,代码看起来很不连续。github

解决方式:利用模块化的方式,将通用的逻辑封装起来,对外提供简单的接口,只要按照约定的接口调用,就可以简化类的构建与类的继承。具体实现请看后面的内容介绍,暂时只能提供理论的说明。编程

2)在给子类的原型设置成父类的实例时,调用的是new SuperClass(),这是对父类构造函数的无参调用,那么就要求父类必须有无参的构造函数。但是在javascript中,函数没法重载,因此父类不可能提供多个构造函数,在实际业务中,大部分场景下父类构造函数又不可能没有参数,为了在惟一的一个构造函数中模拟函数重载,只能借助判断arguments.length来处理。问题就是,有时候很难保证每次写父类构造函数的时候都会添加arguments.length的判断逻辑。这样的话,这个处理方式就是有风险的。要是能把构造函数里的逻辑抽离出来,让类的构造函数所有是无参函数的话,这个问题就很好解决了。bootstrap

解决方式:把父类跟子类的构造函数所有无参化,而且在构造函数内不写任何逻辑,把构造函数的逻辑都迁移到init这个实例方法,好比前面给出的Employee和Manager的例子就能改形成下面这个样子:后端

用init方法来完成构造功能,就能够保证在设置子类原型时(Manager.prototype = new Employee()),父类的实例化操做必定不会出错,惟一很差的是在调用类的构造函数来初始化实例的时候,必须在调用构造函数后手动调用init方法来完成实际的构造逻辑:app

要是能把这个init的逻辑放在构造函数内部就行了,但是这样的话就会违背前面说的构造函数无参无逻辑的原则。换一种方式来考虑,这个原则的目的是为了保证在实例化父类做为子类原型的时候用网盘资源www.sosuopan.com,调用父类的构造函数不会出错,那么就能够稍微打破一下这个原则,在类的构造函数里添加少许的而且必定不会有问题的逻辑来解决:模块化

调用结果仍然和前面的例子同样。可是这个实现还有一个小问题,它引入了一个全局变量initializing,要是能把引入这个全局变量就行了,这个其实很好解决,只要咱们把关于类的构建跟继承,封装成一个模块,而后把这个变量放在模块的内部,就没有问题了。

3)在构造子类的时候,是把子类的原型设置成了父类的一个实例,这个是不符合语义的,继承应该发生在类与类之间,而不是类与实例之间。之因此要用父类的一个实例来做为子类的原型:

彻底是由于父类的这个实例,指向父类的原型,而子类的实例又会指向子类的原型,因此最终子类的实例就能经过原型链访问到父类原型上的方法。这个作法虽然能实现实例方法的继承,可是它不符合语义,并且它还有一个很大的问题就是会增长原型链的长度,致使子类在调用父类方法时,必须经过原型链的查找到父类的方法才行。要是继承层次较深,会对js的执行性能有些影响。

解决方式:在解决这个问题以前,先想一想继承能帮咱们解决什么问题:从父类复用已有的实例属性和实例方法。在javascript面向对象编程中,一直有一个原则就是,实例属性都写在构造函数或者实例方法里面,实例方法写在原型上面,也就是说类的原型,按照这个原则来讲,就是用来写实例方法的,并且是只用来写实例方法,那么咱们彻底能够在构建子类时,经过复制的方式将父类原型的全部方法所有添加到子类的原型上,不必定要把父类的一个实例设置成子类的原型,这样就能将原型链的长度大大地缩短,借助一个简短的copy函数,咱们就能轻松对前面的代码进行改造:

 

这么作了之后,当调用m.toString的时候其实调用的是Manager类自身原型上的方法,而不是Employee类的实例方法,缩短了在原型链上查找方法的距离。这个作法在性能上有很大的优势,但很差的是经过原型链维持的继承关系其实已经断了,子类的原型和子类的实例都没法再经过js原生的属性访问到父类的原型,因此这个调用console.log(m instanceof Employee)输出的是false。不过跟性能比起来,这个均可以不算问题:一是instanceOf的运算,几乎在javascript的开发里面用不到,至少我是没碰到过;二是经过复制方式彻底可以把父类的实例方法继承下来,这就已经达到了继承的最大目的。

这个方法还有一个额外的好处是,解决了第2个问题最后提到的引入initializing全局变量的问题,若是是复制的话,就不须要在构建继承关系时,去调用父类的构造函数,那么也就没有必要在构造函数内先判断initializing才能去调用init方法,上面的代码中就已经去掉了initializing这个变量的处理。

4)在子类的构造函数和实例方法内若是想要调用父类的构造函数或者方法,显得比较繁琐:

每次都得靠apply借用方法来处理。要是能改为以下的调用就好用多了:

解决方式:若是要在每一个实例方法里,都能经过this.base()调用父类原型上相应的方法,那么this.base就必定不是一个固定的方法,须要在每一个实例方法执行期间动态地将this.base指定为父类原型的同名方法,可以作到这个实现的方式,就只有经过方法代理了,前面的Employee和Manager的例子能够改造以下:

经过代理的方式,就解决了在在实例方法内部经过this.base调用父类原型同名方法的问题。但是在实际状况中,每一个实例方法都有可能须要调用父类的实例,那么每一个实例方法都要添加一样的代码,显然这会增长不少麻烦,好在这部分的逻辑也是一样的,咱们能够把它抽象一下,最后都放到模块化的内部去,这样就能简化代理的工做。

5)未考虑静态属性和静态方法。尽管静态成员是不须要继承的,但在有些场景下,咱们仍是须要静态成员,因此得考虑静态成员应该添加在哪里。

解决方式:因为js原生并不支持静态成员,因此只能借助一些公共的位置来处理。最佳的位置是添加到构造函数上:

最后的两行输出了正确的实例id,而这个id是经过Employee类的静态方法生成的。在java的面向对象编程中,子类跟父类均可以定义静态成员,在调用的时候还存在覆盖的问题,在js里面,由于受语言的限制,自定义的静态成员不可能实现全面的面向对象功能,就像上面这种,可以给类提供一些公共的属性和公共方法,就已经足够了。

2. 指望的调用方式

从第1部分的分析能够看出,在js里面,类的构建与继承,有不少通用的逻辑,彻底能够把这些逻辑封装成一个单独的模块,造成一个通用的类库,以便在工做中有须要的时候,均可以直接拿来使用。这个类库要求能完成咱们须要的功能(类的构建与继承和静态成员的添加),同时在使用时要足够简洁方便。在利用bootstrap的modal组件自定义alert,confirm和modal对话框这篇文章里,我曾说过一些从组件指望的调用方式,去反推组件实现的一些观点,当你明确你须要什么东西时,你才知道这个东西你该怎么去创造。本文要编写的这个继承组件也会采起这个方法来实现,我先用前面Employee和Manager的例子来模拟调用这个继承库的场景,经过预设的一些组件名称或者接口名称以及调用方式,来尝试走通真实使用这个继承库的流程,有了这个东西,下一步我只须要根据这个要求去实现便可,模拟以下:

从模拟的结果来看,我想要的继承库对外提供的名称只有Class, instanceMembers, staticMembers和extend而已,调用方式也很简单,只要传递参数给Class函数便可。接下来就按照这个目标,看看如何一步步根据第一部分罗列的那些问题和解决方式,把这个库给写出来。

3. 继承库的详细实现

根据API名称和接口以及前面第1部分提出的问题,这个继承库要完成的功能有:

1)类的构建(关键:init方法)和静态成员处理;

2)继承关系的构建(关键:父类原型的复制);

3)父类方法的简化调用(关键:父类原型上同名方法的代理)。

因此这个库的实现,能够按照这三点分红三版来开发。

1)初版

在初版里面,仅须要实现类的构架和静态成员添加的功能便可,细节以下:

这一版核心代码在于类的构建和静态成员添加的部分,其它代码仅仅提供一些提早能够想到的赋值函数和变量(isObject, isFunction),并作一些参数合法性校验的处理。添加静态成员的代码必定要在设置原型的代码以前,不然就有原型被覆盖的风险。有了这个版本,就能够直接构建带静态成员的Employee类了:

在getId方法中之因此直接使用this就能访问到构造函数Employee,是由于getId这个方法是添加到构造函数上的,因此当调用Employee.getId()时,getId方法里面的this指向的就是Employee这个函数对象。

第二版在初版的基础上,实现继承关系的构建部分:

这一版关键的部分在于:

image

image

this.baseProto主要目的就是为了让子类的实例可以有一个属性能够访问到父类的原型,由于后面的继承方式是复制方式,会致使原型链断裂。有了这一版以后,就能够加入Manager类来演示效果了:

不过在Manager内部,调用父类的方法时仍是apply借用的方式,因此在最后一版里面,须要把它变成咱们指望的this.base的方式,反正原理前面也已经了解了,无非是在方法同名的时候,对实例方法加一个代理而已,实现以下:

核心部分是:

image

只有当须要继承父类,且父类原型中有方法与当前的实例方法同名时,才会去对当前的实例方法添加代理。更详细的原理能够回到文章第1部分回顾相关内容。至此,咱们在Manager类内部调用父类的方法时,就很简单了,只要经过this.base便可:

注意这两处调用:

image

以上就是本文要实现的继承库的所有细节,其实它所作的事就是把本文第1部分提到的那些问题的解决方式和第二部分模拟的调用场景结合起来,封装到一个模块内部而已,各个细节的原理只要理解了第1部分总结的那些解决方式就很掌握了。在最后一版的演示中,也能看到,本文实现的这个继承库,已经彻底知足了模拟场景中的需求,从此有任何须要用到继承的场景,彻底能够拿最后一版的实现去开发。

相关文章
相关标签/搜索