前端【JS】,深入理解原型和原型链

对于原型和原型链,相信有很多伙伴都说的上来一些,但有具体讲不清楚。但面试的时候又经常会碰到面试官的死亡的追问,我们慢慢来梳理这方面的知识!

要理解原型和原型链的关系,我们首先需要了解几个概念;
1、什么是构造函数?
2、构造函数与普通函数有什么区别?

3、原型链的顶端是什么?

4、prototype、__proto__、constructor在什么对象下存在?

OK  我们暂时带着这些疑问往下看;


一、什么是构造函数?构造函数与普通函数有什么区别?

构造函数其实就是一个普通函数,只是我们为了区分普通函数,通常建议构造函数name首字母大写;

// 这是一个构造函数
function Parent(){};

你说我就不首字母大写,那也不影响一个函数是构造函数的事实:

// 这也是一个构造函数
function parent(){
    this.name = '不秃头';
};
let child = new parent();
console.log(child);//parent {name: "不秃头"}

有同学就纳闷了,这普通函数居然也能使用new操作符构造调用,没错,不仅普通函数能new调用,构造函数同样也能普通调用:

// 这是一个构造函数
function Parent() {
    console.log(1);
};
Parent() //1

其实到这里,我们已经解释了 构造函数与普通函数有什么区别 这个问题,构造函数其实就是一个普通函数,且函数都支持new调用与普通调用。也正因如此导致了ES5中构造函数没有区别于普通函数的尴尬局面,这也是为何在ES6中JavaScript正式推出Class类的原因,你会发现Class只支持new调用,如果直接调用会报错:

class Parent {
    sayName() {
        console.log('不秃头');
    };
};
var child = new Parent();
child.sayName(); //不秃头
var child = Parent();//报错,必须使用new调用

解释了构造函数,那么构造函数能用来做什么呢?最基本的就是属性继承了,我们先不聊继承模式,就从最基本的继承说起。

假设现在我们要定制一批蓝色的杯子,杯口直径与高度可互不相同,那么我们可以用构造函数表示:

//定制杯子
function CupCustom(diameter, height) {
    this.diameter = diameter;
    this.height = height;
};
CupCustom.prototype.color = 'blue';
var cup1 = new CupCustom(8, 15);
var cup2 = new CupCustom(5, 10);
console.log(cup1.height);//15
console.log(cup2.color);//blue

那么我们可以将构造函数CupCustom理解成一个制作杯子的模具,cup1与cup2是模具制作出来的杯子,我们称之为实例。大家可以尝试输出实例,可以看到两个实例都继承了构造函数的构造器属性(直径,高)与原型属性(颜色),颜色存放的地方还有点不同,它放在__proto__中,说到这咱们解释了为什么实例能读取height与color两个属性。

出于好奇,咱们也输出打印了构造函数的属性,有同学不知道怎么打印查看函数的属性,这里可以借用console.dir(函数),打印结果如下图:

对比图1与图2可以发现,构造函数除了自身属性与__proto__属性外还多出了一个prototype属性,这里我们其实能先给出一个结论:

所有的对象都有__proto__属性,但只有函数拥有prototype属性;

 


 

二、prototype与__proto__

"万物皆对象",这句话我想不止前端的同学,应该搞开发的同学都听过吧。

我们知道JavaScript中数据类型分类基本数据类型与引用数据类型:

  • 基本数据类型:Number,String,Boolean,Undefined,Null,Symbol。
  • 引用数据类型:Object,Function,Date,Array,RegExp等。

不知道大家有没有想过这样一件事,为什么随便声明一段字符串就能使用字符串的方法?如果字符串真的就是简单类型,方法又是从哪来的呢?

经实验,在这些类型中,基本类型中除了undefined与null之外,任意数字,字符,布尔以及symbol值都有__proto__属性,以字符串为例,我们打印它的__ptoto__并展开,如下可以看到大量我们日常使用的字符串方法均在其中:

 

 

 所有的对象都有__ptoto__属性,而字符串居然也有__proto__属性,__proto__是一个访问器属性,它指向创建它的构造函数的原型prototype。还记得前面做杯子的构造函数吗?每实例个杯子其实只有直径与高度属性,但通过实例的__proto__属性我们找到了构造函数CupCustom的原型prototype,从而成功访问了prototype上的color属性。

prototype:是函数的一个属性(每个函数都有一个prototype属性),这个属性是一个指针,指向一个对象。它是显示修改对象的原型的属性。

__proto__:是一个对象拥有的内置属性(请注意:prototype是函数的内置属性,__proto__是对象的内置属性),是JS内部使用寻找原型链的属性。

那为什么函数的prototype属性下还有一个__proto__属性呢?

我们知道函数有函数表达式,函数声明以及new创建三种模式,而函数声明其实等同于new Function(),我们定义的任意函数本质上也属于原始构造函数Function的实例,那么函数有一个__proto__属性指向构造函数Function的原型不是理所应当的事情么。所以这里我们又得出了一个结论:

每一个函数都属于原始构造函数Function的实例,而每一个函数又能做为构造函数生产属于自己的实例。


 

三、关于prototype

上面已经知道。prototype是函数特有的属性,__proto__是每个对象都有的属性;所以函数对象下面有两个属性,下图1,而不是函数对象就只有一个__proto__属性(实例化的对象)下图2;

 

 

 每个对象都有__proto__属性,对象都能通过此属性找到创建自己构造函数的原型。那么什么是原型呢?原型其实就是一个对象。
上图3中,prototype下面有两个属性:__proto__和constructor,constructor它指向创建它的构造函数,

 

 

 实例的__proto__指向的是创建自己的构造函数的prototype,这个prototype是一个对象;实验是检验真的唯一标准;

 a.__proto__ === Foo.prototype // true  说明:实例化的对象的__proto__ 恒等于构造函数的原型对象prototype;

 让我们来用图形转化来表达;

 

 

通过这个图我们就把上面所说的都总结了;
实例对象的__proto__ 指向构造函数的原型prototype;
构造函数原型对象下面的constructor指向创建自己的构造函数;

我们补充一点知识:

 

数字 123 本质上由构造函数Number()创建,所以数字123通过__proto__访问构造函数Number()原型上的方法属性。

字符串 abc 本质上由构造函数 String()创建,所以abc也能通过__proto__访问构造函数String()原型上的方法属性。

函数本质上由原始构造函数Function创建,所以函数也能通过__proto__访问原始构造函数Function上的原型属性方法,别忘了,我们任意创建的函数都能使用call、apply等方法,不然你以为这些方法是哪来的呢。

上文也说了,我们自己创建构造函数其实和普通函数没任何区别,毕竟每个函数都能使用new调用用于创建属于自己的实例,这种继承方式是不是神似java的类,只是在JavaScript中改用原型prototype了。每一个函数都有作为构造函数的潜力,所以每一个函数都自带了prototype原型。

原始构造函数Function()扮演着创世主女娲的角色,她创造了Object()、Number()、String()、Date()、function fn(){}等第一批人类(也就是构造函数),而人类同样具备了繁衍的能力(使用new操作符),于是Number()繁衍出了数据类型数据,String()诞生了字符串,function fn(){}作为构造函数也诞生了各种各样的对象后代。

我们通过代码证实这一点:

// 所有函数对象的__proto__都指向Function.prototype,包括Function本身
Number.__proto__ === Function.prototype //true
Number.constructor === Function //true

String.__proto__ === Function.prototype //true
String.constructor === Function //true

Object.__proto__ === Function.prototype //true
Object.constructor === Function //true

Array.__proto__ === Function.prototype //true
Array.constructor === Function //true

Function.__proto__ === Function.prototype //true
Function.constructor === Function //true

所以当实例访问某个属性时,会先查找自己有没有,如果没有就通过__proto__访问自己构造函数的prototype有没有,前面说构造函数的原型是一个对象,如果原型对象也没有,就继续顺着构造函数prototype中的__proto__继续查找到构造函数Object()的原型,再看有没有,如果还没有,就返回undefined,因为再往上就是null了,这个过程就是我们熟知的原型链,说的再准确点,就是__proto__访问过程构成了原型链;

那对象可以一直__proto__往下找吗?答案是否定的。实例通过访问器属性__proto__访问创建自己的构造函数原型,相等是很正常的。原型下面的prototype.__proto__返回的是一个对象构造函数的原型Object.prototype,因为prototype是一个对象,对象的构造函数指向的是Object,Object.prototype.__proto__就是原型链的顶端null;上代码,根据下面代码就能理解原型和原型链的关系了;

function Parent() {};
var son = new Parent();
console.log(son.__proto__); //找到了构造函数Parent的原型
console.log(son.__proto__.__proto__); //原型是对象,它的__proto__指向构造函数Object的原型
console.log(son.__proto__.__proto__.__proto__); //null,到头了,null不是对象,没有原型,所以不会继续往上了 


 

总结: 这篇文章写起来说实话我的思路有点乱,但在最后面这张图如果你能理解的话,说明你已经对原型和原型链已经理解了,貌似好像知道了什么是原型和原型链,工作上用的地方好像不多,有一说一,确实~;但它并不影响 我们加深对函数的认识和理解,而且前端面试的时候,这百分之八九十都会问的原型和原型链,如果你理解了的话,相信你就能在面试的过程中迎刃有余;
欢迎大家一起讨论和指导;谢谢大家!

如果我的博客思路不够清晰的话,推荐大家看下这两篇博客:(ps:我也是看这两篇博客理解的)
https://www.cnblogs.com/echolun/p/12321869.html

https://www.cnblogs.com/echolun/p/12384935.html#4569574

 

posted @ 2020-05-08 15:08  八阿哥_小冷  阅读(2540)  评论(8编辑  收藏  举报