前言
在我一开始学习java web的时候,对JS就一直抱着一种只是简单用用的心态,因而并无一步一步地去学习,当时认为用法与java相似,可是在实际web项目中使用时却比较麻烦,便直接粗略了解后开始使用jQuery。但现现在,前端发展迅速,js语法方便也有了至关大的改善,而且伴随着node.js的登场,js的适用性也更加普遍。其实也是本身了解到了electron的存在,再加上web开发中前端与后端开发也比较密切,因而这便又掉头回来从新开始学习js。在学习的过程当中,仔细学习了一下js的原型链,也在这里作个记录,若是有不对的地方,还请各位指出,本人感激涕零!javascript
正文
在js的世界中,一切皆对象,那么咱们先将对象分为三类:实例对象、原型对象、函数对象。前端
实例对象简单说就是经过构造函数所建立的对象。java
函数对象好理解,js的函数自己也是个对象,这个对象有这方法名、参数、方法体等属性。构造函数是一种特殊的函数,了解过其余OOP语言都知道,构造函数每每会在实例对象建立的时候调用,主要是用来完成实例对象的初始化操做。可是在js中,构造函数与普通函数并不太大区别,咱们也能够像使用普通函数同样使用构造函数,即不使用new关键字。因此从本质上讲,普通函数也是构造函数,而构造函数只是从功能上区分的一个称呼,体如今代码里就是用不用new关键字。但为了接下来的说明,下面将都会使用构造函数对象。node
原型对象比较特殊, 如今先暂时记住经过实例对象与函数对象都能找到对应的原型对象。web
这三类对象之间其实都有着联系,而经过这些联系就造成了js的完整的原型链。咱们接下来就按照这三类对象之间的关系来逐渐了解原型链。后端
实例对象与构造函数对象
首先来看实例对象与构造函数对象的联系。经过new关键字,咱们能够经过构造函数获得一个实例对象。例如:electron
function Student(name){ this.name = name; } var stu = new Student('wang');
在上面的片断中,Student是一个构造函数,stu则是一个经过Student建立的实例对象。两者的联系很明显,而在js里则体如今实例对象stu的constructor属性中:函数
stu.constructor === Student; // true
那么反过来,咱们虽然不能经过构造函数对象直接找到它全部的实例对象,可是能够经过instanceof
关键字来判断一个对象是否是这个构造函数的实例对象:学习
stu instanceof Student; // true
原型对象与其余两类对象
上面咱们也说了,构造函数与普通函数没有什么区别,那么直接使用构造函数,那this天然是指内置全局对象window。但若是用new,this就指的是新的实例对象,并且这个方法还会返回这个实例对象。到这里大体就能猜到加了关键字new作了什么操做了,它建立了一个新的空对象,而且把构造函数中的this替换为空对象,最后把这个对象返回。this
那么为实例方法增长一个普通函数也这样作,从结果来讲是没有问题的:
function Student(name){ this.name = name; this.say = function(){ console.log`I'm ${name}`; }; } stu1 = new Student('wang'); stu2 = new Student('li'); stu1.say(); // I'm wang stu2.say(); // I'm li stu1.say === stu2.say; // false
可是咱们会发现,stu1与stu2的say函数对象居然不是一个,那就说明若是建立了1000个Student,就会有1000个say函数对象出现,而这1000个say实现的功能彻底一致,这对内存而言显然是极大的浪费。
如何解决这个问题呢?既然多个函数彻底一致,那么天然能够把这个函数对象放在一个地方,当访问stu1和stu2的say函数时,统一去拿这个地方的函数对象便可。若是咱们本身实现这个功能,当在实例对象中使用函数对象时,咱们又得本身去手动去公共的地方寻找函数对象,这么作显然太费劲了。
好在这些js都已经帮咱们作了,每一个构造函数对象都拥有一个prototype属性,这个属性指向的是一个对象,这个对象咱们就叫它原型对象。而这个原型对象又拥有一个与实例对象同样的constructor属性,一样也是指向构造函数对象。
另外,对于每一个对象,又都有一个__proto__的属性指向它的原型对象。当咱们访问一个对象的某个属性时,其实是先在当前对象寻找这个属性,若是没有找到,则会继续到__proto__所指的对象(原型对象)中寻找。
function Student(name){ this.name = name; } Student.prototype.say = function(){ console.log`I'm ${name}`; }; new Student('liu').say === new Student('zhang').say; // true
为方便理解,这里再放一张图,对照着这张图下面的代码就容易看明白了,以后若是遇到不明白的也能够回过头来看图,直观明了。
var chen = new Student('chen') chen.__proto__ === Student.prototype; // true chen.constructor === Student; // true chen.constructor === Student.prototype.constructor; // true
深刻
明白了上面这些概念,咱们把视角放大,再也不局限于Student。前面咱们说到全部对象都有一个__proto__的属性,那么对于函数对象和原型对象天然也不例外,咱们接下来的关注点就是这两类对象的__proto__属性。
首先来看函数对象。在前面的代码中,Student函数对象的__proto__是谁呢?答案是Function的原型对象。
Student.__proto__ === Function.prototype; // true
一切皆对象,那么Function的__proto__又是谁?仍是Function的原型对象:
Function.__proto__ === Function.prototype; // true
为何?由于Function也是个函数对象。一般咱们建立函数的方式为
function xxx(x){...} var yyy = function(y){...};
那么其实还有一种写法:
var zzz = new Function('z','...'); // 例如: var hello = new Function('msg','console.log(msg)'); hello('hi'); // hi
这样的写法显然能直接看出来,Function是个函数对象。因而便有一些有趣的事情了:
Function.__proto__ === Function.prototype; // true Function.constructor === Function; // true Function.constructor === Function.prototype.constructor; // true
Function是本身的函数对象,也是本身的实例对象:
var Function = new Function(...);
至于为何会这样,这就比较像先有鸡仍是先有蛋的问题了。咱们只须要知道全部函数对象(包括Function)的__proto__都指向Function的原型对象。
与Function相似,Object也是一个函数对象。(触类旁通,Array,String,Number等都是)
咱们能够这样建立一个空的Object:
var obj = new Object();
那么Object的原型对象的__proto__是谁呢?是null。
Object.prototype.__proto__; // null
以前说过,当咱们用.操做符去拿一个属性时,js会先在当前对象里寻找,没有的话去__proto__的对象(原型对象)里寻找。那么若是__proto__(原型对象)里尚未,就继续去它的__proto__里寻找,以此重复。那么何时是个头呢?直到__proto__为null时。
咱们知道全部对象都有toString方法,Student的实例对象stu也是个对象,但咱们明显没有给它添加toString方法,为何它会有呢?由于stu的__proto__最终指向的是Object的原型对象。这也就是js继承的本质了。
stu.__proto__; // {constructor: ƒ} stu.__proto__.__proto__; // {constructor: ƒ, …, toString: ƒ, …} stu.__proto__.__proto__ === Object.prototype; // true stu.toString === Object.prototype.toString; // true
因此,遍历全部对象的__proto__最终都会来到Object的原型对象。