前端【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