当前位置: 首页 > 科技观察

JS从原型链到继承——详解来龙去脉_0

时间:2023-03-13 09:05:40 科技观察

前言在面向对象编程中,继承是一个非常实用和核心的功能,在面向类的语言中,继承都是基于类的。但是,javascript不同于面向类的语言。它没有类作为蓝图。javascript中只有对象,但是抽象继承的思想是如此重要,所以聪明的javascript开发者利用javascript原型链的特性,实现了和类继承在功能上相同的继承方式。什么是原型我们要了解原型链,首先要了解原型,可以理解为一种设计模式。《你不知道的javascript》是这样描述原型的:javascript中的对象有一个特殊的[[Prototype]]内置属性,它实际上是对另一个对象的引用。几乎所有对象都是用[[Prototype]]分配的非空值创建的。《javascript高级程序设计》是这样描述原型的:每个函数都会创建一个原型属性,它是一个对象,包含应该由特定引用类型的实例共享的属性和方法。其实这个对象就是调用构造函数创建的对象的原型。使用原型对象的好处是定义在其上的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋给它们的原型。这两段我们通过一段代码来理解:functionPerson(){}//在Person的原型对象上挂载属性和方法Person.prototype.name='funnyduck'Person.prototype.age=22Person.prototype。getName=function(){returnthis.name}consthjy=newPerson()console.log('hjy:',hjy)console.log('getName:',hjy.getName())这是上面的代码chrome控制台显示的结果:可以看到,我们首先创建了一个空的构造函数Person,然后创建了一个Person的实例hjy,hjy本身并没有挂载任何属性和方法,但是它有一个[[Prototype]]内置-在property中,这个属性是一个对象,里面包含了name、age属性和getName函数,仔细一看,这个东西不是上面写的Person.prototype对象。实际上,Person.prototype和hjy的[[Prototype]]都指向同一个对象,Person构造函数称为原型对象,hjy实例称为原型对象。下图形象地展示了上述代码中构造函数、实例和原型之间的关系:因此,构造函数、原型和实例之间的关系是这样的:每个构造函数都有一个原型对象(实例的原型),Prototypes有一个constructor属性指向构造函数,而实例有一个指向原型的内部指针。在chrome、firefox、safari浏览器环境下,这个指针是__proto__,在其他环境下没有标准的方式来访问[[Prototype]]。里面有更多的细节。推荐大家阅读《javascript高级程序设计》原型链在上面的原型基础上,如果hjy的原型是另外一种实例呢?所以hjy的原型本身有一个指向另一个原型的内部指针,对应的other原型也有一个指向另一个构造函数的指针。这样在实例和原型之间就形成了一条长链,这就是原型链。所有普通的[[Prototype]]都会指向内置的Object.prototype,而Object的[[Prototype]]指向null。也就是说,所有的普通对象都派生自Object.prototype,其中包含了很多javascript中常用的函数。在原型链中,如果在对象上找不到需要的属性或方法,引擎会继续在[[Prototype]]指向的原型上搜索。同样,如果在后者中没有找到需要的对象,引擎会继续查找其[[Prototype]]所指向的原型。理解上图:理解继承继承是面向对象编程的三大特性(封装、继承、多态)之一。当多个类中存在相同的属性和行为时,将这些内容提取到一个类中,那么多个类就不需要定义这些属性和行为,只需要继承那个类即可。多个类可以称为子类,这个单个类称为父类或超类、基类等。子类可以直接访问父类中的非私有属性和行为。以我们人类为例,我们都有一个头、两只手和两只脚,很多基本特征都是相同的。但是人类也可以细分为黄种人、白种人、黑种人。如果要给这三类人下定义,就不用说头、手、脚这些共同的特征了。黄种人是人类的基础。将皮肤更改为黄色,高加索人为白色皮肤,黑人为黑色。如果还有其他特征,就加上,比如蓝眼睛、黄头发等。如果封装在代码中,我们可以把人定义为一个基类或超类,有头、手、脚等属性,有说话、走路等行为。黄种人、白种人、黑人是子类,自动将父类的属性和行为复制给自己,然后在此基础上添加或改写某些属性和行为。例如,黄种人的皮肤是黄色的,头发是黑色的。这就是继承的思想。js中的继承(原型继承)在其他面向类的语言中,继承就是复制操作,子类实际上是复制父类的属性和方法,但是javascript中的继承不是这样的。根据原型的特点,js中继承的本质是委托机制。对象可以将需要的属性和方法委托给原型,需要的时候从原型中获取,这样多个对象就可以共享一个原型和方法上的属性,这个过程没有拷贝操作。javascript中的继承主要依赖于原型链。当原型在原型链中时,它可以是一个对象的原型,也可以是另一个原型的实例,这样就可以形成原型之间的继承关系。但是依赖原型链的继承方式有很多缺点,我们需要通过各种操作来消除这些缺点。在探索过程中,出现了很多通过改造原型链的继承实现的继承方式。js的六种继承方式原型链继承直接利用原型链的继承特性,让构造函数的原型指向构造函数的另一个实例。functionPerson(){this.head=1this.hand=2}functionYellowRace(){}YellowRace.prototype=newPerson()consthjy=newYellowRace()console.log(hjy.head)//1console.log文件(hjy.hand)//2上面代码中的Person构造函数、YellowRace构造函数和hjy实例之间的关系如下:根据原型链的特点,当我们查找head和hand属性时hjy实例,因为hjy本身没有这两个属性,引擎会去寻找hjy的原型,如果没有,继续寻找hjy原型的原型,也就是Person原型对象,而它会找到它。这样,通过原型链实现了YellowRace和Person的继承关系。但是这种继承是有问题的:创建hjy实例时不能传递参数,即YellowRace构造函数本身不接受参数。当原型上的属性是引用数据类型时,所有实例都会共享这个属性,即一个实例重写这个属性会影响到其他实例。对于第二点,我们来看一段代码:()consthjy=newYellowRace()hjy.colors.push('green')console.log(hjy.colors)//['white','yellow','black','green']constlaowang=newYellowRace()console.log(laowang.colors)//['white','yellow','black','green']可以看出hjy只是想给自己的生活加点绿色,老王却很享受,这绝对不是我们想看到的结果。为了解决引用类型不能传递参数和共享属性的问题,一种叫做窃取构造函数实现继承的技术应运而生。窃取构造函数窃取构造函数也称为“对象伪装”或“经典继承”。其原理是通过在子类中调用父类构造函数来实现上下文绑定。functionPerson(eyes){this.eyes=eyesthis.colors=['white','yellow','black']}functionYellowRace(){Person.call(this,'black')//调用构造函数和传递参数}consthjy=newYellowRace()hjy.colors.push('green')console.log(hjy.colors)//['white','yellow','black','green']console.log(hjy.eyes)//blackconstlaowang=newYellowRace()console.log(laowang.colors)//['white','yellow','black']console.log(laowang.eyes)//black以上代码,YellowRace内部使用call来调用构造函数,这样在创建YellowRace实例的时候,Person会在YellowRace实例的上下文中执行,所以每个YellowRace实例都会有自己的colors属性,这个过程可以传递参数,Person.call()接受的参数最终将分配给YellowRace的实例。它们之间的关系如下图所示:窃取构造函数虽然解决了原型链继承的两大问题,但也有其自身的缺点:必须在构造函数中定义方法,窃取构造函数继承的方法本质上是变了。它成为实例自己的方法,而不是公共方法,因此失去了可重用性。子类无法访问父类原型上定义的方法,所以所有类型只能使用构造函数模式。如上图所示,YellowRace的构造函数、hjy和laowang实例都没有连接到Person的原型对象上。对于第二点,我们看一段代码:.eyes}functionYellowRace(){Person.call(this,'black')}consthjy=newYellowRace()console.log(hjy.getEyes())//blackconsole.log(hjy.ReturnEyes())//TypeError:hjy.ReturnEyesisnotafunction可见hjy实例可以继承Person构造函数内部的方法getEyes(),而hjy无法访问Person原型对象上的方法。组合继承原型链继承和被盗构造函数继承各有缺点,而组合继承结合了前两者的优点,取其精华去其糟粕,得到一个可以定义在原型上的方法,实现复用,允许每个实例有自己的属性继承方案。组合继承的原理是先通过窃取构造函数实现上下文绑定和参数传递,然后利用原型链继承将子构造函数的原型指向父构造函数的实例。代码如下:functionPerson(eyes){this.眼睛=眼睛this.colors=['white','yellow','black']}Person.prototype.getEyes=function(){returnthis.eyes}functionYellowRace(){Person.call(this,'black')//调用构造函数并传递参数}YellowRace.prototype=newPerson()//再次调用构造函数consthjy=newYellowRace()hjy.colors.push('green')constlaowang=newYellowRace()console.log(hjy.colors)//['white','yellow','black','green']console.log(laowang.colors)//['white','yellow','black']console.log(hjy.getEyes())//blackhjy终于松了一口气,他终于可以独享一点“绿色”的生活了,再也不会被老王分享了。此时Person构造函数、YellowRace构造函数、hjy和laowang实例之间的关系如下:组合继承相对于窃取构造函数继承,额外指向了YellowRace的原型对象(也是hjy和laowang实例的原型)到Person的原型对象,结合了原型链继承和窃取构造函数继承的优点。但是组合继承还是有一个小缺点,就是Person的构造函数在实现过程中被调用了两次,存在一定的性能浪费。这个缺点可以在最后的寄生组合继承中得到改善。原型继承2006年,DouglasCrockford写了一篇文章《Javascript中的原型式继承》。本文介绍一种严格意义上不涉及构造函数的继承方法。他的出发点是即使没有自定义类型,也可以通过原型实现对象间的信息共享。文章最后给出了一个函数:constobject=function(o){functionF(){}F.prototype=oreturnnewF()}其实不难看出这个函数封装了prototype的核心代码chaininheritanceintoAfunction,但是这个函数有不同的适用场景:如果你有一个已知的对象,想基于它创建一个新的对象,那么你只需要将这个已知对象传递给object函数即可。constobject=function(o){functionF(){}F.prototype=oreturnnewF()}consthjy={eyes:'black',colors:['white','yellow','black']}}constlaowang=object(hjy)console.log(laowang.eyes)//blackconsole.log(laowang.colors)//['white','yellow','black']ES5添加了一个新方法Object.create()规范化原型继承。与上面的object()方法相比,Object.create()可以接受两个参数,第一个参数是object作为新对象的原型,第二个参数也是一个对象,需要添加到new中对象属性(可选)。第二个参数与Object.defineProperties()方法的第二个参数相同。每个新添加的属性都由其自己的属性描述符来描述。以这种方式添加的属性将隐藏原型上具有相同名称的属性。当Object.create()只传入第一个参数时,效果和上面的object()方法是一样的。consthjy={eyes:'black',colors:['white','yellow','black']}constlaowang=Object.create(hjy,{name:{value:'老王',writable:false,可枚举:true,可配置:true},age:{value:'32',writable:true,可枚举:true,可配置:false}})console.log(laowang.eyes)//blackconsole.log(laowang.colors)//['white','yellow','black']console.log(laowang.name)//老王console.log(laowang.age)//32需要注意的是object.create()传递的属性第二个参数添加的对象直接挂载在新创建的对象本身上,而不是挂载在它的原型上。原型继承非常适合不需要创建单独的构造函数,但仍然需要在对象之间共享信息的情况。上面代码中各个对象之间的关系还是可以用一张图来表示:这种关系和原型链继承中的原型和实例的关系基本是一样的,只是上图中的F构造函数是一个中间功能。在执行object.create()后,它与函数作用域一起被回收。hjy的构造函数最后会指向哪里呢?以下是浏览器和node环境下的打印结果:根据资料,chrome的打印结果是内置的,不是javascript语言标准。我不知道它到底是什么。