在面向对象编程中,继承是非常实用也非常核心的功能,这一切都基于面向类语言中的类。然而,javascript和面向类的语言不同,它没有类作为蓝图,javascript中只有对象,但抽象继承思想又是如此重要,于是聪明绝顶的javascript开发者们就利用javascript原型链的特性实现了和类继承功能一样的继承方式。,要想弄清楚原型链,我们得先把原型搞清楚,原型可以理解为是一种设计模式。以下是《你不知道的javascript》对原型的描述:,《javascript高级程序设计》这样描述原型:,我们通过一段代码来理解这两段话:,这是上面这段代码在chrome控制台中显示的结果:,可以看到,我们先是创建了一个空的构造函数Person,然后创建了一个Person的实例hjy,hjy本身是没有挂载任何属性和方法的,但是它有一个[[Prototype]]内置属性,这个属性是个对象,里面有name、age属性和getName函数,定睛一看,这玩意儿可不就是上面写的Person.prototype对象嘛。,事实上,Person.prototype和hjy的[[Prototype]]都指向同一个对象,这个对象对于Person构造函数而言叫做原型对象,对于hjy实例而言叫做原型。下面一张图直观地展示上述代码中构造函数、实例、原型之间的关系:,因此,构造函数、原型和实例的关系是这样的:每个构造函数都有一个原型对象(实例的原型),原型有一个constructor属性指回构造函数,而实例有一个内部指针指向原型。,在chrome、firefox、safari浏览器环境中这个指针就是__proto__,其他环境下没有访问[[Prototype]]的标准方式。,这其中还有更多细节建议大家阅读《javascript高级程序设计》,在上述原型的基础上,如果hjy的原型是另一个类型的实例呢?于是hjy的原型本身又有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数。这样,实例和原型之间形成了一条长长的链条,这就是原型链。,在原型链中,如果在对象上找不到需要的属性或者方法,引擎就会继续在[[Prototype]]指向的原型上查找,同理,如果在后者也没有找到需要的东西,引擎就会继续查找它的[[Prototype]]指向的原型。上图理解一下:,以咱们人类为例,咱全地球人都是一个脑袋、双手双脚,很多基本特征都是一样的。但人类也可以细分种类,有黄种人、白种人、黑种人,咱们如果要定义这三种人,无需再说一个脑袋、双手双脚之类的共同特征,黄种人就是在人类的基础上将皮肤变为黄色,白种人皮肤为白色,黑种人为黑色,如果有其他特征就再新增即可,例如蓝眼睛、黄头发等等。,如果用代码封装,咱们就可以将人类定义为基类或者超类,拥有脑袋、手、足等属性,说话、走路等行为。黄种人、白种人、黑种人为子类,自动复制父类的属性和行为到自身,然后在此基础上新增或者重写某些属性和行为,例如黄种人拥有黄皮肤、黑头发。这就是继承的思想。,在其他面向类语言中,继承意味着复制操作,子类是实实在在地将父类的属性和方法复制了过来,但javascript中的继承不是这样的。,根据原型的特性,js中继承的本质是一种委托机制,对象可以将需要的属性和方法委托给原型,需要用的时候就去原型上拿,这样多个对象就可以共享一个原型上的属性和方法,这个过程中是没有复制操作的。,javascript中的继承主要还是依靠于原型链,原型处于原型链中时即可以是某个对象的原型也可以是另一个原型的实例,这样就能形成原型之间的继承关系。,然而,依托原型链的继承方式是有很多弊病的,我们需要辅以各种操作来消除这些缺点,在这个探索的过程中,出现了很多通过改造原型链继承而实现的继承方式。,直接利用原型链特征实现的继承,让构造函数的prototype指向另一个构造函数的实例。,上述代码中的Person构造函数、YellowRace构造函数、hjy实例之间的关系如下图:,根据原型链的特性,当我们查找hjy实例的head和hand属性时,由于hjy本身并没有这两个属性,引擎就会去查找hjy的原型,还是没有,继续查找hjy原型的原型,也就是Person原型对象,结果就找到了。就这样,YellowRace和Person之间通过原型链实现了继承关系。,但这种继承是有问题的:,针对第二点,我们通过一段代码来看一下:,可以看到,hjy只是想给自己的生活增添一点绿色,但是却被laowang给享受到了,这肯定不是我们想看到的结果。,为了解决不能传参以及引用类型属性共享的问题,一种叫盗用构造函数的实现继承的技术应运而生。,盗用构造函数也叫作“对象伪装”或者“经典继承”,原理就是通过在子类中调用父类构造函数实现上下文的绑定。,上述代码中,YellowRace在内部使用call调用构造函数,这样在创建YellowRace的实例时,Person就会在YellowRace实例的上下文中执行,于是每个YellowRace实例都会拥有自己的colors属性,而且这个过程是可以传递参数的,Person.call()接受的参数最终会赋给YellowRace的实例。它们之间的关系如下图所示:,虽然盗用构造函数解决了原型链继承的两大问题,但是它也有自己的缺点:,针对第二点,我们看一段代码:,可以看到,hjy实例能继承Person构造函数内部的方法getEyes(),对于Person原型对象上的方法,hjy是访问不到的。,原型链继承和盗用构造函数继承都有各自的缺点,而组合继承综合了前两者的优点,取其精华去其糟粕,得到一种可以将方法定义在原型上以实现重用又可以让每个实例拥有自己的属性的继承方案。,组合继承的原理就是先通过盗用构造函数实现上下文绑定和传参,然后再使用原型链继承的手段将子构造函数的prototype指向父构造函数的实例,代码如下:,hjy终于松了口气,自己终于能独享生活的一点“绿”,再也不会被老王分享去了。,此时Person构造函数、YellowRace构造函数、hjy和laowang实例之间的关系如下图:,相较于盗用构造函数继承,组合继承额外的将YellowRace的原型对象(同时也是hjy和laowang实例的原型)指向了Person的原型对象,这样就集合了原型链继承和盗用构造函数继承的优点。,但组合继承还是有一个小小的缺点,那就是在实现的过程中调用了两次Person构造函数,有一定程度上的性能浪费。这个缺点在最后的寄生式组合继承可以改善。,文章最终给出了一个函数:,其实不难看出,这个函数将原型链继承的核心代码封装成了一个函数,但这个函数有了不同的适用场景:如果你有一个已知的对象,想在它的基础上再创建一个新对象,那么你只需要把已知对象传给object函数即可。,ES5新增了一个方法Object.create()将原型式继承规范化了。相比于上述的object()方法,Object.create()可以接受两个参数,第一个参数是作为新对象原型的对象,第二个参数也是个对象,里面放入需要给新对象增加的属性(可选)。,第二个参数与Object.defineProperties()方法的第二个参数是一样的,每个新增的属性都通过自己的属性描述符来描述,以这种方式添加的属性会遮蔽原型上的同名属性。当Object.create()只传入第一个参数时,功效与上述的object()方法是相同的。,稍微需要注意的是,object.create()通过第二个参数新增的属性是直接挂载到新建对象本身,而不是挂载在它的原型上。原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。,上述代码中各个对象之间的关系仍然可以用一张图展示:,这种关系和原型链继承中原型与实例之间的关系基本是一致的,不过上图中的F构造函数是一个中间函数,在object.create()执行完后它就随着函数作用域一起被回收了。那最后hjy的constructor会指向何处呢?下面分别是浏览器和node环境下的打印结果:,查阅资料得知chrome打印的结果是它内置的,不是javascript语言标准。具体是个啥玩意儿我也不知道了?。,既然原型式继承和原型链继承的本质基本一致,那么原型式继承也有一样的缺点:,寄生式继承与原型式继承很接近,它的思想就是在原型式继承的基础上以某种方式增强对象,然后返回这个对象。,这是一个最简单的寄生式继承案例,这个例子基于hjy对象返回了一个新的对象laowang,laowang拥有hjy的所有属性和方法,还有一个新方法sayHai()。,可能有的小伙伴就会问了,寄生式继承就只是比原型式继承多挂载一个方法吗?这也太low了吧。其实没那么简单,这里只是演示一下挂载一个新的方法来增强新对象,但我们还可以用别的方法呀,比如改变原型的constructor指向,在下面的寄生式组合继承中就会用到。,寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路就是使用寄生式继承来继承父类的原型对象,然后将返回的新对象赋值给子类的原型对象。,首先实现寄生式继承的核心逻辑:,这里没有将新建的对象返回出来,而是赋值给了子类的原型对象。,接下来就是改造组合式继承,将第二次调用构造函数的逻辑替换为寄生式继承:,上述寄生式组合继承只调用了一次Person造函数,避免了在Person.prototype上面创建不必要、多余的属性。于此同时,原型链依然保持不变,效率非常之高效。,如图,寄生组合式继承与组合式继承中的原型链关系是一样的:,原型与实例的关系可以用两种方式来确定:instanceof操作符和isPrototypeOf()方法。,instanceof操作符左侧是一个普通对象,右侧是一个函数。,以o instanceof Foo为例,instanceof关键字做的事情是:判断o的原型链上是否有Foo.prototype指向的对象。,根据instanceof的特性,我们可以实现一个自己instanceof,思路就是递归获取左侧对象的原型,判断其是否和右侧的原型对象相等,这里使用Object.getPrototypeOf()获取原型:,isPrototypeOf()不关心构造函数,它只需要一个可以用来判断的对象就行。以Foo.prototype.isPrototypeOf(o)为例,isPrototypeOf()做的事情是:判断在a的原型链中是否出现过Foo.prototype。,在实现各种继承方式的过程中,经常会用到new关键字,那么new关键字起到的作用是什么呢?,简单来说,new关键字就是绑定了实例与原型的关系,并且在实例的的上下文中调用构造函数。下面就是一个最简版的new的实现:,实际上,真正的new关键字会做如下几件事情:,代码如下:,有些小伙伴可能会疑惑最后这个判断是为了什么?因为语言的标准肯定是严格的,需要考虑各种情况下的处理。比如const res = Fn.apply(o, args)这一步,如果构造函数有返回值,并且这个返回值是对象或者函数,那么new的结果就应该取这个返回值,所以才有了这一层判断。,之前对原型链的概念一直模模糊糊,这个写作探索的过程中对知识的巩固理解非常有帮助。如果看完此文对你有点帮助,还请手下留赞?,感谢感谢。
© 版权声明
文章版权归作者所有,未经允许请勿转载。