这篇文章主要讲一下JS中面向对象以及 __proto__
,ptototype
和constructor
,这几个概念都是相关的,因此一块儿讲了。javascript
在讲这个以前咱们先来讲说类,了解面向对象的朋友应该都知道,若是我要定义一个通用的类型我可使用类(class)。好比在java中咱们能够这样定义一个类:java
public class Puppy{ int puppyAge; public Puppy(age){ puppyAge = age; } public void say() { System.out.println("汪汪汪"); } }
上述代码咱们定义了一个Puppy类,这个类有一个属性是puppyAge,也就是小狗的年龄,而后有一个构造函数Puppy(),这个构造函数接收一个参数,能够设置小狗的年龄,另外还有一个说话的函数say。这是一个通用的类,当咱们须要一个两岁的小狗实例是直接这样写,这个实例同时具备父类的方法:git
Puppy myPuppy = new Puppy( 2 ); myPuppy.say(); // 汪汪汪
可是早期的JS没有class关键字啊(如下说JS没有class关键字都是指ES6以前的JS,主要帮助你们理解概念),JS为了支持面向对象,使用了一种比较曲折的方式,这也是致使你们迷惑的地方,其实咱们将这种方式跟通常的面向对象类比起来就很清晰了。下面咱们来看看JS为了支持面向对象须要解决哪些问题,都用了什么曲折的方式来解决。github
首先JS连class关键字都没有,怎么办呢?用函数代替,JS中最不缺的就是函数,函数不只可以执行普通功能,还能当class使用。好比咱们要用JS建一个小狗的类怎么写呢?直接写一个函数就行:ajax
function Puppy() {}
这个函数能够直接用new关键字生成实例:babel
const myPuppy = new Puppy();
这样咱们也有了一个小狗实例,可是咱们没有构造函数,不能设置小狗年龄啊。app
当作类用的函数自己也是一个函数,并且他就是默认的构造函数。咱们想让Puppy函数可以设置实例的年龄,只要让他接收参数就好了。函数
function Puppy(age) { this.puppyAge = age; } // 实例化时能够传年龄参数了 const myPuppy = new Puppy(2);
注意上面代码的this
,被做为类使用的函数里面this老是指向实例化对象,也就是myPuppy。这么设计的目的就是让使用者能够经过构造函数给实例对象设置属性,这时候console出来看myPuppy.puppyAge
就是2。this
console.log(myPuppy.puppyAge); // 输出是 2
上面咱们实现了类和构造函数,可是类方法呢?Java版小狗还能够“汪汪汪”叫呢,JS版怎么办呢?JS给出的解决方案是给方法添加一个prototype
属性,挂载在这上面的方法,在实例化的时候会给到实例对象。咱们想要myPuppy能说话,就须要往Puppy.prototype
添加说话的方法。spa
Puppy.prototype.say = function() { console.log("汪汪汪"); }
使用new关键字产生的实例都有类的prototype
上的属性和方法,咱们在Puppy.prototype
上添加了say方法,myPuppy就能够说话了,我么来试一下:
myPuppy.say(); // 汪汪汪
__proto__
那myPuppy怎么就可以调用say
方法了呢,咱们把他打印出来看下,这个对象上并无say啊,这是从哪里来的呢?
这就该__proto__
上场了,当你访问一个对象上没有的属性时,好比myPuppy.say
,对象会去__proto__
查找。__proto__
的值就等于父类的prototype, myPuppy.__proto__
指向了Puppy.prototype
。
若是你访问的属性在Puppy.prototype
也不存在,那又会继续往Puppy.prototype.__proto__
上找,这时候其实就找到了Object.prototype
了,Object.prototype
再往上找就没有了,也就是null,这其实就是原型链。
咱们说的constructor通常指类的prototype.constructor
。prototype.constructor
是prototype上的一个保留属性,这个属性就指向类函数自己,用于指示当前类的构造函数。
既然prototype.constructor
是指向构造函数的一个指针,那咱们是否是能够经过它来修改构造函数呢?咱们来试试就知道了。咱们先修改下这个函数,而后新建一个实例看看效果:
function Puppy(age) { this.puppyAge = age; } Puppy.prototype.constructor = function myConstructor(age) { this.puppyAge = age + 1; } const myPuppy2 = new Puppy(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就是一个静态方法 console.log('我是静态方法,this拿不到实例对象'); } Puppy.statciFunc(); // 直接经过类名调用
静态方法和实例方法最主要的区别就是实例方法能够访问到实例,能够对实例进行操做,而静态方法通常用于跟实例无关的操做。这两种方法在jQuery中有大量应用,在jQuery中$(selector)
其实拿到的就是实例对象,经过$(selector)
进行操做的方法就是实例方法。好比$(selector).append()
,这会往这个实例DOM添加新元素,他须要这个DOM实例才知道怎么操做,将append
做为一个实例方法,他里面的this就会指向这个实例,就能够经过this操做DOM实例。那什么方法适合做为静态方法呢?好比$.ajax
,这里的ajax
跟DOM实例不要紧,不须要这个this,能够直接挂载在$上做为静态方法。
面向对象怎么能没有继承呢,根据前面所讲的知识,咱们其实已经可以本身写一个继承了。所谓继承不就是子类可以继承父类的属性和方法吗?换句话说就是子类可以找到父类的prototype
,最简单的方法就是子类原型的__proto__
指向父类原型就好了。
function Parent() {} function Child() {} Child.prototype.__proto__ = Parent.prototype; const obj = new Child(); console.log(obj instanceof Child ); // true console.log(obj instanceof Parent ); // true
上述继承方法只是让Child访问到了Parent原型链,可是没有执行Parent的构造函数:
function Parent() { this.parentAge = 50; } function Child() {} Child.prototype.__proto__ = Parent.prototype; const obj = new Child(); console.log(obj.parentAge); // undefined
为了解决这个问题,咱们不能单纯的修改Child.prototype.__proto__
指向,还须要用new执行下Parent的构造函数:
function Parent() { this.parentAge = 50; } function Child() {} Child.prototype.__proto__ = new Parent(); const obj = new Child(); console.log(obj.parentAge); // 50
上述方法会多一个__proto__
层级,能够换成修改Child.prototype
的指向来解决,注意将Child.prototype.constructor
重置回来:
function Parent() { this.parentAge = 50; } function Child() {} Child.prototype = new Parent(); Child.prototype.constructor = Child; // 注意重置constructor const obj = new Child(); console.log(obj.parentAge); // 50
固然还有不少其余的继承方式,他们的原理都差很少,只是实现方式不同,核心都是让子类拥有父类的方法和属性,感兴趣的朋友能够自行查阅。
结合上面讲的,咱们知道new其实就是生成了一个对象,这个对象可以访问类的原型,知道了原理,咱们就能够本身实现一个new了。
function myNew(func, ...args) { const obj = {}; // 新建一个空对象 const result = func.call(obj, ...args); // 执行构造函数 obj.__proto__ = func.prototype; // 设置原型链 // 注意若是原构造函数有Object类型的返回值,包括Functoin, Array, Date, RegExg, Error // 那么应该返回这个返回值 const isObject = typeof result === 'object' && result !== null; const isFunction = typeof result === 'function'; if(isObject || isFunction) { return result; } // 原构造函数没有Object类型的返回值,返回咱们的新对象 return obj; } function Puppy(age) { this.puppyAge = age; } Puppy.prototype.say = function() { console.log("汪汪汪"); } const myPuppy3 = myNew(Puppy, 2); console.log(myPuppy3.puppyAge); // 2 console.log(myPuppy3.say()); // 汪汪汪
知道了原理,其实咱们也知道了instanceof是干啥的。instanceof不就是检查一个对象是否是某个类的实例吗?换句话说就是检查一个对象的的原型链上有没有这个类的prototype
,知道了这个咱们就能够本身实现一个了:
function myInstanceof(targetObj, targetClass) { // 参数检查 if(!targetObj || !targetClass || !targetObj.__proto__ || !targetClass.prototype){ return false; } let current = targetObj; while(current) { // 一直往原型链上面找 if(current.__proto__ === targetClass.prototype) { return true; // 找到了返回true } current = current.__proto__; } return false; // 没找到返回false } // 用咱们前面的继承实验下 function Parent() {} function Child() {} Child.prototype.__proto__ = Parent.prototype; const obj = new Child(); console.log(myInstanceof(obj, Child) ); // true console.log(myInstanceof(obj, Parent) ); // true console.log(myInstanceof({}, Parent) ); // false
最后仍是提一嘴ES6的class,其实ES6的class就是前面说的函数类的语法糖,好比咱们的Puppy用ES6的class写就是这样:
class Puppy { // 构造函数 constructor(age) { this.puppyAge = age; } // 实例方法 say() { console.log("汪汪汪") } // 静态方法 static statciFunc() { console.log('我是静态方法,this拿不到实例对象'); } } const myPuppy = new Puppy(2); console.log(myPuppy.puppyAge); // 2 console.log(myPuppy.say()); // 汪汪汪 console.log(Puppy.statciFunc()); // 我是静态方法,this拿不到实例对象
使用class可让咱们的代码看起来更像标准的面向对象,构造函数,实例方法,静态方法都有明确的标识。可是他本质只是改变了一种写法,因此能够看作是一种语法糖,若是你去看babel编译后的代码,你会发现他其实也是把class编译成了咱们前面的函数类,extends关键字也是使用咱们前面的原型继承的方式实现的。
最后来个总结,其实前面小节的标题就是核心了,咱们再来总结下:
prototype
属性。prototype
上的属性和方法,实例对象的__proto__
指向了类的prototype
。因此prototype
是函数的属性,不是对象的。对象拥有的是__proto__
,是用来查找prototype
的。prototype.constructor
指向的是构造函数,也就是类函数自己。改变这个指针并不能改变构造函数。constructor
属性,你访问到的是原型链上的prototype.constructor
。__proto__
,他指向的是JS内置对象Function
的原型Function.prototype
。因此你才能调用func.call
,func.apply
这些方法,你调用的实际上是Function.prototype.call
和Function.prototype.apply
。prototype
自己也是对象,因此他也有__proto__
,指向了他父级的prototype
。__proto__
和prototype
的这种链式指向构成了JS的原型链。原型链的最终指向是Object
的原型。Object
上面原型链是null,即Object.prototype.__proto__ === null
。Function.__proto__ === Function.prototype
,这是由于JS中全部函数的原型都是Function.prototype
,也就是说全部函数都是Function
的实例。Function
自己也是能够做为函数使用的----Function()
,因此他也是Function
的一个实例。相似的还有Object
,Array
等,他们也能够做为函数使用:Object()
, Array()
。因此他们自己的原型也是Function.prototype
,即Object.__proto__ === Function.prototype
。换句话说,这些能够new的内置对象其实都是一个类,就像咱们的Puppy类同样。再来看一下完整图:
文章的最后,感谢你花费宝贵的时间阅读本文,若是本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是做者持续创做的动力。
做者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges