本文主要讲的是面向对象和JS中的__proto__、ptototype和constructor。这些概念是相关的,所以我们将它们放在一起说。在我们谈论这个之前,让我们先谈谈类。了解面向对象的朋友应该知道,如果我要定义一个通用的类型,可以使用类。比如在java中,我们可以这样定义一个类:publicclassPuppy{intpuppyAge;publicPuppy(age){puppyAge=age;}publicvoidsay(){System.out.println("Wow,woof");}}以上代码定义了一个PuppyClass,这个类有一个属性puppyAge,就是小狗的年龄,然后有一个构造函数Puppy(),接收一个可以设置小狗年龄的参数,还有一个会说话的函数say.这是一个通用类。当我们需要一个两岁的小狗实例时,我们直接写。这个实例还有父类的方法:PuppymyPuppy=newPuppy(2);myPuppy.say();//哇塞,但是早期的JS没有class关键字(下面JS没有class关键字的说法参考toJSbeforeES6,主要是帮助大家理解概念,本文不涉及ES6类),JS使用了比较曲折的方式来支持面向对象这也是大家比较迷惑的地方。其实我们把这个方法和一般的面向对象类对比一下就很清楚了。下面我们就来看看JS为了支持面向对象需要解决哪些问题,又用了哪些曲折的方式来解决。如果没有类,请改用函数。首先,JS连class关键字都没有。我应该怎么办?改用函数。JS中最不可或缺的就是函数。函数不仅可以执行普通的功能,还可以作为类来使用。比如我们怎么用JS写一个puppy类?直接写一个函数就可以了:functionPuppy(){}这个函数可以直接使用new关键字生成一个实例:constmyPuppy=newPuppy();这样,我们也有了一个小狗实例,但是我们没有构造函数,所以我们不能设置小狗的年龄。函数本身是一个构造函数。作为类使用的函数本身也是一个函数,他是默认的构造函数。我们希望Puppy函数能够设置实例的年龄,只需要让它接受参数即可。functionPuppy(age){this.puppyAge=age;}//age参数可以在实例化的时候传入constmyPuppy=newPuppy(2);注意上面代码中的this,作为类使用的函数中的this始终指向实例化的对象,即myPuppy。这样设计的目的是让用户可以通过构造函数为实例对象设置属性。这时候控制台出来可以看到myPuppy.puppyAge是2.console.log(myPuppy.puppyAge);//输出的是2个实例方法usingprototype上面我们实现了类和构造函数,但是类方法呢?Java版的小狗还能叫“哇,汪”,那JS版呢?JS给出的解决方案是在方法上加一个prototype属性,挂载在上面的方法在实例化的时候会被赋予一个实例对象。如果我们想让myPuppy能够说话,我们需要在Puppy.prototype中添加一个speaking方法。Puppy.prototype.say=function(){console.log("Wow,woof");}使用new关键字生成的实例在类的原型上具有属性和方法。我们在Puppy.prototype中添加了say方法,myPuppy现在可以说话了,我们试试看:myPuppy.say();//Wow旺旺实例方法搜索使用了__proto__,那么myPuppy怎么调用say方法呢,我们打印出来看看,这个object在网站上没有说,这是从哪里来的?这就是__proto__发挥作用的地方。当你访问一个不在对象上的属性时,比如myPuppy.say,对象会去__proto__寻找它。__proto__的值等于父类的原型,myPuppy.__proto__指向Puppy.prototype。如果你正在访问的属性在Puppy.prototype中不存在,那么你将继续在Puppy.prototype.__proto__上寻找它。这时候,你就会真正找到Object了。如果再去查找Object,它就没有了,也就是null。其实就是原型链。我们所说的构造函数一般指的是类的prototype.constructor。prototype.constructor是原型上的保留属性,指向类函数本身,用于指示当前类的构造函数。既然prototype.constructor是一个指向构造函数的指针,那么我们是否可以通过它来修改构造函数呢?让我们试试看。我们先修改一下这个函数,然后新建一个实例看看效果:=newPuppy(2);console.log(myPuppy2.puppyAge);//输出为2,上面的例子说明我们在修改prototype.constructor时只修改了指针,并没有修改真正的构造函数。可能有朋友会说打印myPuppy2.constructor也有值,那么constructor是不是也是对象本身的一个属性呢?事实上,它不是。之所以能打印这个值是因为打印的时候发现myPuppy2本身没有这个属性,于是在原型链上搜索,找到了prototype.constructor。我们可以通过hasOwnProperty来看一下:其实上面我们已经解释了prototype、__proto__、constructor的关系了。我们画一张图更直观的看一下:静态方法我们知道很多面向对象的方法都有静态方法。方法的概念,比如Java,可以通过直接加上static关键字,将一个方法定义为静态方法。在JS中定义静态方法比较简单,直接作为类函数的属性即可:Puppy.statciFunc=function(){//statciFunc是静态方法conlose.log('我是静态方法,这个不能gettheinstanceobject');}Puppy.statciFunc();//通过类名直接调用静态方法和实例方法的主要区别在于实例方法可以访问实例并对实例进行操作,而静态方法一般用于与实例无关的操作。这两种方法在jQuery中被广泛使用。在jQuery中$(selector)实际获取的是实例对象,通过$(selector)操作的方法就是实例方法。例如,$(selector).append()将向该实例的DOM添加新元素。他需要这个DOM实例来知道如何操作。使用append作为实例方法,里面的this会指向这个实例,你可以通过this操作DOM实例。那么什么样的方法适合作为静态方法呢?比如$.ajax,其中ajax与DOM实例无关,不需要这个,可以直接挂载在$上作为静态方法。继承面向对象怎么能没有继承呢?根据上面提到的知识,我们其实可以自己写一个继承。所谓继承不就是子类可以继承父类的属性和方法吗?也就是说,子类可以找到父类的原型。最简单的方法是子类原型的__proto__指向父类原型。functionParent(){}functionChild(){}Child.prototype.__proto__=Parent.prototype;constobj=newChild();console.log(objinstanceofChild);//trueconsole.log(objinstanceofParent);//true上面的继承方式是只是让Child访问Parent原型链,但不执行Parent构造函数:log(obj.parentAge);//undefined为了解决这个问题,我们不能简单的修改Child.prototype.__proto__点,还需要用new执行Parent构造函数:functionParent(){this.parentAge=50;}functionChild(){}Child.prototype.__proto__=newParent();constobj=newChild();console.log(obj.parentAge);//50上面的方法会增加一个__proto__层,可以通过修改Child来替换.prototype解决,注意把Child.prototype.constructor重置回来:functionParent(){this.parentAge=50;}functionChild(){}Child.prototype=newParent();ChildChild.prototype.constructor=Child;//注意resetconstructorconstobj=newChild();console.log(obj.parentAge);//50当然还有很多其他的继承方式。它们的原理相似,但实现方法不同。核心是让子类拥有父类的方法和属性,感兴趣的朋友可以自行查看。自己实现一个new结合上面我们知道new其实是生成了一个对象,这个对象可以访问类的原型。知道了原理,我们就可以自己实现一个新的了。functionmyNew(func,...args){constobj={};//创建一个新的空对象func.call(obj,...args);//执行构造函数obj.__proto__=func.prototype;//设置原型链returnobj;}functionPuppy(age){this.puppyAge=age;}Puppy.prototype.say=function(){console.log("Wow,woof");}constmyPuppy3=myNew(Puppy,2);console.log(myPuppy3.puppyAge);//2console.log(myPuppy3.say());//汪汪汪自己实现一个instanceof,知道原理。其实我们也知道instanceof是干什么的。instanceof不就是检查一个对象是否是某个类的实例吗?也就是说,就是检查一个对象的原型链上是否存在这个类的原型。知道了这一点,我们可以自己实现一个:functionmyInstanceof(targetObj,targetClass){//参数检查if(!targetObj||!targetClass||!targetObj.__proto__||!targetClass.prototype){returnfalse;}letcurrent=targetObj;while(current){//一直去原型链中寻找if(current.__proto__===targetClass.prototype){returntrue;//找到则返回true}currentcurrent=current.__proto__;}returnfalse;//返回falseifnotfound}//用我们之前的继承实验functionParent(){}functionChild(){}Child.prototype.__proto__=Parent.prototype;constobj=newChild();console.log(myInstanceof(obj,Child));//trueconsole.log(myInstanceof(obj,Parent));//trueconsole.log(myInstanceof({},Parent));//false总结最后,这里是一个总结。其实上一节的标题才是核心。再总结一下:JS中的函数可以作为函数,也可以作为类的函数,函数具有原型属性。为了让实例化对象能够访问原型上的属性和方法,实例对象的__proto__指向类的原型。所以原型是函数的属性,而不是对象。该对象具有__proto__,用于查找原型。5.prototype.constructor指向构造函数,也就是类函数本身。更改此指针不会更改构造函数。6、对象本身没有constructor属性,你访问的是原型链上的prototype.constructor。7、函数本身也是一个对象,也有__proto__,指向JS内置对象Function的原型Function.prototype。所以你可以调用func.call,func.apply这些方法,你实际上是在调用Function.prototype.call和Function.prototype.apply。8.原型本身也是一个对象,所以他也有__proto__,指向他父母的原型。__proto__和prototype这个链指向JS的原型链。原型链的最后一点是Object。Object上面的原型链为null,即Object.__proto__===null。我们来看看全貌:
