这篇文章次要讲一下JS中面向对象以及 __proto__ptototypeconstructor,这几个概念都是相干的,所以一起讲了。

在讲这个之前咱们先来说说类,理解面向对象的敌人应该都晓得,如果我要定义一个通用的类型我能够应用类(class)。比方在java中咱们能够这样定义一个类:

public class Puppy{    int puppyAge;    public Puppy(age){      puppyAge = age;    }      public void say() {      System.out.println("汪汪汪");     }}

上述代码咱们定义了一个Puppy类,这个类有一个属性是puppyAge,也就是小狗的年龄,而后有一个构造函数Puppy(),这个构造函数接管一个参数,能够设置小狗的年龄,另外还有一个谈话的函数say。这是一个通用的类,当咱们须要一个两岁的小狗实例是间接这样写,这个实例同时具备父类的办法:

Puppy myPuppy = new Puppy( 2 );myPuppy.say();     // 汪汪汪

然而晚期的JS没有class关键字啊(以下说JS没有class关键字都是指ES6之前的JS,次要帮忙大家了解概念),JS为了反对面向对象,应用了一种比拟波折的形式,这也是导致大家蛊惑的中央,其实咱们将这种形式跟个别的面向对象类比起来就很清晰了。上面咱们来看看JS为了反对面向对象须要解决哪些问题,都用了什么波折的形式来解决。

没有class,用函数代替

首先JS连class关键字都没有,怎么办呢?用函数代替,JS中最不缺的就是函数,函数不仅可能执行一般性能,还能当class应用。比方咱们要用JS建一个小狗的类怎么写呢?间接写一个函数就行:

function Puppy() {}

这个函数能够间接用new关键字生成实例:

const myPuppy = new Puppy();

这样咱们也有了一个小狗实例,然而咱们没有构造函数,不能设置小狗年龄啊。

函数自身就是构造函数

当做类用的函数自身也是一个函数,而且他就是默认的构造函数。咱们想让Puppy函数可能设置实例的年龄,只有让他接管参数就行了。

function Puppy(age) {  this.puppyAge = age;}// 实例化时能够传年龄参数了const myPuppy = new Puppy(2);

留神下面代码的this,被作为类应用的函数外面this总是指向实例化对象,也就是myPuppy。这么设计的目标就是让使用者能够通过构造函数给实例对象设置属性,这时候console进去看myPuppy.puppyAge就是2。

console.log(myPuppy.puppyAge);   // 输入是 2

实例办法用prototype

下面咱们实现了类和构造函数,然而类办法呢?Java版小狗还能够“汪汪汪”叫呢,JS版怎么办呢?JS给出的解决方案是给办法增加一个prototype属性,挂载在这下面的办法,在实例化的时候会给到实例对象。咱们想要myPuppy能谈话,就须要往Puppy.prototype增加谈话的办法。

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

咱们说的constructor个别指类的prototype.constructorprototype.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 );   // trueconsole.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;      // 留神重置constructorconst obj = new Child();console.log(obj.parentAge);    // 50

当然还有很多其余的继承形式,他们的原理都差不多,只是实现形式不一样,外围都是让子类领有父类的办法和属性,感兴趣的敌人能够自行查阅。

本人实现一个new

联合下面讲的,咱们晓得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);  // 2console.log(myPuppy3.say());     // 汪汪汪

本人实现一个instanceof

晓得了原理,其实咱们也晓得了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) );   // trueconsole.log(myInstanceof(obj, Parent) );   // trueconsole.log(myInstanceof({}, Parent) );   // false

ES6的class

最初还是提一嘴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);    // 2console.log(myPuppy.say());       // 汪汪汪console.log(Puppy.statciFunc());  // 我是静态方法,this拿不到实例对象

应用class能够让咱们的代码看起来更像规范的面向对象,构造函数,实例办法,静态方法都有明确的标识。然而他实质只是扭转了一种写法,所以能够看做是一种语法糖,如果你去看babel编译后的代码,你会发现他其实也是把class编译成了咱们后面的函数类,extends关键字也是应用咱们后面的原型继承的形式实现的。

总结

最初来个总结,其实后面大节的题目就是外围了,咱们再来总结下:

  1. JS中的函数能够作为函数应用,也能够作为类应用
  2. 作为类应用的函数实例化时须要应用new
  3. 为了让函数具备类的性能,函数都具备prototype属性。
  4. 为了让实例化进去的对象可能拜访到prototype上的属性和办法,实例对象的__proto__指向了类的prototype。所以prototype是函数的属性,不是对象的。对象领有的是__proto__,是用来查找prototype的。
  5. prototype.constructor指向的是构造函数,也就是类函数自身。扭转这个指针并不能扭转构造函数。
  6. 对象自身并没有constructor属性,你拜访到的是原型链上的prototype.constructor
  7. 函数自身也是对象,也具备__proto__,他指向的是JS内置对象Function的原型Function.prototype。所以你能力调用func.call,func.apply这些办法,你调用的其实是Function.prototype.callFunction.prototype.apply
  8. prototype自身也是对象,所以他也有__proto__,指向了他父级的prototype__proto__prototype的这种链式指向形成了JS的原型链。原型链的最终指向是Object的原型。Object下面原型链是null,即Object.prototype.__proto__ === null
  9. 另外要留神的是Function.__proto__ === Function.prototype,这是因为JS中所有函数的原型都是Function.prototype,也就是说所有函数都是Function的实例。Function自身也是能够作为函数应用的----Function(),所以他也是Function的一个实例。相似的还有ObjectArray等,他们也能够作为函数应用:Object(), Array()。所以他们自身的原型也是Function.prototype,即Object.__proto__ === Function.prototype。换句话说,这些能够new的内置对象其实都是一个类,就像咱们的Puppy类一样。
  10. ES6的class其实是函数类的一种语法糖,书写起来更清晰,但原理是一样的。

再来看一下残缺图:

文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。

作者博文GitHub我的项目地址: https://github.com/dennis-jiang/Front-End-Knowledges