JS原型与原型链终极详解

来源:互联网 发布:mac 查看所有进程 编辑:程序博客网 时间:2024/05/16 04:33

http://www.108js.com/article/article1/10201.html?id=1092

开篇

之前对js中的原型链和原型对象有所了解,每当别人问我什么是原型链和原型对象时,我总是用很官方(其实自己不懂)的解释去描述。有一句话说的好:如果你不能把一个很复杂的东西用最简单的话语描述出来,那就说明你没有真正的理解。最近正在读《JavaScript高级程序设计》,书中对原型对象和原型链的描述让我受益匪浅,下面仅用一个对比性的例子来说明。

我们经常会这么写

    function Person () {        this.name = 'John';    }    var person = new Person();    Person.prototype.say = function() {        console.log('Hello,' + this.name);    };    person.say();//Hello,John
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上述代码非常简单,Person原型对象定义了公共的say方法,虽然此举在构造实例之后出现,但因为原型方法在调用之前已经声明,因此之后的每个实例将都拥有该方法。从这个简单的例子里,我们可以得出:

原型对象的用途是为每个实例对象存储共享的方法和属性,它仅仅是一个普通对象而已。并且所有的实例是共享同一个原型对象,因此有别于实例方法或属性,原型对象仅有一份。

所以就会有如下等式成立:

    person.say == new Person().say
  • 1
  • 1

可能我们也会这么写

    function Person () {        this.name = 'John';    }    var person = new Person();    Person.prototype = {        say: function() {            console.log('Hello,' + this.name);        }    };    person.say();//person.say is not a function
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

很不幸,person.say方法没有找到,所以报错了。其实这样写的初衷是好的:因为如果想在原型对象上添加更多的属性和方法,我们不得不每次都要写一行Person.prototype,还不如提炼成一个Object来的直接。但是此例子巧就巧在构造实例对象操作是在添加原型方法之前,这样就会造成一个问题: 
var person = new Person()时,Person.prototype为:Person {} (当然了,内部还有constructor属性),即Person.prototype指向一个空的对象{}。而对于实例person而言,其内部有一个原型链指针proto,该指针指向了Person.prototype指向的对象,即{}。接下来重置了Person的原型对象,使其指向了另外一个对象,即

 Object {say: function}
  • 1
  • 1

这时person.proto的指向还是没有变,它指向的{}对象里面是没有say方法的,因为此报错。

从这个现象我们可以得出:

在js中,对象在调用一个方法时会首先在自身里寻找是否有该方法,若没有,则去原型链上去寻找,依次层层递进,这里的原型链就是实例对象的proto属性

若想让上述例子成功运行,最简单有效的方法就是交换构造对象和重置原型对象的顺序,即:

    function Person () {        this.name = 'John';    }    Person.prototype = {        say: function() {            console.log('Hello,' + this.name);        }    };    var person = new Person();    person.say();//person.say is not a function
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

一张图让你秒懂原型链

其实,只需要明白原型对象的结构即可:

    Function.prototype = {        constructor : Function,        __proto__ : parent prototype,        some prototype properties: ...    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

总结:

函数的原型对象constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针proto,该指针指向上一层的原型对象,而上一层的原型对象的结构依然类似,这样利用proto一直指向Object的原型对象上,而Object的原型对象用Object.proto= null表示原型链的最顶端,如此变形成了javascript的原型链继承,同时也解释了为什么所有的javascript对象都具有Object的基本方法。

下面这道面试题就是上面图的应用:

昨天又重新看了一下自己写的插件方面的文章再加上看了叶小钗博客中关于面试题的文章,找到了一个关于原型方面的题目,个人感觉很好,然后分析一下。


题目是:

[javascript] view plain copy
 print?
  1. var F = function(){};  
  2. Object.prototype.a = function(){};  
  3. Function.prototype.b = function(){};  
  4. var f = new F();  


问:调用f.a( )和f.b( ) 能成功么?


答案:能调用f.a( )但是不能调用f.b( ); // 反正我一开始是没答出来的~ -。- 哈哈所以才会有这一篇文章,如果有高手可以不用往下看了,不过也可以看看我的理解可取么~


然后就请跟着我的思路,Follow me!

"预习":一般来说我们需要有一定的知识储备,这样有利于我们对于问题的理解,先提一下,大概有一个印象,然后我们带着这些往下看,忘记了的话可以再上来温习一下。

1.JS中,函数本身也是一个包含了方法和属性的对象。

2.prototype是每个函数中都有的一个属性,叫做原型,它是一个对象。

3.__proto__是每个对象都具有的一个属性,这个指向了它的构造函数的原型(prototype),是它的引用(我们称这个属性为链接),而且每个prototype都有__proto__属性,因此本身也包含了指向其原型的链接,由此形成了一条链,我们称为原型链。

4.Object是JS所有对象的父级对象,Object()为构造函数。

5.Function是所有函数对象的构造对象,Function()为构造函数,所以Object和Function都有prototype属性。

6.我们可以通过constructor来查看对象的构造函数,isPrototypeOf来确定某个对象是不是我的原型,hasOwnProperty 方法判断属性是否存在。


好了,有了上面的只是储备,现在开始我们的代码分析之旅。

提示!!:在分析代码的过程中其实会发现一些很神奇的事,这个代码的逻辑分析就好比我们机器人要变身了,身体的一个个零件开始组合,放在这里来说就是一条条原型链(__proto__)要开始悄悄连接了(回顾一下我们前面的知识点)。

1.创建一个函数F(恩,没毛病,匿名函数的形式传给F)

   var F = function(){};


//F.__proto__ ---> Function.prototype    (__proto__ 指向了它的构造函数的原型)

//F.prototype.__proto__ --->Object.prototype  (每个prototype都有__proto__属性)


2.给Object的原型定义一个方法a

Object.prototype.a = function( ){ };


3.给Function的原型定义一个方法b

Function.prototype.b = function( ){ };


4.用F当做构造函数去构造一个对象f

var f = new F( );

这里是我们需要拆分一下才能看懂(不然鬼知道经历了什么!):

var f={ };

f.__proto__=F.prototype;

F.prototype.__proto__= Object.prototype

//f.__proto__ --->F.prototype

//F.prototype.__proto__--->Object.prototype

//f.__proto__.__proto__ ---> Object.prototype

F.call(f);


new的过程:创建了一个f对象,f对象中有一个__proto__属性(这个前面说过),它指向了它构造函数的原型(F.prototype),然后将其作用域指向f。


"奇迹”发送了,一条条链子(原型链)开始已经自己连上了。不知道大家有没有发现,把线全部连起来后,其实可以得到

f.__proto__ --->F.prototype     F.prototype.__proto__ ---> Object.prototype


所以我们最终可以得到 f.__proto__.__proto__ ---> Object.prototype

这就是原型链,它会一层层往上找,直到最终的对象,这样我们就可以调用a方法了。


这个时候大家就会有疑问了,那b方法呢?

我们这边逆推一下:

Function.prototype --->F.__proto__    而我们的F.prototype !=F.__proto__  所以我们不能使用b方法。

哇,分析了这么久,大家不知道糊涂了没有,不过这里我个人感觉__proto__和prototype就是一个承上启下的作用,__proto__就是引用了它构造函数的prototype,只不过__proto__在下面(被构造出来的对象的属性),prototype在上面(构造函数对象的属性)。环环相扣。


Tips:如果这里我们希望有b方法的话,只需要改成这样:F.prototype.b = function(){};



最后说一下原型中存在的小陷阱:

function f( ) { }


var people={

name:"lzj",

say:function(){

return this.name;

  }

};

f.prototype=people;

f.prototype.constructor=f;//当我们把原型对象完全替换的时候需要重置相对应的constructor属性,否则会使我们原有对象不能访问原型的新增属性。

/*如果这里我们没有这一步的话,我们来查看一下f的原型对象的构造对象是谁

f.prototype.constructor  // function Object() { [native code] }

它的构造对象就成了object,而不是f,这样你再去f里面新增属性的时候,f实例出来的对象是不能继承的

*/



0 0