执行环境及作用域
作用域是个很重要的概念,决定了标识符的有效的范围。但是在ECMAScript中,我们先抛开作用的概念和思路,从一个新的概念开始,这就是执行环境,它定义了变量或者函数有权访问的其他数据,决定了它们的各自行为。并且,每个执行环境都有一个与之关联的变量对象,执行环境中定义的所有变量和函数都保存在这个对象当中。虽然无法通过代码来访问这个对象,但是理解才能更好的理解ECMAScript是如何处理数据的。
执行环境
如上所述,执行环境定义了变量或者函数有权访问其他数据,决定了它们的各自行为。定义很拗口,下面慢慢理解。为了简单起见,执行环境有时候就被简称为“环境”。环境可以大致分为两种:
- 全局环境: 最外围的执行环境。根据宿主环境的不同,表示执行环境的对象也不一样,在Web浏览器中,全局执行环境被认为是window对象。
- 局部环境: 每个函数都会有自己的执行环境。当执行进入到一个函数中时,函数的环境就会被压到环境栈中,而在函数执行完后,函数的环境又会被弹出,还原到之前的环境栈。
所谓的局部环境其实是函数专属的
作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链,它的用途就是保证对执行环境有权访问的所有变量和函数的有序访问。所谓的作用域链就可以理解成一个变量对象组成的链表。作用域链的最前端,始终就是当前执行的代码所在环境的变量对象。下一个变量对象则来自下一个包含环境,这样一直延续到全局执行环境。全局环境始终是作用域链的最后一个对象。
标识符的解析就是沿着作用域链一级一级的搜索标识符的过程。搜索过程从最前端开始,逐级向后回溯,一直到找到标识符的定义为止,如果没有找到,通常会导致错误。
var color = "blue"; |
在这个例子当中,函数changColor
的作用域链包含两个对象,第一个是函数自己的变量对象,其中定义着arguments
对象,第二个也是最后一个则是全局环境的变量对象。可以在函数当中访问color
变量也正是因为在这个作用域链中可以找到它。
再看一个稍微复杂一点的例子:
var color = "blue"; |
这里有三个执行环境:
- 全局环境: 定义了color变量和changColor函数;
- changColor局部环境: 定义了anotherColor变量和swapColor函数;
- swapColor局部环境:定义了tmpColor变量;
所以代码执行到 swapColor()
内部时,作用域链是类似于:
swapColor的变量对象-->changColor的变量对象-->全局变量对象 |
所以在swapColor()
内部访问color, anotherColor和tmpColor,因为沿着作用域链找,可以找到他们全部。
如果函数有参数,函数参数也被当做变量来对待,访问规则和执行环境中的其他变量是一致的
没有块作用域
看到这,可能会觉得这个跟C语言的作用域不是一回事嘛,为什么非得弄个作用域链这个复杂的东西,按照C语言的作用域去理解也一样可以得到相同的结论。那下面我们就看一下是不是一回事:
if(true){ |
结果是不是很出乎意料,按照C语言的块级作用域去理解,在块结束后是不能再访问其中的变量的,但这里显然不是这样的。但是按照上面ECMAScript的作用域链去理解就可以很好的解释这个结果,在ECMAScript中没有块级的作用域, if
和for
中定义的变量,会被添加到当前的执行环境中,所以在if
和for
结束之后还是可以继续的访问。
声明变量
综上所述,使用var
定义的变量会自动被添加到最接近的环境中。原来这才是关键,不在乎定义的变量是在if
语句块里还是在for
循环了,它们和其他的普通定义的变量都是一样的。而如果初始化变量时没有用var声明,该变量会自动添加到全局环境中。这也很好的解释了之前的省略var
定义的变量就是全局变量。
延长作用域链
虽然执行环境的类型只有两种——全局和局部(函数),但是也有另外一些方法来延长作用域链。延长作用域链的方法是在当前的作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。下面两种情况会发生这种现象,具体来说就是代码执行到下列任何一个语句时,作用域链会得到加长:
try-catch
语句的catch
块: 创建一个包含被抛出的错误对象的变量对象,将它增加到作用域链前;with
语句:将指定的对象增加到作用域链前。
|
这个例子可以说明两点:
- 在
with
语句块里定义的url
也同样是添加到函数的环境中,所以可以在最后被返回; with
接受的是location
对象,因此变量对象中就包含了location
对象的所有属性和方法,被加到了作用域链的前端,所以可以直接访问href
(其实是location.href
)