当前位置: 首页 > Web前端 > JavaScript

吃透JS原型和原型链_0

时间:2023-03-27 12:30:28 JavaScript

说到JavaScript原型和原型链,相关文章很多,但大多晦涩难懂。本文将从不同的角度入手,先了解原型和原型链是什么以及它们的作用,然后分析那些麻烦的关系。1、引用类型都是对象原型,原型链是从对象的概念衍生出来的,为对象服务,所以我们首先要明确一点:JavaScript中所有的引用类型都是对象,而对象是属性的集合。Array类型、Function类型、Object类型、Date类型、RegExp类型等都是引用类型。也就是说,数组是对象,函数是对象,正则表达式是对象,对象还是对象。2、什么是原型和原型链?上面我们提到,对象是属性的集合。可能有人会问有方法吗?其实方法也是一个属性,因为它也是键值对的表示,如下图所示。可见obj确实多了一个sayHello属性,取值是一个函数,但是问题来了,obj上没有hasOwnProperty方法,为什么我们可以调用呢?这导致了原型。每个对象自创建以来就与另一个对象相关联,并从另一个对象继承其属性。这个另一个对象是原型。访问对象的属性时,首先在对象本身中寻找,如果找不到,就去对象的原型,如果还是找不到,就去对象的原型(prototype也是一个对象,它也有自己的prototype)向上查找,依此类推,直到找到,或者在最顶层的prototype对象中没有找到,则结束查找,返回undefined。这个对象链和它们的原型被称为原型链。现在我们对原型和原型链有了初步的了解,大家也就明白为什么数组可以使用push、slice等方法,函数可以使用call、bind等方法了,因为在它们的原型链上都能找到对应的方法。OK,总结一下:prototype的意义就是形成一个原型链:所有的引用类型都是对象,每个对象都有一个原型,一个原型也是一个对象,它也有自己的原型,一层一层,形成一个原型链。原型链的意义是继承:在访问对象属性时,如果在对象本身找不到,可以在原型链上一层层查找。说白了,一个对象可以访问其他对象的属性。继承的意思就是属性共享:有两个好处:一是代码复用,字面意思;另一个是可扩展性,不同的对象可以继承相同的属性,或者定义自己的属性。3、创建对象创建对象主要有两种方式,一种是new操作符后面跟着一个函数调用,另一种是字面量表示。目前,我们现在可以理解,所有的对象都是通过new操作符后跟函数调用创建的,字面意思只是语法糖(即本质也是new的,功能不变,使用更多简洁的)。//new运算符后跟函数调用letobj=newObject()letarr=newArray()//文字表示法letobj={a:1}//等同于letobj=newObject()obj.a=1letarr=[1,2]//相当于让arr=newArray()arr[0]=1arr[1]=2Object,Array等都叫构造器,不要怕这个概念,构造器和普通的函数是一样的,没有区别,只是因为这些函数经常被用来在new之后创建对象。在new之后调用一个空函数也会返回一个对象,任何函数都可以作为构造函数。因此,对构造函数更合理的理解应该是函数的构造调用。Number,String,Boolean,Array,Object,Function,Date,RegExp,Error都是函数,都是原生的构造函数,运行时会自动出现在执行环境中。构造函数用于创建特定类型的对象。通过同一个构造函数创建的这些对象具有相同的原型并共享某些方法。例如,所有数组都可以调用push方法,因为它们具有相同的原型。我们自己实现一个构造函数://按照约定,构造函数应该以大写字母开头functionPerson(name){//函数中的this指向构造的对象//构造一个name属性this.name=name//构造一个sayNameMethodthis.sayName=function(){console.log(this.name)}}//使用自定义构造函数Person创建一个对象letperson=newPerson('logan')person.sayName()//输出:logansummaryA:构造函数用于创建对象,同一个构造函数创建的对象具有相同的原型。4.__proto__与原型一切都逃不过真香定律。对相关知识有了初步的了解之后,我们就要试着去理解这些令人头疼的词,看看来回指向的箭头。正如上面总结的,每个对象都有一个原型,那么我们如何得到一个对象的原型呢?那就是对象的__proto__属性,它指向对象的原型。上面总结,所有的引用类型都是对象,所以引用类型有__proto__属性,对象有__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,都有__proto__属性,都指向他们各自的原型对象。虽然__proto__属性在ECMAScript6语言规范中进行了标准化,但不建议使用。现在推荐使用Object.getPrototypeOf,Object.getPrototypeOf(obj)也可以得到obj对象的原型。本文中使用__proto__只是为了便于理解。Object.getPrototypeOf(person)===person.__proto__//true上面说了,构造函数是创建特定类型的对象,所以如果我想让Person构造函数创建的对象共享一个方法,是不行的像下面这样吧:错误演示//调用构造函数Person创建一个新对象personAletpersonA=newPerson('张三')//在personA的原型中添加一个方法,供Person创建的对象共享personA.__proto__。eat=function(){console.log('Eat')}letpersonB=newPerson('李四')personB.eat()//输出:eat但是每次都需要修改一类的原型对象对象,创建一个新的对象实例,然后访问其原型对象并添加或修改属性总是感觉多余。由于构造函数创建的对象实例的原型对象都是相同的,因此构造函数与它所构造的对象实例的原型对象之间的联系是完美的。这个连接就是原型。每个函数都有一个原型属性,指向使用new运算符和函数创建的对象实例的原型对象。Person.prototype===person.__proto__//true看到这里,我们就可以理解如果想让Person创建的对象实例共享属性,应该这样写:正确示范Person.prototype.drink=function(){安慰。log('drink')}letpersonA=newPerson('张三')personB.drink()//输出:drinkOK,约定俗成,总结:对象有__proto__属性,函数有__proto__属性,数组也有一个__proto__属性,只要是引用类型,都有一个__proto__属性指向它的原型。只有函数才有原型属性,只有函数才有原型属性,只有函数才有原型属性,指向new操作符加上调用函数创建的对象实例的原型对象。参考视频讲解:进入学习5.原型链顶层。之所以将原型链称为原型链而不是原型环,说明它有始有终。那么原型链的最顶层是什么呢?再看我们的person对象,它的原型对象很简单//1.Person的原型对象person.__proto__===Person.prototype然后往上看,Person.prototype也是一个普通的对象,可以理解为一个Object结构Function创建,所以得出以下结论,//2.Person.prototypePerson.prototype.__proto__===Object.prototypeObject.prototype的原型对象也是一个对象,那么它的原型呢?这个地方很特别,记住!!!Object.prototype.__proto__===null我们可以用另一种方式来描述原型链:由对象的__proto__属性连接到Object.prototype.__proto__(为null)的链是原型链。在以上内容的基础上,我们来模拟js引擎读取对象属性:functiongetProperty(obj,propName){//查找对象本身(obj.__proto__!==null){//如果对象有原型,则递归查找原型returngetProperty(obj.__proto__,propName)}else{//直到找到Object.prototype,Object.prototype.__proto__如果为null,则返回undefinedreturnundefined}}6.构造函数回想前面的描述,构造函数有一个prototype属性,它指向使用这个构造函数创建的对象实例的原型对象。原型对象默认有一个constructor属性,指向构造函数。Person.prototype.constructor===Person//之所以一开始没有提到true,是因为这个属性对我们理解原型和原型链的帮助不大,反而容易混淆。7、函数对象的原型链前面说了,引用类型都是对象,函数也是对象,那么什么是函数对象的原型链呢?对象是由构造函数创建的,函数对象的构造函数是Function。注意这里F是大写的。letfn=function(){}//函数(包括原生构造函数)的原型对象为Function.prototypefn.__proto__===Function.prototype//trueArray.__proto__===Function.prototype//trueObject.__proto__===Function.prototype//trueFunction.prototype也是一个普通对象,所以Function.prototype.__proto__===Object.prototype这里有个特例,Function的__proto__属性指向Function.prototype。总结一下:函数是由Function的原生构造函数创建的,所以函数的__proto__属性指向了Function的prototype属性。有点乱?没关系,我们先总结一下前面的知识,再慢慢分析这张图:知识点的引用类型都是对象,每个对象都有一个原型对象。对象是由构造函数创建的,对象的__proto__属性指向它的原型对象,而构造函数的原型属性指向它创建的对象实例的原型对象,所以对象的__proto__属性等于创建它的构造函数的原型属性.所有通过字面量表示法创建的普通对象的构造函数都是Object。所有原型对象都是普通对象,构造函数是Object。所有函数的构造函数都是FunctionObject.prototype没有原型对象。OK,我们根据以上六点分析图,从左上角的f1和f2开始://f1和f2是newFoo()创建的对象,构造函数是Foo,所以f1.__proto__===Foo.prototype//Foo.prototype是普通对象,构造函数是Object,所以有Foo.prototype.__proto===Object.prototype//Object.prototype没有原型对象Object.prototype.__proto__===null然后从构造函数Foo开始://Foo是一个函数Object,构造函数是FunctionFoo.__proto__===Function.prototype//Function.prototype是一个普通的对象,而构造函数是Object,所以就有了Function.prototype。__proto__===Object.prototype然后是原生构造函数Object创建的o1,o2开始://o1,o2构造函数是Objecto1.__proto__===Object.prototype最后是原生构造函数Object和Function://native构造函数也是一个函数对象,它的构造函数是FunctionObject.__proto__===Function.prototype//特例Function.__proto__===Function.prototype分析了一下,并没有想象的那么复杂吧?如有内容引起不适,建议从头阅读,或阅读参考文章中的文章。九、举一反三1.instanceof运算符通常我们使用typeof运算符来判断一个变量的类型,但是引用类型是不适用的。除了返回函数的函数对象外,其他都是返回对象。如果我们想知道一个对象的具体类型,就需要使用instanceof。letfn=function(){}letarr=[]fninstanceofFunction//truearrinstanceofArray//truefninstanceofObject//truearrinstanceofObject//true为什么fninstanceofObject和arrinstanceofObject都返回true?我们看一下MDN上对instanceof操作符的描述:instanceof操作符用于测试构造函数的prototype属性是否出现在对象原型链的任何地方。也就是说instanceof运算符左边是对象,右边是构造。函数,在左边对象的原型链上查找,如果找到右边构造函数的prototype属性则返回true,如果找到顶层null(即Object.prototype.__proto__)则返回false).我们来模拟实现一下:functioninstanceOf(obj,Constructor){//obj代表左??边的对象,Constructor代表右边的构造函数letrightP=Constructor.prototype//取构造函数并显示原型letleftP=obj.__proto__//取对象隐式原型//returnfalseif(leftP===null){returnfalse}//如果对象实例的隐式原型等于构造函数的显示原型则返回trueif(leftP===rightP){returntrue}//在原型链上找到一层returninstanceOf(obj.__proto__,Constructor)}现在可以解释一些令人费解的结果了:fninstanceofObject//true//1.fn.__proto__===Function.prototype//2.fn.__proto__.__proto__===Function.prototype.__proto__===Object.prototypearrinstanceofObject//true//1.arr.__proto__===Array.prototype//2.arr.__proto__.__proto__===Array.prototype.__proto__===Object.prototypeObjectinstanceofObject//true//1.Object.__proto__===Function.prototype//2.Object.__proto__.__proto__===Function.prototype.__proto__===Object.prototypeFunctioninstanceofFunction//true//Function.__proto__===Function.prototype总结一下:instanceof操作符是用来检查右边构造函数的prototype属性是否出现在左边对象的原型链中的任何地方。其实代表的是一种原型链继承关系。2.Object.create之前说过,创建对象主要有两种方式,一种是new操作符后面跟着一个函数调用,另一种是字面量表示。其实还有第三种方法,ES5提供的Object.create()方法,会创建一个新的对象。第一个参数接收一个对象,该对象将作为新建对象的原型对象。第二个可选参数是属性描述。字符(不常用,默认未定义)。有关详细信息,请参阅Object.create()。我们来模拟一个简单版本的Object.create:functioncreateObj(proto){functionF(){}F.prototype=protoreturnnewF()}我们平时所说的空对象并不是严格意义上的空Object,它的原型对象指向到Object.prototype,也可以继承hasOwnProperty、toString、valueOf等方法。如果你想生成一个不继承任何属性的对象,你可以使用Object.create(null)。如果要生成普通字面量方法生成的对象,需要将其原型对象指向Object.prototype:letobj=Object.create(Object.prototype)//等价于letobj={}3.newoperator就是我们用new的时候做了什么?创建一个全新的对象并将其__proto__属性指向构造函数的原型属性。将构造函数调用的this指向这个新对象,并执行构造函数。如果构造函数返回一个对象类型Object(包括Functoin、Array、Date、RegExg、Error等),则正常返回,否则返回这个新对象。我们还是模拟实现一下:functionnewOperator(func,...args){if(typeoffunc!=='function'){console.error('第一个参数必须是函数,你传入的参数是',func)return}//创建一个新对象并将其`__proto__`属性指向构造函数的`prototype`属性letnewObj=Object.create(func.prototype)//将构造函数调用的this指向thisnewobject,并执行构造函数letresult=func.apply(newObj,args)//如果构造函数返回对象类型Object,则正常返回,否则返回这个新对象return(resultinstanceofObject)?结果:newObj}4.函数。__proto__===Function.prototype其实不用纠结鸡生蛋还是蛋生鸡的问题。我自己的理解是:Function是一个native构造函数,在运行环境中自动出现,所以不存在自生成。Function.__proto__===Function.prototype的原因是为了表明Function作为原生构造函数本身就是一个函数对象,仅此而已。5、真的是继承吗?我们之前提到过,每个对象都会从原型“继承”属性。其实继承是一个很容易混淆的说法。引用《你不知道的JavaScript》的话,就是:继承就是复制,但是JavaScript默认是不复制的,相反JavaScript只是在两个对象之间建立关联,让一个对象可以通过委托访问另一个对象的属性,所以反而称其为继承,委派更准确。