执行环境、变量对象和作用域链

执行环境又称执行上下文,英文缩写是EC(Execution Context),每当执行流转到可执行代码时,即会进入一个执行环境。在JavaScript中,执行环境分三种:

  • 全局执行环境 — 这个是最外围的代码执行环境,一旦代码被载入,引擎最先进入的就是这个环境。在浏览器中,全局环境就是window对象,一次所有全局属性和函数都是作为window对象的属性和方法创建的。全局执行环境直到应用程序退出时才会被销毁。

  •  函数执行环境 — 当执行一个函数时,JavaScript引擎进入执行环境。某个执行环境中的代码执行完之后,该环境销毁,保存在其中的所有变量和函数定义也随之销毁。

  •  Eval执行环境 — Eval的执行环境和函数调用的执行环境相同。

活动的执行环境构成一个栈:栈的底部始终是全局环境,顶部是当前活动的执行环境。当执行流进入一个函数时,函数的环境被压入栈中。而在函数执行完之后,栈将其环境弹出,把控制权返回给之前的执行环境。

建立一个执行环境分为两个阶段:

  1. 进入上下文阶段:发生函数调用,进入执行环境时,此时具体的函数代码还没有执行。

  2. 执行代码阶段:进行变量赋值,函数引用,以及执行其它代码。

变量对象的英文缩写是VO(Variable Object),每一个执行环境都对应一个变量对象,这个对象存储着环境中定义的以下内容:

1. 函数的形参  
  
2. var声明的变量  
  
3. 函数声明(但不包含函数表达式)  

变量对象有两种存在方式,一种就是全局对象(用Global表示),存放着全局属性和函数,我们可以通过this关键字引用到该对象。另外一种是函数执行环境中定义的变量对象,改对象在函数的执行上下文中是不能直接访问的,被称为活动对象,英文缩写为AO(Activation Object)。

接下来我们来看下再不同的执行环境中,变量对象是怎样初始化的?

首先是全局环境中的变量对象,这个对象就是全局对象,全局对象是在进入任何执行环境之前就已经创建了的对象。这个对象只存在一份,它的属性在程序中的任何地方都可以访问,全局对象的生命周期终止于程序退出的那一刻。全局对象的初始化阶段,将Math、String等作为自身属性,初始化如下:

Global = {  
   Math:{...},  
   String:{...},  
   ...  
   ...  
   window:Global // 引用自身  
}; 

接下来我们重点研究下函数执行环境中的变量对象,即上文提到的活动对象。活动对象是在进入函数执行环境时创建的,它通过函数的arguments属性初始化:

AO = {  
   arguments: {...} //参数对象,包括callee, length等属性  
};  

理解了变量对象的初始化之后,接下来就是进入执行环境的代码部分了。上文中提到过,执行环境的建立分为两个阶段,第一个阶段就是进入上下文阶段。在该阶段,变量对象包含以下属性:

  1. 函数的所有形参:全局环境中没有形参,这里只是针对函数的执行环境而言。此时由形参名称和对应值构成变量对象的属性。如果没有传递相应的形参值,对应值为undefined。
  2. 所有的函数声明:需要注意的是这里特指函数的声明,函数表达式不算。此时有函数名和对应的函数对象构成变量对象的属性。如果变量对象已经存在同名的属性,则覆盖这个属性。
  3. 所有的变量声明:由var关键字声明的变量,由变量名和对应值组成,作为变量对象的属性。如果变量名与已经声明的形参或函数名相同,则变量声明不会干扰已经存在的这里属性。

上文中,我们提到过变量声明提前的问题,在这里就反映为在进入上下文阶段,首先将初始化变量声明,构成变量对象的属性,此时该属性的值为undefined。例如下面的例子:

function test(a, b){  
   console.log(a); // 10  
   console.log(b); // undefined  
   console.log(c); // undefined  
   console.log(d); // function d(){}  
   console.log(e); // undefined  
   console.log(f); //Reference error  
   var c = 10;  
   function d(){}  
   var e = function _e(){};  
   (function f(){});  
}  
test(10);  

我们考虑进入到带有参数10的test函数的执行环境时,在进入上下文阶段,活动对象初始化如下:

AO(test) = {  
   a: 10,  
   b: undefined,  
   c: undefined,  
   d: 指向函数d,  
   e: undefined  
};  

活动对象不包含属性f,这是因为f是一个函数表达式,而不是函数声明,函数表达式不会影响到变量对象。函数_e同样是函数表达式,但是它分配给了变量e,所以赋值语句执行后,就可以通过e访问到函数表达式_e。
接下来进入到执行环境的第二个阶段,执行代码。在这个阶段开始时,变量对象已经拥有了属性,参考上面的例子,代码执行后变量对象被修改为:

AO(test) = {  
   a: 10,  
   b: undefined,  
   c: 10,  
   d: 指向函数d,  
   e: 指向函数表达式_e  
}; 

理解了以上内容后,我们再来看一个例子:

function test2(a){  
   console.log(a); // function a(){}  
   var a = 3;  
   console.log(a); // 3  
   function a(){};  
}  
test2(20);  

上文中提到的在进入到执行上下文阶段时,变量对象会被初始化,在初始化阶段,变量声明构成的对象属性是最后被执行的,并且如果变量名和已经声明的函数名或形参同名的话,变量声明不会干扰到已经存在的属性,所以在函数执行环境的第一阶段,变量对象为:

AO(test2) = {  
   a: 指向函数a  
}; 

不过,在紧接着的代码执行阶段,属性a被重新赋值为3。

另外,需要特别指明的是变量只能通过var关键字来声明,对于类似于a=4这样的赋值语句,如果a没有通过var声明的话,相当于是创建了一个全局对象的属性,而并没有创建新的变量,它之所以可以认为是全局变量对象的属性,仅仅是因为全局对象等同于全局变量对象。参考以下代码:

function test3(){  
   console.log(a); // undefined  
   console.log(b); // Reference error  
   var a = 3;   
   b = 4;  
}  
test3();

所以在函数执行环境的第一阶段,变量对象为:

AO(test2) = {  
   a: undefined  
};  

因为b不是一个变量,所以在这个阶段,根本就不存在b,只有在代码执行阶段,b才会以全局对象的属性出现。但是还未执行到这里之前,就已经出错了。另外一个需要记住的就是,通过var声明的变量不能通过delete删除,而属性则可以,所以上述例子中的a是不可以通过delete删除的,而b则可以。

现在我们已经知道,执行环境中的数据作为属性存储在变量对象中,同时也知道,变量对象在在每次进入执行环境时创建,并初始化,在代码执行时,更新属性的值。接下来,将讨论下作用域链的概念。

作用域链大多数时候和内部函数有关,我们可以创建内部函数,甚至可以从父函数中返回这些函数。示例代码如下:

var x = 10;  
function foo(){  
   var y = 20;  
   function bar(){  
      console.log(x + y);  
   }  
   return bar;  
}  
foo(); // 30  

每个环境都有自己的变量对象,作用域链正是内部环境所有变量对象(包括父变量对象)的列表。此链用来在标识符解析中查找变量。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。对于上面的例子,bar执行环境中的作用域链包括:bar变量对象、foo变量对象和全局变量对象。

函数的作用域链是在函数调用时创建,包含这个函数的活动对象和[[scope]]属性。示例如下:

活动的执行环境 = {  
   AO: 变量对象,  
   this:thisValue,  
   Scope: [变量对象列表] // 作用域链  
};  

其中Scope = 被调用函数的活动对象 + 被调用函数的[[scope]]属性。

这种标识符的解析过程,与函数的生命周期有关。函数的生命周期可以分为创建和激活(调用时)两个阶段。在函数创建时,函数对象的内部存在一个[[scope]]属性,[[scope]]是所有父变量对象的层级链。[[scope]]属性在函数创建时被存储,永远不变,直到函数被销毁。函数可以不被调用,但该属性一直存在。与作用域链相比,作用域链是活动的执行环境的一个属性,而[[scope]]是函数的属性。

参考以上例子,foo函数在进入全局环境后被创建,此时foo函数拥有了[[scope]]属性,如下图所示:

同样的,bar函数在进入到foo函数的执行环境时被创建,此时foo函数的活动对象已经被创建,所以bar函数的[[scope]]属性如下图所示:

然后,在函数调用激活阶段,生成的活动对象和[[scope]]属性共同组成执行环境的作用域链。也就是说将活动对象添加到 [[scope]]链表的最前端,在查找标识符时,首先从自身变量对象开始,逐渐向父变量查找。

另外需要特别注意的是,通过构造函数创建的函数的[[scope]]属性中仅包含全局对象。

posted @ 2015-02-10 10:57  VadarTeam  阅读(946)  评论(0编辑  收藏  举报