作者:蒋鹏飞
链接: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 );   // 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 = {};     // 新建一个空对象  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);  // 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

总结

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

  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

再来看一下完整图:

点赞、在看、转发支持作者❤️

js先执行一个方法再往下执行_轻松理解JS中的面向对象,顺便搞懂prototype和__proto__...相关推荐

  1. js 添加属性_轻松理解JS中的面向对象,顺便搞懂prototype和__proto__

    这篇文章主要讲一下JS中面向对象以及 __proto__,ptototype和construcator,这几个概念都是相关的,所以一起讲了. 在讲这个之前我们先来说说类,了解面向对象的朋友应该都知道, ...

  2. android子线程没有运行完,android假如主线程依赖子线程A的执行结果,如何让A执行完成,之后主线程再往下执行呢?...

    /* String ObjectResult="原先的结果"; //使用VOLLY框架(与问题无关) JsonObjectRequest jsonObjectRequest = n ...

  3. android广播怎样运行在子线程,android假如主线程依赖子线程A的执行结果,如何让A执行完成,之后主线程再往下执行呢?...

    抛开你这段代码不看,单根据你的标题来回答: android假如主线程依赖子线程A的执行结果,如何让A执行完成,之后主线程再往下执行呢? 需要在子线程执行完成的地方,通过主线程的Handler发送一条消 ...

  4. Java黑皮书课后题第8章:*8.30(代数:解答线性方程)编写一个方法,解答下面的2*2线性方程组系统

    *8.30(代数:解答线性方程)编写一个方法,解答下面的2*2线性方程组系统 题目 题目描述 代码 题目 题目描述 8.30(代数:解答线性方程)编写一个方法,解答下面的22线性方程组系统: a00 ...

  5. 程序员面试金典——解题总结: 9.17中等难题 17.9设计一个方法,找出任意指定单词在一本书中的出现频率

    #include <iostream> #include <stdio.h> #include <vector> #include <string.h> ...

  6. Java黑皮书课后题第8章:*8.2(求矩阵对角线元素的和)使用下面的方法头编写一个方法,求n*n的double类型矩阵中主对角线上所有数字的和。编写一个程序,读取一个4*4的矩阵,显示主对角线和

    *8.2(求矩阵对角线元素的和)使用下面的方法头编写一个方法,求n*n的double类型矩阵中主对角线上所有数字的和.编写一个程序,读取一个4*4的矩阵,显示主对角线和 题目 题目描述与运行示例 破题 ...

  7. Java黑皮书课后题第6章:*6.20(计算一个字符串中字母的个数)编写一个方法,使用下面的方法体计算字符串中的字母个数。编写一个测试程序,提示用户输入字符串,然后显示字符串中的字母个数

    6.20(计算一个字符串中字母的个数)编写一个方法,使用下面的方法体计算字符串中的字母个数.编写一个测试程序,提示用户输入字符串,然后显示字符串中的字母个数 题目 题目描述 破题 代码 运行示例 题目 ...

  8. java饲养员喂动物_做一个饲养员给动物喂食物的例子体现JAVA中的面向对象思想,接口(抽象类)的用处...

    做一个饲养员给动物喂食物的例子体现JAVA中的面向对象思想,接口(抽象类)的用处 interface Animal{ //动物的接口    public void eat(Food food); // ...

  9. java调用一个方法后怎么继续执行不等待该方法的返回_Java面试题大全2020版(二)...

    今天给大家推送第二部分,主要的大块内容分为:多线程.反射.对象拷贝.三大块内容中涉及到的考点如下: 三.多线程 35. 并行和并发有什么区别? 并行是指两个或者多个事件在同一时刻发生:而并发是指两个或 ...

  10. java并发执行一个方法_JAVA的执行并发原理

    Volatile Volatile关键字用于确保共享数据的可见性与有序性,但是并不能保证方法的原子性,在程序中对Volatile关键字使用得当的话,它比synchronized的使用和执行成本会更低, ...

最新文章

  1. 【ZooKeeper】集群安装与配置
  2. hdu 5419(数学期望)
  3. CSS样式特点及优先级
  4. 10G_Ethernet_01 万兆以太网设计引言
  5. CSS基础必备知识点05
  6. 2009年5月软件设计师考试试题解析全国首发
  7. 举例HTML的图像标记,教案html之css滤镜及练习层div块及span标记举例窗口内例题演示功能的实现总结.pdf...
  8. 逆向记录Assaultcube for Mac OS
  9. server 08 做DNS和域分离
  10. 破解word只读文档 word文档保护后的破解办法
  11. html json是什么文件,JSON是什么?
  12. Android ContentProvider初探
  13. ORA-01081: 无法启动已在运行的 ORACLE - 请首先关闭它
  14. 计算机人文素养教案,小学信息技术课堂中人文素养的教学探究
  15. 概率论与数理统计 | 知识点总结(下)
  16. javascript尾递归优化
  17. 三菱PLC读取旋转编码器信号
  18. nrf51822基础知识整理
  19. 【275】◀▶ Python 控制语句说明
  20. 在eclipse中用bluestack运行android程序

热门文章

  1. 坑爹!Quartz 重复调度问题,你遇到过么?
  2. 电商金额计算的 4 个坑,千万注意了!
  3. 面试再问HashMap,求你把这篇文章发给他!
  4. 千万级用户-亿级请求的平台架构演变
  5. 阿里人告诉你毕业3年,为何技术能力相差越来越大?
  6. mysql update 几万 非常慢_MySQL慢日志这样分析才够味
  7. python第二十二天,configparser模块 subprocess 模块,xlrd 模块(表格处理)
  8. 仓库建设-斜率优化DP
  9. XE5 搭建DataSnap服务
  10. 个股打板机会:国民技术(实战)