原型与原型链
1.前置知识
在聊原型和原型链之前,我们需要知道几个概念:构造函数和普通函数、函数对象和实例对象。
(1) 构造函数和普通函数
1  |  | 
在函数声明的时候,无法判断一个函数是否为构造函数。只有使用new操作符创建对象时,调用的函数才叫做构造函数。
两者区别:
作用不同
普通函数的作用自然是执行函数体内的代码以实现某种功能。构造函数的作用是用来创建对象。
调用方式不同
普通函数直接调用:
函数名()构造函数是为了创建对象,所以需要使用
new关键字来调用:new 函数名()书写习惯不同
为了区别普通函数和构造函数,构造函数的函数名一般大写开头。
this指向不同
普通函数中的this指向window对象,构造函数中的this则是指向它创建的对象。
写法不同
构造函数中一般不写
return。
所以上述代码中test为普通函数,Test为构造函数。
(2) 函数对象和实例对象
以下面的代码为例:
1  |  | 
在JavaScript中,函数也是对象,所以从这个角度讲,上述代码中Student构造函数也称为函数对象,而通过new操作符创建的对象,称为实例对象,上述代码中st1就是通过Student构造函数创建的实例对象。
不仅仅是构造函数,任何函数从对象的角度讲或者当做对象去使用时,都可以称为函数对象。
这里所说的函数对象不是JS内置的Function对象。
2.原型
什么是原型?我们还是通过上面的代码来讲解:
1  |  | 
假设现在我们需要给st1添加一个speak方法,可以这么做:
1  |  | 
但假如我要给每一个Student创建出来的实例对象都添加上这个speak方法,可以这么做:
1  |  | 
这种方式每创建一个实例对象,就会创建一个新的speak方法,也就是说每个实例对象的speak方法都是唯一的。

但这种方式有个问题:如果我们创建出来的实例对象越来越多,在内存中占用的空间是不是也越来越多。
我们仔细想想,这个speak方法需要每个实例对象都唯一吗?能不能共用一个?答案是可以共用,因为每个实例对象的speak方法做的事情都是一样的。
那么我们要怎么修改呢?可以这么做:
1  |  | 
这时候的内存空间:

这种方式看上去很不错,但是仍然会带来一些问题:
_speak是在全局作用域下声明的,可能会污染全局作用域(变量冲突)。- 随着全局作用域下的函数声明越来越多,全局作用域会变得越来越臃肿。
 
因此,JS就提出了原型的概念。每一个函数对象身上都有一个prototype属性,该属性指向一个对象。
1  |  | 
这个对象叫做原型对象,每个函数对象都有自己的原型对象。
不管是构造函数还是普通函数,都有
prototype属性,只不过在普通函数上这一个属性没有什么作用。所以在讲原型时,我们比较强调构造函数,下文中的函数对象也都指构造函数。
另外,创建实例对象时,实例对象身上会有一个属性__proto__,它指向创建该实例对象的函数对象的原型对象。同一个函数对象创建的每一个实例对象,它们的__proto__属性都指向同一个原型对象。
1  |  | 

这就意味着,如果我们在函数对象的原型对象中添加属性和方法,该函数对象创建的实例对象也可以访问到。JS就是这么做的,JS中对于对象属性和方法的访问顺序是从对象本身到对象的原型,如果对象本身中就有要找的属性或方法,直接使用对象本身中的属性或方法,否则从对象的原型中找。比如:
1  |  | 


那么为了实现我们上面所说的对于speak方法的操作,可以这么做:
1  |  | 

再补充一点,原型对象在函数声明时一同创建,然后挂载到函数对象的prototype属性上,另外原型对象上有一个constructor属性,指向创建它的函数对象,这样两者的关系就紧密结合起来了。
完整关系图:

总结:
- 函数对象上有一个
prototype属性,该属性称为显示原型。它指向一个对象,该对象称为原型对象。 - 函数对象创建的每个实例对象都有一个 
__proto__属性 ,该属性称为隐式原型。它指向创建该实例对象的函数对象的原型对象。 - 原型对象上有一个
constructor属性,它指向创建该原型对象的函数对象。 
所以原型就是一个对象,它的作用是所有实例对象共享属性和方法。
一般是共享方法,因为属性一般都是实例对象独有的,不同实例对象他们的属性不同,比如上面的
st1和st2,他们的name属性各不相同,不应该把name放入Student原型对象中。
有些人可能会问:为什么一定要通过函数对象的
prototype往原型对象上添加方法和属性,能不能通过实例对象的__proto__去添加呢?答案是不行的,实例对象的__proto__属性的意义在于为对象的查找机制提供一个方向或者说一条线路,但它是一个非标准属性,因此实际开发中不可以使用这个属性,这也是为什么称它为隐式原型的原因。
3.原型链
理解了原型之后,原型链理解起来就比较容易。我们还是从上面的例子入手:
1  |  | 
我们会发现上述第九行代码,会输出[object Object],也就是说st1能够执行toString()方法。但是st1这个实例对象本身和它__proto__所指向的原型对象上都没有这个方法。那这个方法是哪来的呢?
我们知道,Object对象是所有对象的“祖先”,既然原型对象也是一个对象,那它也不例外,所以其实原型对象是Object对象的实例对象。既然是实例对象,那它身上就会有一个__proto__属性,指向Object对象的原型对象。

我们简化一下这张图:

接着我们来看下Object对象的原型对象上有没有toString()方法:

果然有,这说明了什么?说明JS对于对象属性和方法的查找规则是这样的:
先从对象本身去找,如果找不到,就去该对象的原型对象上去找,如果还是没有找到,就去原型对象的原型对象上去找……但原型对象不可能永无止境,Object对象的原型对象__proto__属性值为null,也就是说查找到Object对象的原型对象就结束了,如果还是没有找到,则返回undefined。

像图上这条被__proto__链接起来的链式关系,就叫原型链。它直接反应了JS对于对象属性和方法的查找顺序。
4.补充
理解完了原型和原型链,我们再想一想,所有的函数都可以通过new Function()的方式创建,那么也就是说所有的函数都是Function对象的实例对象。既然是实例对象,那它身上就会有一个__proto__属性,并且这一属性指向Function对象的原型对象。

那么Function对象是谁的实例对象呢?
我们刚说所有函数都是Function对象的实例对象,而Function对象也是构造函数,那么不就成了Function对象创建了Function对象?其实不必太过纠结这一点,因为Function对象是JS的内置对象,在脚本还没开始执行,就已经创建好了。所以Function.__proto__ === Function.prototype,记住这一个特殊情况就好了。

Object对象和Function对象都是函数对象,也就是构造函数。只不过它们两个都是JS的内置对象,所以没有特别标注函数对象。
这里有一张非常流行的关于原型的图,就是我们刚刚讲到的所有内容:

5.继承
继承是面向对象编程的一个概念。继承可以使子类具有父类的属性和方法,同时可以在子类中重新定义或追加属性和方法。继承是类与类之间的关系。
但是JavaScript并没有类的概念,只有对象。那怎么会有继承呢?因为JavaScript是非常灵活的,我们可以通过构造函数和原型来模拟类的继承。
如果熟悉面向对象编程的语言,我们会发现JS中的构造函数和类有点相似。在ES6中,JS也提出了类的概念,但ES6中的类其实是一个语法糖,它的本质还是构造函数,感兴趣的可以自行了解。
(1) 通过构造函数继承属性
假如我们现在有一个Father构造函数,并且希望有另一个构造函数继承Father构造函数,我们可以这么做:
1  |  | 
但是这样还不够,因为这样Son构造函数只继承了Father构造函数的属性,没有继承Father构造函数的方法:
1  |  | 

接下去,我们就需要让Son构造函数继承Father构造函数的方法。
(2) 通过原型对象继承方法
由上面的图像结合原型链的知识,我们可以发现,如果让Son原型对象链接到Father原型对象,那我们Son构造函数创建的实例对象,是不是就可以使用Father原型对象上的方法了?

原型对象的链接是依靠__proto__属性去操作的,所以最简单的方式:
1  |  | 
但是我们说过__proto__属性不能直接使用。那有没有什么办法同样能实现上述代码的效果?我们可以这么做:
1  |  | 

这样Father构造函数创建的实例对象就变成了Son构造函数的原型对象,Son构造函数new出来的实例对象,也指向这一个Father构造函数创建的实例对象。从而达到我们预先的目的。

除此之外还差一步,我们原先说过原型对象中有一个constructor属性指向构造函数,我们修改了构造函数的原型对象,那就需要将新的原型对象中的constructor属性指向构造函数。
1  |  | 

上述这种继承方式叫做组合继承。
参考: