前言原型链与继承、作用域与闭包、单线程与异步并称为前端三座大山,都属于JavaScript基础但很复杂的部分,面试也问了经常。今天,我们就来详细介绍一下原型链和继承,说说它的概念、作用和用法。如果这部分你已经学过,只是有点忘记了,可以直接跳到原型链图上复习一下。接下来,让我们一步步介绍原型链和继承。知道原型当我们创建一个函数时,我们会自动为其创建一个原型属性,指向该函数的原型对象。所有原型对象也会自动获得一个名为constructor的属性,该属性指向与其关联的构造函数。我们所说的原型一般指的是__proto__或prototype属性。也有人将prototype属性称为显示原型,将__proto__称为隐式原型。两者有什么区别和关系?prototype是我们在创建函数的时候自动给函数加上的一个属性,它会指向函数的prototype对象。__proto__是所有对象都具有的私有属性。指向其构造函数的原型对象,看看这是不是混淆了,新增两个概念:什么是构造函数和原型对象?构造函数:构造函数也是一个函数,与普通函数的唯一区别是它的调用方式。凡是用new运算符调用的函数都是构造函数,没有用new运算符调用的函数就是普通函数。一般构造函数的首字母都会大写,比如Object()Array()Function()等。原型对象:原型对象是用构造函数创建的,与普通对象没有区别,但原型对象在创建时会自动获得一个构造函数属性,指向关联的构造函数。每次使用构造函数创建实例对象时,都会将实例内部的[[Prototype]]指针赋值为构造函数的原型对象。目前各大浏览器都在每个对象上暴露了__proto__属性来访问实例的原型。看代码可能会更好理解:constobj=newObject()console.log(obj['__proto__']===Object.prototype)//trueconsole.log(Object.prototype.constructor===Object)//true备注:Object.create(null)创建的对象是特例,不具有__proto__属性;es6中新的箭头函数没有prototype属性,不能作为构造函数使用;虽然Symbol和BigInt不能通过new调用,但是它有一个原型属性。您对继承有任何疑问吗?当我们创建一个对象时,上面显然没有属性,但是我们仍然可以调用很多方法,比如toStringhasOwnProperty...constobj={}console.log(obj.toString())//'[objectObject]'console.log(obj.hasOwnProperty('a'))//false其实所有对象都是Object的实例,obj={}完全等同于obj=newObject()。我们在控制台展开这个对象,发现它的[[Prototype]]接口指向的是Object,上面的方法也在里面。为了区分方便,以下将普通对象称为实例。测试一下,实例上的toString方法指向其原型上的方法,其原型上添加的属性也可以通过实例直接访问。constobj={}console.log(obj.toString===obj['__proto__'].toString)//trueobj['__proto__'].a=1console.log(obj.a)//1个实例可以访问其原型对象上的属性和方法,这就是继承。但是其原型的属性和方法是不能通过实例修改的,操作实例的属性只会创建或修改实例上的属性。如果实例具有与其原型相同的属性,那么原型对象上的同名属性将被隐藏。constobj={}obj['__proto__'].a=1obj.a=2console.log(obj['__proto__'].a)//1console.log(obj.a)//我们知道的2个原型链Eachinstance有一个原型对象,可以通过实例的__proto__属性访问。但是原型对象也是一个对象,属于上一层构造函数的实例,也有__proto__属性,指向上一层的原型。当我们访问实例属性时,如果没有实例,我们将访问它的__proto__,如果没有原型对象,我们将访问原型的原型,并逐层访问。我们把这个由__proto__属性组成的原型路径称为原型链。但是,沿着原型一路攀升是无止境的。不禁要问:原型链的尽头是什么?要回答这个问题,必须从实际出发。原型对象是为了方便我们使用和继承一些属性和方法而设计的。Object是所有对象的基类,我们通常不会直接访问或使用Object的原型,所以为它设计一个更高层的原型对象是没有意义的,所以Object.prototype的原型为null,即原型链的末端。从控制台打印Object.prototype也可以看出其__proto__属性值为null。函数也是对象函数和特殊对象。所有函数都是Function()的实例。你可能根本没见过Function(),但实际上,除了第一种有变量提升之外,下面三种创建函数的方式是完全等价的。我们只是习惯于使用速记。函数fun1(a,b){控制台。log(a,b)}fun1(1,2)//12constfun2=function(a,b){console.log(a,b)}fun2(1,2)//12constfun3=newFunction('a','b','console.log(a,b)')fun3(1,2)//12函数也有__proto__属性,指向FunctionFunction的原型,Function的原型也是一个对象,属于Object的实例。console.log(fun1['__proto__']===Function.prototype)//trueconsole.log(Function.prototype['__proto__']===Object.prototype)//true但除非另有说明,一般来说函数的原型对象,引用其原型属性。使用原型学习原型,那么如何使用呢?一般有以下几种用法:检查类型instanceof运算符根据原型进行操作,检查一个对象是否是一个函数的实例,可以用来区分对象和数组。constarr=[]constobj={}console.log(typeofarr)//'object'console.log(typeofobj)//'object'console.log(arrinstanceofArray)//trueconsole.log(objinstanceofArray)//falseconsole.log(arr['__proto__']===Array.prototype)//true扩展原型可以通过原型扩展实例方法,比如我们认为要获取一个数的绝对值,打电话给数学。abs()过于繁琐,不能直接扩展Number的原型。Number.prototype.abs=function(){returnMath.abs(this.valueOf())}letnum=-1console.log(num.abs())//1和Vue2中的全局事件总线也是扩展的Prototype对于Vue。//创建全局事件总线Vue.prototype.$bus=this//注册事件this.$bus.$on('eventName',(data)=>{})//触发事件this.$bus.$emit('eventName','data')原型模式可以通过构造函数批量创建实例,并让它们共享属性和方法。functionPerson(name,age){if(name!=undefined)this.name=nameif(age!=undefined)this.age=age}Person.prototype.age=18Person.prototype.say=function(){控制台.log(this.name,this.age)}constxiaoming=newPerson('Xiaoming',18)xiaoming.say()//小明18constxiaolan=newPerson('Xiaolan',17)xiaolan.say()//xiaolan17constxiaohong=newPerson('Xiaohong')xiaohong.say()//Xiaoming18console.log(xiaominginstanceofPerson&&xiaolaninstanceofPerson&&xiaohonginstanceofPerson)//true修改原型可以修改原型获取方法。consttime=function(){//修改原型为数组,使用map方法arguments['__proto__']=Array.prototypereturnarguments.map((item)=>(String(item).length<2?'0'+item:item)).join('-')}console.log(time(2022,5,20,12,0,0))//2022-05-20-12-00-00但是一般不建议直接修改原型,因为修改原型容易导致逻辑混乱。如果想获取某个原型的方法,推荐使用Object.create()继承其实例。constxiaoming=newPerson('Xiaoming',18)xiaoming.say()//小明18constxiaoming2=Object.create(xiaoming)xiaoming2.name='xiaoming'xiaoming2.age='19'xiaoming2.say()//Smallname19console.log(xiaoming2['__proto__']===xiaoming)//true对于前面的例子,建议使用Array.from()转为数组。consttime=function(){constarr=Array.from(arguments)arr['__proto__']=Array.prototypereturnarr.map((item)=>(String(item).length<2?'0'+item:item)).join('-')}原型链图片用一张图来展示原型链的关系。希望看完这篇文章,你也能轻松画出下图:总结各个原型之间的关系可能会有点混乱,但只要看懂了,其实并不难。总结一下:所有的对象都有一个__proto__属性,指向它的原型对象,通过对象可以访问它的原型属性。沿着__proto__的路径是原型链。原型链的末尾为null(Object.prototype['__proto__'])。所有函数在创建时都会自动创建它们的原型对象,并将它们赋值给函数的原型属性。所有的函数也是Function的实例,它们的__proto__属性指向Function的原型对象。您可以通过扩展函数的原型为其实例添加属性和方法。Object.create(null)创建的对象没有任何属性;es6中加入的箭头函数没有prototype属性,不能作为构造函数使用,但它仍然是Function的一个实例。结论文中如有错误或不准确之处,请务必指正,万分感谢。如果喜欢或者受到启发,欢迎点赞关注,鼓励新人。
