轻松理解 JS 中的面向对象,顺便搞懂 prototype 和 __proto__

Posted 前端开发博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了轻松理解 JS 中的面向对象,顺便搞懂 prototype 和 __proto__相关的知识,希望对你有一定的参考价值。

点击上方“前端开发博客”,选择“设为星标”

回复“2”加入前端群

作者:蒋鹏飞

https://juejin.im/post/5e50e5b16fb9a07c9a1959af

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

在讲这个之前我们先来说说类,了解面向对象的朋友应该都知道,如果我要定义一个通用的类型我可以使用类(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,主要帮助大家理解概念,本文不涉及ES6的class),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了,Object再往上找就没有了,也就是null,这其实就是原型链


constructor

我们说的constructor一般指类的prototype.constructorprototype.constructor是prototype上的一个保留属性,这个属性就指向类函数本身,用于指示当前类的构造函数。

既然prototype.constructor是指向构造函数的一个指针,那我们是不是可以通过它来修改构造函数呢?我们来试试就知道了。我们先修改下这个函数,然后新建一个实例看看效果:

function Puppy(age) {
  this.puppyAge = age;
}


Puppy.prototype.constructor = function myConstructor(age) {
  this.puppyAge2 = 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就是一个静态方法
  conlose.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其实就是生成了一个对象,这个对象能够访问类的原型,知道了原理,我们就可以自己实现一个new了。

function myNew(func, ...args) {
  const obj = {};     // 新建一个空对象
  func.call(obj, ...args);  // 执行构造函数
  obj.__proto__ = func.prototype;    // 设置原型链


  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是干啥的。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


总结

最后来个总结,其实前面小节的标题就是核心了,我们再来总结下:

  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的原型链。原型链的最终指向是ObjectObject上面原型链是null,即Object.__proto__ === null

再来看一下完整图:

推荐阅读

看完这篇文章,彻底了解 “原型” & “this”

忙碌的一周过去了,一起来汇总一下前端开发博客最近一周有哪些值得阅读的东西吧。

文章排行

  1. 不到30行 JS 实现一个炫酷的全景交互

  2. 尤大为何放弃Webpack?来探索一下 Vite

  3. 面试官:谈谈A和B两者区别,这里有24个解答

  4. 你所不了解的TypeScript 类型编程

  5. 为什么程序员怕改需求?看完这些神解释我笑了

  6. 前端如何在项目中做出亮点

  7. 基于Vite2+Vue3的项目复盘总结

END

关注下方「前端开发博客」,回复 “简历模板”

领取33个精选前端简历模板

❤️ 看完两件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我两个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 关注公众号「前端开发博客」,每周重点攻克一个前端面试重难点

如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~

公众号也开始通过互动率推送了,互动少了可能就很晚或者收不到文章了。

大家点个在看,星标我的公众号,就可以及时获得推文。

点个在看少个Bug

以上是关于轻松理解 JS 中的面向对象,顺便搞懂 prototype 和 __proto__的主要内容,如果未能解决你的问题,请参考以下文章

理解js中的原型链,prototype与__proto__的关系

理解面向对象

JS高级——对象的原型__proto__函数的原型prototype构造函数

怎么理解js的原型链继承?

怎么理解js的原型链继承?

JS原型和面向对象