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

深入浅出地解释JavaScript的原型链和

时间:2023-03-19 20:31:58 科技观察

Javascript语言的继承机制。它没有“子类”和“父类”的概念,也没有“类”(class)和“实例”(instance)的区分。一种非常奇特的“原型链”(prototypechain)模式来实现继承。这部分知识也是JavaScript中的核心点之一,也是难点。整理了学习笔记,方便大家学习,同时加深印象。这部分代码细节很多,需要反复推敲。让我们开始吧。一个测试原型链的小例子(要点写在注释里,可以把代码复制到浏览器中测试,下同)functionfoo(){}//定义一个函数对象foo.prototype.z=3通过functionfoo(){};//函数默认有一个原型对象属性(typeoffoo.prototype;//"object")varobj=newfoo();//我们通过newfoo构造了一个新对象obj.y=2()constructor//通过赋值给objobj.x=1添加两个属性;//这样构造对象,对象的原型会指向构造函数的prototype属性,即foo.prototypeobj.x;//1//当访问obj.x时,发现obj上有x的属性,于是返回1obj.y;//2//访问obj.y时,发现有y的属性在obj上,所以返回2obj.z;//3//访问obj.z时,发现obj上没有z属性,怎么办?它不会停止寻找,它会去寻找它的原型,也就是foo.prototype,此时找到了z,所以返回3//我们用字面量创建的对象或函数的默认原型对象,其实它也是有原型的,它的原型指向Object.prototype,然后Object.prototype也有原型,它的原型指向null。//那么Object.prototype在这里的作用是什么?typeofobj.toString;//'function'//我们发现typeofobj.toString是一个函数,但是对象或者对象原型上没有toString方法,因为最后的null之前有一个Object.prototype它的原型链方法,//和toString正是Object.prototype上面的方法。这也解释了为什么JS中基本上所有的对象都有toString方法'z'inobj;//true//obj.z继承自foo.prototype,所以'z'inobj返回trueobj.hasOwnProperty('z');//false//但是obj.hasOwnProperty('z')返回false,说明z不在obj的直接对象上,而是对象原型链上的一个属性。(hsaOwnProperty也是Object.prototype上的一个方法)刚才我们访问了x,y,z,分别遍历了原型链。我们可以知道:当我们访问一个对象的某个属性,而这个对象上并没有对应的属性时,它就会向上遍历原型链查找,如果找到null,则返回undefined。基于原型的继承functionFoo(){this.y=2;}Foo.prototype.x=1;varobj3=newFoo();//①使用new调用时,函数会作为构造函数被调用②this会指向An对象(这里是obj3),这个对象的原型会指向构造函数的prototype属性(这里是Foo.prototype)obj3.y;//2obj3.x;//1//可以看到y是在object上,x是原型链上的原型(也就是Foo.prototype上)prototype属性和prototype我们来看看Foo.prototype的结构。当我们使用函数声明创建一个空函数时,那么这个函数是有一个prototype属性的,它默认有两个属性,constructor和__proto__,constructor属性会指向自己的Foo,而__proto__是暴露在chrome中的(不是标准属性,知道就好),那么Foo.prototype的原型就会指向Object.prototype。因此,Object.prototype上的一些方法toString和valueOf将被每个通用对象使用。functionFoo(){}typeofFoo.prototype;//“对象”Foo.prototype.x=1;varobj3=newFoo();总结一下:我们这里有一个Foo函数,这个函数有一个prototype对象属性,它的作用就是在用newFoo()构造实例的时候,构造函数的prototype属性会作为这些的原型新对象。所以我们要明确一点,prototype和prototype是两个不同的东西。Prototype是函数对象上的预设属性,prototype通常是构造函数上的原型属性。实现一个类继承另一个类functionPerson(name,age){this.name=name;//如果直接调用,this指向全局对象(本知识点排列)this.age=age;//如果使用new调用Peoson,this会指向原型为Person.prototype的空对象,通过this.name给空对象赋值,***this作为返回值}Person.prototype.hi=function(){//通过Person.prototype.hi共享方法创建所有Person实例,(可以参考上一节左图:对象的原型会指向构造函数的prototype属性,所以如果要obj1,obj2,obj3共享一些方法,只需要一次性在原型对象上添加属性和方法即可);console.log('Hi,mynameis'+this.name+',Iam'+this.age+'yearsoldnow.')//这里是全局对象};Person.prototype.LEGS_NUM=2;//然后设置一些数据由Person类的所有实例共享Person.prototype.ARMS_NUM=2;Person.prototype.walk=function(){console.log(this.name+'iswalking...');};functionStudent(name,age,className){//每个学生都属于Person.call(this,name,age);//在Student子类中先调用父类this.className=className;}//下一步就是继承Person的一些方法.prototypefromtheinstanceofStudentStudent.prototype=Object.create(Person.prototype);//Object.create():创建一个空对象,这个对象的原型指向它的参数//这样,我们可以在访问Student.prototype的时候查找Person.prototype,在不影响Person的情况下创建我们自己的方法Student.prototype.constructor=Student;//一致性,如果不设置,构造函数会指向PersonStudent.prototype.hi=function(){//Student.prototype.hi的赋值可以覆盖我们的基类Person.prototype.hiconsole.log('Hi,mynameis'+this.name+',Iam'+this.age+'yearsoldnow,andfrom'+this.className+'.');}Student.prototype.learn=function(subject){//同时,我们有我们的自己的learnmethodconsole.log(this.name+'islearning'+subject+'at'+this.className+'.');};//testvaryun=newStudent('Yunyun',22,'Class3,Grade2');yun.hi();//嗨,我的名字是Yunyun,我现在22岁了,还有fromClass3,Grade2.console.log(yun.ARMS_NUM);//2//我们没有自己的对象,对象的原型是Student....结合图,我们逆向分析一下上面的代码:我们先通过newStudent创建一个Student实例yun,yun的原型指向构造函数的prototype属性(这里是Student.prototype),里面有hi方法和learnStudent.prototype,Student上的方法。原型是通过Object.create(Person.prototype)构造的,所以这里的Student.prototype是一个空对象,而这个对象的原型指向Person.prototype,然后我们在Person.prototype上也设置了LEGS_NUM,ARMS_NUM属性和hi,walk方法然后我们直接定义一个Person函数,Person.prototype是一个预设的对象,它也有它的原型,它的原型是Object.prototype,正因如此,我们任何对象都会有这样的public函数如hasOwnProperty、valueOf和toString,这些函数都来自Object.prototype。这样就实现了基于原型链的继承。那么当我们调用hi、walk和learn方法时发生了什么?比如我们在调用hi方法的时候,首先检查yun这个对象上有没有hi方法,但是这个实例中没有hi方法,所以我们就去查找yun的原型上的hi方法,即Student.protoype,所以我们最终调用的是Student.prototype.hi,调用其他方法类似。改变原型我们知道,JavaScript中的原型原型不像Java中的类。Java中的类一旦写好,就很难动态改变,而JavaScript中的原型其实就是一个普通的对象,这意味着在程序运行阶段,我们也可以动态地给原型增加或删除一些属性。在上面代码的基础上,我们已经有了一个yun的实例,我们继续实验:student.prototype.x=101;//通过Student.prototype.x为yun的原型动态添加一个属性xyun.x;//101//然后我们发现所有的实例都会受到影响//然后我们做一个有趣的实验Student.prototype={y:2};//我们直接修改构造函数的prototype属性,赋值给一个新的yun.y;//undefinedyun.x;//101//所以我们得到:当我们修改Student.prototype的值时,我们不能修改实例化对象varTom=newStudent('Tom',3,'ClassLOLKengB');Tom.x;//undefined//但是当我们创建一个新的实例时,这个时候x就没有了,Tom.y;//2//而y是新的值所以当dynamic原型被修改时,所有创建的还是新的创建的实例会受到影响,但如果将整个原型分配给一个新对象,则创建的实例不会受到影响,但后续实例会受到影响。有很多方法可以实现继承。下面我们就来分析一下functionPerson(){}functionStudent(){}Student.prototype=Person.prototype;//可以用这个方法吗?这个方法是错误的:因为子类Student有自己的一些方法//,如果你这样赋值,改变了Student也改变了Person。Student.prototype=newPerson();//这个方法是可以实现的,但是有时候调用构造函数会出问题,比如传入一个Person的姓名和年龄//,其中Student是一个类,没有实例Transformation,就是这时候有点奇怪,没什么。Student.prototype=Object.create(Person.prototype);//比较理想,这个方法比较理想,这里创建一个空对象//,对象的原型指向Person.prototype,这样可以保证继承Person.prototype和Student.prototype上的方法有自己的空对象。//但是Object.create只有在ES5之后才有