原型链---小白专用

来源:互联网 发布:淘宝手机类目保证金 编辑:程序博客网 时间:2024/05/16 11:13

原型对象与原型链

      JavaScript对象是一个属性的集合,另外有一个隐式的对象:原型对象。原型的值可以是一个对象或者null。一般的引擎实现中,JS对象会包含若干个隐藏属性,对象的原型由这些隐藏属性之一引用,我们在本文中讨论时,将假定这个属性的名称为"__proto__"(事实上,SpiderMonkey内部正是使用了这个名称,但是规范中并未做要求,因此这个名称依赖于实现)。

由于原型对象本身也是对象,根据上边的定义,它也有自己的原型,而它自己的原型对象又可以有自己的原型,这样就组成了一条链,这个链就是原型链。

JavaScritp引擎在访问对象的属性时,如果在对象本身中没有找到,则会去原型链中查找,如果找到,直接返回值,如果整个链都遍历且没有找到属性,则返回undefined.原型链一般实现为一个链表,这样就可以按照一定的顺序来查找。

 

结合下边的例子:

 

 

Js代码  收藏代码
  1. var base = {  
  2.     name : "base",  
  3.     getInfo : function(){  
  4.        return this.name;  
  5.     }  
  6. }  
  7.    
  8. var ext1 = {  
  9.     id : 0,  
  10.     __proto__ : base  
  11. }  
  12.    
  13. var ext2 = {  
  14.     id : 9,  
  15.     __proto__ : base  
  16. }  
  17.    
  18. print(ext1.id);  
  19. print(ext1.getInfo());  
  20. print(ext2.id);  
  21. print(ext2.getInfo());  
 

 

 

可以得到:

 

0
base
9
base

 

 


图 上例中对象的原型链

 

可以看到,当执行ext1.id时,引擎在ext1对象本身中就找到了id属性,因此返回其值0,当执行ext1.getInfo时,ext1对象中没有找到,因此在其原型对象base中查找,找到之后,执行这个函数,得到输出”base”。

 

我们将上例中的ext1对象稍加修改,为ext1对象加上name属性:

 

 

Js代码  收藏代码
  1. var base = {  
  2.     name : "base",  
  3.     getInfo : function(){  
  4.        return this.name;  
  5.     }  
  6. }  
  7.    
  8. var ext1 = {  
  9.     id : 0,  
  10.     name : "ext1",     
  11.     __proto__ : base  
  12. }  
  13.    
  14. print(ext1.id);  
  15. print(ext1.getInfo());  
 

 

 

可以看到:

 

0
ext1

 

 

 

这个运行效果同样验证了原型链的运行机制:从对象本身出发,沿着__proto__查找,直到找到属性名称相同的值(没有找到,则返回undefined)。

 

我们对上例再做一点修改,来更好的演示原型链的工作方式:

 

 

Js代码  收藏代码
  1. var base = {  
  2.     name : "base",  
  3.     getInfo : function(){  
  4.        return this.id + ":" + this.name;  
  5.     }  
  6. }  
  7.    
  8. var ext1 = {  
  9.     id : 0,  
  10.     __proto__ : base  
  11. }  
  12.    
  13. print(ext1.getInfo());  
 

 

我们在getInfo函数中加入this.id,这个id在base对象中没有定义。同时,删掉了ext1对象中的name属性,执行结果如下:

 

0:base

 

应该注意的是,getInfo函数中的this表示原始的对象,而并非原型对象。上例中的id属性来自于ext1对象,而name来自于base对象。这个特性的机制在10.3小节再做讨论。如果对象没有显式的声明自己的”__proto__”属性,这个值默认的设置为Object.prototype,而Object.prototype的”__proto__”属性的值为”null”,标志着原型链的终结。

 

10.1.2构造器

我们在来讨论一下构造器,除了上边提到的直接操作对象的__proto__属性的指向以外,JavaScript还支持构造器形式的对象创建。构造器会自动的为新创建的对象设置原型对象,此时的原型对象通过构造器的prototype属性来引用。

 

我们以例子来说明,将Task函数作为构造器,然后创建两个实例task1, task2:

 

 

Js代码  收藏代码
  1. function Task(id){  
  2.     this.id = id;  
  3. }  
  4.    
  5. Task.prototype.status = "STOPPED";  
  6. Task.prototype.execute = function(args){  
  7.     return "execute task_"+this.id+"["+this.status+"]:"+args;  
  8. }  
  9.    
  10. var task1 = new Task(1);  
  11. var task2 = new Task(2);  
  12.    
  13. task1.status = "ACTIVE";  
  14. task2.status = "STARTING";  
  15.    
  16. print(task1.execute("task1"));  
  17. print(task2.execute("task2"));  
 

 

运行结果如下:

 

execute task_1[ACTIVE]:task1
execute task_2[STARTING]:task2

 

 

 

构造器会自动为task1,task2两个对象设置原型对象Task.prototype,这个对象被Task(在此最为构造器)的prototype属性引用,参看下图中的箭头指向。




图 构造器方式的原型链

 

由于Task本身仍旧是函数,因此其”__proto__”属性为Function.prototype, 而内建的函数原型对象的”__proto__”属性则为Object.prototype对象。最后Obejct.prototype的”__proto__”值为null.

10.2执行期上下文

执行器上下文的概念贯穿于JavaScript引擎解释代码的全过程,这个概念是一个运行期的概念,执行器上下文一般实现为一个栈。按照ECMAScript的规范,一共有三种类型的代码,全局代码(游离于任何函数体之外),函数代码,以及eval代码(eval接受字符串,并对这个字符串求值,通常来讲,函数式编程都会提供这个函数或者类似的机制)。这三种代码均在自身的执行期上下文中求值。全局上下文仅有一个,函数上下文和eval上下文则可能有多个。

引擎在调用一个函数时,进入该函数上下文,并执行函数体,与其他程序设计语言类似,函数体内可以有递归,也可以调用其他函数(进入另外一个上下文,此时调用者被阻塞,直至返回)。调用eval会有类似的情况。

 

引擎在初始化之后,将golbal的上下文对象压入栈:




图 执行期上下文栈(初始状态)

 

 

Js代码  收藏代码
  1. <span style="font-family: 'Courier New'; font-size: x-small;">(function(name){  
  2.     print("hello, "+name);  
  3. })("jack");</span>  

 

 

 

执行上边代码的时候,进入函数执行期上下文,将匿名函数执行期上下文压入栈中:

 




图 进入函数执行期上下文

 

执行完成之后,弹出该上下文对象。既然执行期上下文栈中存放的是执行期上下文对象,那么我们来详细看看这个对象的结构。上文提到,ECMAScript代码有三类,对应的执行期上下文对象也有三类,每个上下文对象都有一些必须的属性用以为执行于其上的代码服务,以及记录/跟踪代码执行状态等。

 

一个典型的上下文对象的结构如下:

 




图 上下文对象结构

 

当然,根据不同的实现,这个对象可以包含任意其他的属性(上图中的<properties>部分)。每个上下文对象所需要包含的有变量对象,作用域链以及this.这三个属性在不同类型的上下文对象中意义可能不同。

变量对象只是一个抽象概念,在全局上下文中,变量对象是全局变量自身。而在函数上下文中,变量对象表现为活动对象,活动对象将在下一小节展开,对于eval上下文,eval可能使用全局的变量对象,也可能使用函数的变量对象,这取决于其调用的位置。因此可以说,变量对象有两类,全局的变量对象(全局变量global本身或者活动对象,eval使用这两者之一)。

 

结合执行器上下文栈和原型对象,我们可以得到下列的示意图:

 




图 执行器上下文栈及变量对象,原型链示意图

10.3活动对象

在JavaScript中,当一个函数被调用的时候,就会产生一个特殊的对象:活动对象。这个对象中包含了参数列表和arguments对象等属性。由于活动对象是变量对象的特例,因此它包含变量对象所有的属性如变量定义,函数定义等。

 

我们来看一个实例:

 

 

Js代码  收藏代码
  1. function func(handle, message){  
  2.     var id = 0;  
  3.     function doNothing(x){  
  4.        return x;  
  5.     }  
  6.     handle(message);  
  7. }  
  8.    
  9. func(print, "hello");  
 

 

当代码执行到func(print, “hello”)时,活动对象被创建,这个活动对象的图形示意如下:




图 上例中函数调用时的活动对象

 

10.4作用域链

作用域链与原型链类似,也是一个对象组成的链,用以在上下文中查找标识符(变量,函数等)。查找时也与原型链类似,如果激活对象本身具有该变量,则直接使用变量的值,否则向上层搜索,一次类推,知道查找到或者返回undefined。作用域链的主要作用是用以查找自由变量,所谓自由变量是指,在函数中使用的,非函数内部局部变量,也非函数内部定义的函数名,也非形式参数的变量。这些变量通常来自于函数的“外层”或者全局作用域,比如,我们在函数内部使用的window对象及其属性。

 

关于作用域链及自由变量,我们可以来看下面一个例子:

 

 

Js代码  收藏代码
  1. var topone = "top-level";  
  2.    
  3. (function outter(){  
  4.     var middle = "mid-level";  
  5.      
  6.     (function inner(){  
  7.        var bottom = "bot-level";  
  8.         
  9.        print(topone+">"+middle+">"+bottom);  
  10.     })();  
  11. })();  

  

 

在函数inner之中,print语句中出现的topone, middle变量就是自由变量。

 



 

图 上例中的作用域链

 

根据上图我们可以看出,内部函数的作用域链,由两部分:内部函数自身的活动对象,内部函数的一个属性”[[scope]]”,而”[[scope]]”的值为其外部函数outter的活动对象,其更外部的全局global对象的变量对象。这样,如果在inner中要使用外部的自由变量,显然可以很方便的沿着作用域链上溯。

事实上,函数的属性”[[scope]]”会在函数对象创建的时候被创建,这个特性在下一小节中讨论,而不论函数的嵌套层次有多深,它的”[[scope]]”总会引用所有的位于其外层的上下文中的变量对象(在函数中,为活动对象)。

 

10.5 this值

this在之前的章节中做过讨论,在ECMAScript的规范中对this的定义为:this是一个特殊的对象,与执行期上下文相关,因此可以称之为上下文对象。this是执行期上下文对象的一个属性,参见本章10.2小节的图。

由于this是执行期上下文对象的属性,因此在代码中使用this,其值直接从上下文对戏那个中获得,而无需查找作用域链,其值在进入上下文的那个时刻被确定。

 

在全局上下文中,this是全局对象本身:

 

Js代码  收藏代码
  1. <strong>var attribute = "attribute";  
  2.    
  3. print(attribute);  
  4. print(this.attribute);</strong>  

 

执行结果为:

 

attribute
attribute

 

 

 

而在函数上下文中,不同的调用方式可以有不同的值。

10.5.1 词法作用域

 

在JavaScript中,函数对象的创建和函数本身的执行是完全不同的两个过程:

 

Js代码  收藏代码
  1. <strong>function func(){  
  2.     var x = 0;  
  3.     print("function func");  
  4. }</strong>  

  

是为函数的创建,而下面这条语句:

 

Js代码  收藏代码
  1. func();  
  

才是函数的执行。

 

所谓词法作用域(静态作用域)是指,在函数对象的创建时,作用域”[[scope]]”就已经建立,而并非到执行时,因为函数创建后可能永远不会被执行,但是作用域是始终存在的。

 

比如在上例中,如果在程序中使用没有调用func(),那么,func对象仍旧是存在的,在内存的结构可能是这样的:

 

Js代码  收藏代码
  1. func.["[[scope]]"] = global.["variable object"];  
  

而当函数执行时,进入函数执行期上下文,函数的活动对象被创建,此时的作用域链是活动对象和”[[scope]]”属性的合成。

 

10.5.2 this的上下文

this值是执行期上下文对象的一个属性(执行期上下文对象包括变量对象,作用域链以及this)。执行期上下文对象有三类,当进入不同的上下文时,this的值会确定下来,并且this的值不能更改。结合前面小节讨论的内容,在执行全局代码时,控制流会进入全局执行期上下文,而在执行函数时,又会有函数执行期上下文。我们来看下面一个例子:


Js代码  收藏代码
  1. <strong>var global = this;  
  2. var tom = {  
  3.     name : "Tom",  
  4.     home : "desine",  
  5.     getInfo : function(){  
  6.        print(this.name + ", from "+this.home);  
  7.     }  
  8. };  
  9.    
  10. tom.getInfo();  
  11.    
  12. var jerry = {  
  13.     name : "Jerry",  
  14.     getInfo : tom.getInfo  
  15. }  
  16.    
  17. jerry.getInfo();  
  18.    
  19. global.getInfo = tom.getInfo;  
  20. global.getInfo();</strong>  

 

 

 

执行结果为:

 

Tom, from desine
Jerry, from undefined
undefined, from undefined

  

tom对象本身具有name和home属性,因此在执行tom.getInfo时,会打印tom对象上的这两个属性值。当将global.getInfo属性设置为tom.getInfo时,getInfo中的this值,在运行时,事实上是global对象(还记得在全局执行期上下文对象中,global的变量对象的this值吗?)的变量对象中的this就是自身。而global.name和global.home都没有定义,因此会得到上边的结果。

0 0
原创粉丝点击