在javascript的学习中,执行环境、作用域是2个非常非常重要和基本的概念,理解了这2个概念对于javsacript中很多脚本的运行结果就能明白其中的道理了,比如搞清作用域和执行环境对于闭包的理解至关重要。
一、执行环境(exection context,也有称之为执行上下文)
所有 JavaScript 代码都是在一个执行环境中被执行的。执行环境是一个概念,一种机制,用来完成JavaScript运行时在作用域、生存期等方面的处理,它定义了变量或函数是否有权访问其他数据,决定各自行为。
在javascript中,可执行的JavaScript代码分三种类型:
1. Global Code,即全局的、不在任何函数里面的代码,例如:一个js文件、嵌入在HTML页面中的js代码等。
2. Eval Code,即使用eval()函数动态执行的JS代码。
3. Function Code,即用户自定义函数中的函数体JS代码。
不同类型的JavaScript代码具有不同的执行环境,这里我们不考虑evel code,对应于global code和function code存在2种执行环境:全局执行环境和函数执行环境。
在一个页面中,第一次载入JS代码时创建一个全局执行环境,全局执行环境是最外围的执行环境,在Web浏览器中,全局执行环境被认为是window对象。因此,所有的全局变量和函数都是作为window对象的属性和方法创建的。
当调用一个 JavaScript 函数时,该函数就会进入与该函数相对应的执行环境。如果又调用了另外一个函数(或者递归地调用同一个函数),则又会创建一个新的执行环境,并且在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。因而,运行中的 JavaScript 代码就构成了一个执行环境栈。
Code1:
function Fn1(){ function Fn2(){ alert(document.body.tagName);//BODY //other code... } Fn2(); } Fn1(); //code here |
(图1 执行环境栈 摘自:笨蛋的座右铭的博文)
程序在进入每个执行环境的时候,JavaScript引擎在内部创建一个对象,叫做变量对象(Variable Object)。对应函数的每一个参数,在Variable Object上添加一个属性,属性的名字、值与参数的名字、值相同。函数中每声明一个变量,也会在Variable Object上添加一个属性,名字就是变量名,因此为变量赋值就是给Variable Object对应的属性赋值。在函数中访问参数或者局部变量时,就是在variable Object上搜索相应的属性,返回其值。(另外注意:一般情况下Variable Object是一个内部对象,JS代码中无法直接访问。规范中对其实现方式也不做要求,因此它可能只是引擎内部的一种数据结构。)
大致处理方式就这样,但作用域的概念不只这么简单,例如函数体中可以使用全局变量、函数嵌套定义时情况更复杂点。这些情况下怎样处理?JavaScript引擎将不同执行位置上的Variable Object按照规则构建一个链表,在访问一个变量时,先在链表的第1个Variable Object上查找,如果没有找到则继续在第2个Variable Object上查找,直到搜索结束。这就是Scope/Scope Chain的大致概念。
二、Scope/Scope Chain(作用域/作用域链)
当代码在一个环境中执行时,都会创建基于Variable Object的一个作用域链。 作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。整个作用域链是由不同执行位置上的Variable Object按照规则所构建一个链表。作用域链的最前端,始终是当前正在执行的代码所在环境的Variable Object。如果这个环境是函数(比如Fn2),则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,就是函数内部的arguments对象。作用域链中的下一个Variable Object来自该函数(Fn2)的包含环境(也就是Fn1),而再下一个Variable object来自再下一个包含环境。这样,一直延续到全局执行环境,全局执行环境的Variable Object始终是作用域链中的最后一个对象。
如上所述,作用域链感觉就是一个Variable Object链表,当访问一个变量时,先在链表的第一个Variable Object(最前端)上查找,如果没有找到则继续在第二个Variable Object上查找,直到搜索结束,也就是搜索到全局执行环境的Variable Object中。这也就形成了Scope Chain的概念。
与上面Code1代码对应的作用域链图如下所示(摘自:笨蛋的座右铭的博文,这个图感觉不是很理想,不如下面的图3更形象,把右侧这部分调个个就好了。)
(图2 作用域链图)
如上图所示,对于Code1代码中的函数Fn2所对应的作用域链为:Fn2 Variable Object->Fn1 variable Object->全局对象。也就是说,在Fn2函数中进行变量访问时,首先会在Fn2 Variable object访问内进行寻找,如果没有找到,则向上,搜索Fn1 Variable Object中是否存在,直至找到,停止搜索。
再拿《javascript高级程序设计第二版》中提到的例子来说明一下。代码如下所示:
Code2:
var color="blue"; function changecolor(){ var anothercolor="red"; function swapcolors(){ var tempcolor=anothercolor; anothercolor=color; color=tempcolor; // Todo something } swapcolors(); } changecolor(); //这里不能访问tempcolor和anocolor;但是可以访问color; alert("Color is now "+color); |
在Code2代码中,涉及3个执行环境全局环境、changecolor函数的局部环境和swapcolor局部环境。该段代码的作用域链如下图所示。
上图中,
- -》全局环境有1个变量color和1个函数changecolor()。
- -》changecolor()函数的局部环境中具有1个anothercolor属性和1个swapcolors函数,当然,changecolor函数中可以访问自身以及它外围(也就是全局环境)中的变量。
- -》swapcolor()函数的局部环境中具有1个变量tempcolor。在该函数内部可以访问上面的2个环境(changecolor和window)中的所有变量,因为那2个环境都是它的父执行环境。
通过上面的分析,我们可以得知内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间是线性、有次序的。每个环境都可以向上搜索作用域链,以便查询变量和函数名;但任何环境不能通过向下搜索作用域链条而进入另一个执行环境。对于上述例子的swapcolor()函数而言,其作用域链包括:swapcolor()的变量对象、changecolor()变量对象和全局对象。swapcolor()的局部环境开始先在自己的Variable Object中搜索变量和函数名,找不到,则向上搜索changecolor作用域链。。。。。以此类推。但是,changecolor()函数是无法访问swapcolor中的变量。
关于作用域总结以下几条:
1、javascript 没有块级作用域。
直接上代码:
for(var i=0;i<10;i++){ doSomething(i); } alert(i); // output :10,why? |
上述代码运行后会返回10,为什么呢?如果是同样的java或是c#代码,则不会是10,可能会提示运行错误,因为i只存在于for循环体重,在运行完for循环后,for中的所有变量就被销毁了。而在javascript中则不是这样的,在for中的变量声明将会添加到当前的执行环境中(这里是全局执行环境),因此在for循环完后,变量i依旧存在于循环外部的执行环境。因此,会输出10。
2、声明变量
使用var声明变量时,这个变量将被自动添加到距离最近的可用环境中。对于函数而言,自然声明的变量就会被添加到函数的局部环境中,变量在整个函数环境内都是可用的。
但是,如果变量没有是用var进行声明,将会被添加到全局环境,也就是说成位全局变量了。
所以在函数体内,进行声明时,一般要在开头用var进行声明。
最后出几个小例题:
var x = 1; function rain(){ alert( x ); //弹出 'undefined',而不是1,也不是10,why? var x = '10'; alert( x ); //弹出 '10',why? } rain() |
为什么会是代码中所说明的结果呢?
我认为和2个事情有关:作用域和预解析。我们可以很容易得出上述代码的作用域链。
window全局环境和rain()函数局部环境。window全局环境中存在全局变量x和rain,而rain()函数的局部环境中包括:局部变量x。因此,在执行rain()函数时,不可能去访问全局变量x的了,因为在当前的rain()函数内已经有局部变量x。所以alert出1,
但为什么第一次alert出undefined呢?这可能和预解析有关。在代码执行时,首先会解析出变量和函数的定义,上述代码等价于下面的代码。
function rain(){ var x; alert( x ); x = '10'; alert( x ); } |
主要参考资料;
1、《Javascript高级程序设计第二版》
2、http://www.cnblogs.com/fool/archive/2010/10/16/1853326.html
3、http://www.cn-cuckoo.com/2007/08/01/understand-javascript-closures-72.html
4、http://www.cnblogs.com/RicCC/archive/2008/02/15/JavaScript-Object-Model-Execution-Model.html
5、http://lifesinger.org/blog/2009/01/javascript-run-mechanism/