【原型】JavaScript原型等概念的理解

1.创建对象的方法

在了解原型链之前,首先先了解一下创建对象的几种方式,介绍以下三种。
【原型】JavaScript原型等概念的理解_第1张图片
代码:


打印结果:
【原型】JavaScript原型等概念的理解_第2张图片
对象是创建出来了,但你可能对结果很诧异,为什么不同呢?别急,慢慢来。

2.构造函数、实例、原型、原型链

先来一张图简单了解一下
【原型】JavaScript原型等概念的理解_第3张图片

2.1 什么是原型对象?实例?构造函数?

概念就不多说了,看代码吧

var M = function (name) { this.name = name; }
var o3 = new M('o3')
  • 实例就是通过new一个构造函数生成的对象。在本例中o3就是实例,M就是构造函数。
  • 每个函数都有prorotype属性(原型),每个对象都有_proto_ 属性(隐式原型)
  • 从上图中可以知道,实例的__protpo__指向原型对象。
  • 从上图中可以知道,实例的构造函数的prototype也是指向原型对象。
  • 原型对象的construor指向构造函数。

再来通过下面这个图来理解一下
【原型】JavaScript原型等概念的理解_第4张图片

2.2 什么是原型链?

简单理解就是原型组成的链,实例的__proto__就是原型,而原型也是一个对象,也有__proto__属性,它会指向另一个原型…就这样可以一直通过__proto__向上找,这就是原型链,当向上找找到Object这个构造函数的原型(即null)时,这条原型链就算到头了。也就是说,原型链的尽头是null 。

2.3 原型作用何在?

原型的存在是为了实现继承。我们先来思考一个问题:假如现在通过一个构造函数创建了多个实例,想要给它们添加同一个方法,该怎么做呢?

1.给每个实例去添加。太过麻烦,并不是一个明智的选择;
2 . 创建构造函数的时候添加方法。这样做的话在每次用构造函数创建实例时都会大量产生方法的副本,影响性能,且不利于代码复用;
3 .这时,就该用上原型了。只要给实例的原型添加一个方法,那么这个原型的所有实例便都有了这个方法。由关系图可知,访问原型有两种方式,一是通过实例的__proto__ ,二是通过构造函数的prototype。一般我们选择后者。接着上面的例子继续演示:

var M = function (name) { this.name = name; }
var o3 = new M('o3')
var o5 = new M()o3.__proto__.say=furnction(){
console.log('hello world')}
/ * 或者
M.prototype.say=furnction(){
console.log('hello world')} 
 */
 o3.say()
 o5.say() 

打印结果
在这里插入图片描述
按照JS引擎的分析方式,在访问一个实例的方法的时候,首先在实例本身中找,如果没找到就去它的原型中找,还没找到就再往上找,直到找到。实例使用原型的方法,就是继承的体现。当然,不止是方法可以继承,属性也是可以继承的。

既然原型的属性可以被实例继承,那么怎么判断属性是实例本身具有的还是继承的?对实例用 hasOwnProperty( )方法即可。那么实例为何有这个方法?同样是继承来的。 由于所有的对象的原型链都会找到Object.prototype,因此所有的对象都会有Object.prototype的方法,其中就包括 hasOwnProperty( )方法 。

2.4 原型、构造函数、实例、Function、Object的关系

前面我们给出了一幅图简单梳理了一下关系,但想追本溯源,光靠那张图是不够的。下面我们给出另一张更详细的图。请先记住,Function和Object 是特殊的构造函数。

【原型】JavaScript原型等概念的理解_第5张图片

首先从构造函数Foo(或任意一个普通构造函数)出发,它创建了实例f1和f2等,而实例的_proto_指向了Foo.prototype这个原型,该原型的_proto_向上再次指向其他构造函数的原型,一直向上,最终指向Object这个构造函数的原型,即Object.prototype。而Object.prototype的 proto 指向了null,这时我们说到达了原型链的终点null。回过头看,该原型又被Object构造函数的实例的_proto_指向,而函数的实例就是我们通常通过字面量创建的那些对象,也即是图中的o1,o2。那么,普通构造函数(这里指Foo)和特殊构造函数Object又来自于哪里?答案是,来自于另一个特殊构造函数Function。

实际上,所有的函数都是由Function函数创建的实例,而构造函数当然也是函数,所以也来自于Function。从图中可以看到,实例Foo的_proto_和实例Object的_proto_ 都指向了 Function的prototype,即Function.prototype 。

既然所有的函数都是由Function函数创建的实例,那么Function又是怎么来的?答案是,Function自己创造了自己。它既作为创造其他实例函数的构造函数而存在,也作为实例函数而存在,所以可以在图上看到作为实例的Function的_proto_ 指向了作为构造函数的Function的prototype。正如我们前面所说的,Function.prototype的_proto_也像其他构造函数.prototype的_proto_一样,最终指向Object.porototype,而Object.porototype的_proto_最终指向null,原型链结束。

可以发现,经过简单梳理,这几者的关系没有我们想象的那么复杂。一句话,看懂这幅图就够了。

3.instanceof的原理

【原型】JavaScript原型等概念的理解_第6张图片
instanceof 沿着 实例—> proto —> … 这条线来找,同时沿着 实例的构造函数的prototype—>proto —> … 这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。如下图,很显然 f1 instanceof Object 成立
【原型】JavaScript原型等概念的理解_第7张图片

注意:正因为 instanceof 的原理如上所述,因此实例的instanceof在比较的时候,与原型链上向上找的的构造函数相比都是true。

继续上面的代码
【原型】JavaScript原型等概念的理解_第8张图片

那怎么判断实例是由哪个构造函数生成的呢?这时候就要用到constructor了。
实例的原型的构造函数, obj.proto.constructor
【原型】JavaScript原型等概念的理解_第9张图片

4.new运算符

new运算符的原理

  • 构造函数在执行的时候,默认会创建并返回一个对象。它继承自 foo.prototype。
  • 构造函数在执行的时候,相应的参数会被传入,同时上下文(this)会被指定为这个新的实例。
  • new foo等同于new foo() ,只能用在不传递任何参数的情况
  • 如果为构造函数显式指定了一个返回对象( return xxxxx ),那个这个对象会取代整个new出来的结果;如果未显式指定返回对象,那么那个new出来的结果为步骤1创建的对象。

下面根据new的工作原理通过代码手动实现一下new运算符

var new2 = function (func) {
var o = Object.create(func.prototype);    //创建对象
var k = func.call(o);         //改变this指向,把结果付给k
if (typeof k === 'object') {       //判断k的类型是不是对象
return k;               //是,返回k
} else {
return o;               //不是,返回构造函数的执行结果
}
}  

验证
【原型】JavaScript原型等概念的理解_第10张图片

经过上图一系列折腾,不难看出,我们手动编写的new2和new运算符的作用是一样的。

5.修复constructor的指向

构造函数的prototype属性指向它的prototype对象,也就是原型对象,在原型对象中有一个constructor属性,指向该构造函数。但是我们在使用构造函数时,一般会重写它的原型(如:obj.prototype = new Dog( )),这会导致constructor的指向出现问题,造成继承链的紊乱,因此为了修复这个错误指向,需要显式指定obj.prototype.constructor = obj 。拿下面例子说明:
【原型】JavaScript原型等概念的理解_第11张图片未重写原型对象之前,实例化了一个dog;第6行重写了原型对象,使其指向另一个实例(等式右边是字面量,因此可以看作是由Object构造函数实例化出来的一个对象),之后实例化了一个cat。

查看dog和cat的constructor:

console.log(dog.constructor);        //function Animal() 
console.log(cat.constructor);         //function Object()

dog.say();   //wan 
cat.say();    //miao

首先,实例默认是没有constructor属性的,因此会向上追溯对应的原型对象的constructor属性。dog.constructor可以指向原来的构造函数,说明原来的原型对象还存在;而cat.constructor 指向另一个构造函数,是因为Animal( )的原型被重写,并且作为Object( )构造函数的一个实例而存在,那么由cat实例出发,向上进行constructor属性追溯的时候,最终会找到Object( ) 构造函数。同样的,正因为原型重写前后创建的实例分别对应了初始原型和新的原型,所以我们可以对旧实例调用初始原型的方法、对新实例调用新的原型的方法,放在本例子中,就表现为dog依然可以调用say( )方法发出wan,而cat也可以调用say( )方法发出miao 。

总结:
重写原型对象之后,会切断构造函数与最初原型之间的连接,使新构造的实例对象的原型指向重写的原型,而先前构造的实例对象的原型还是指向最初原型。在这种情况下,先前的实例对象可以使用最初原型的方法,新的实例对象可以使用重写的原型的方法。

参考文章:
http://www.cnblogs.com/wangfupeng1988/p/3978131.html
https://www.cnblogs.com/chengzp/p/prototype.html
PS:这是两篇我认为介绍原型链比较好的博客,尤其是第一篇。作者是富文本编辑器WangEditor项目的发起者。我个人比较喜欢他讲解的方式,推荐看看。我这篇博客的灵感来自于他们,并且结合了个人的一些心得。

你可能感兴趣的:(JavaScript)