JS学习之执行环境和作用域
执行环境和作用域相关介绍
执行环境
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
变量对象
每个执行环境都有与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
虽然我们在编写程序时,无法访问这个对象,但是解析器在处理数据时会在后台使用它。
- 全局执行环境
全局执行环境是最外层的一个执行环境。根据ECMAScript实现所在的宿主环境的不同,表示执行环境的对象也不同。在Web浏览中,全局执行环境被认为是window对象,因此所有的全局变量和函数都是作为window对象的属性和方法创建的。当一个执行环境中的所有代码执行完毕后,该环境就会被销毁,环境内的变量和函数也就不复存在了。对于全局执行环境来说,只有应用程序退出时才会被销毁。
- 函数执行环境
每个函数也有自己的执行环境。
当执行流进入一个函数时,该函数的执行环境就会被推进一个环境栈中,当函数执行完毕后,该函数的执行环境被弹出,把控制权还给之前的执行环境。
- 作用域链
作用域就是变量和函数的可访问范围。若学过C语言,则可知道以下代码中c的作用域就是这个if语句范围内。
if(a>b)
{
int c = a;
}
当代码在一个执行环境中执行时,会创建一个基于变量对象的作用域链。作用域链的用途是保证对执行环境有权访问的变量和函数的有序访问。作用域链的最前端是当前执行环境的变量对象。如果这个环境是函数,那么其变量对象就是其活动对象。活动对象在最开始时只包含一个变量,即arguments对象(全局环境中不存在,这是因为arguments是保存函数传入数据的对象) 作用域链是一个链式结构,那么其下一个变量对象来自包含环境,再下一个来自下一个包含环境 , … … 直到全局执行环境。
因此,全局执行环境的变量对象位于作用域链的末尾。
- 标识符解析
标识符解析是沿着作用域链一级一级地搜索标识符的过程。从作用域链的前端开始,逐级往上搜索,直到找到标识符或找不到标识符而产生错误。
为什么是基于变量对象的作用域链
生成作用域链的目的是为了找到相应的标识符,而对于每个执行环境来说,环境内定义的所有变量和函数均保存在与执行环境相关联的变量对象中。因此,作用域链中保存的是各个执行环境中的环境对象。
接下来看一个示例:
var color = "blue";
function changeColor()
{
var anotherColor = "red";
function swapColors()
{
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
sawpColors();
}
changeColor();
我们来分析一下,全局环境中有一个变量color和一个函数changeColor。changeColor()的局部变量中有一个变量anotherColor和函数swapColors(),在这个局部环境中,可以访问到全局环境中的color变量。swapColors()的局部环境中,有一个变量tempColor, 且通过代码我们可以看到,在这个局部环境中可以访问到changeColor()局部环境和全局环境中的变量,这是因为这两个环境是swapColors()环境的父执行环境。关系图如上。
内部环境可以通过作用域链访问到所有的外部环境,但是外部环境不能访问到内部环境的任何变量和函数。
一个简单的理解方式
作用域链是有着先后关系的,我们把处于作用域前端的想象成古代地位高的人,全局环境由于具有普遍性,即每个内部环境都能访问全局环境中的变量和函数,所以就把全局环境当作古代平民,那么,作用域链最前端的则代表帝王,帝王可以随便拿大臣的,百姓的东西。大臣只能随便拿平民的,不能随便拿帝王的。同样,平民只能拿他自己的,不能随便拿大臣的,帝王的东西。
这些环境之间的关系是线性的,有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名,但任何变量不能向下查询。
函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同。
延长作用域链
有些语句可以在作用域链的前端 临时 增加一个变量对象,该变量对象会在代码执行后被移除。
- try-catch语句中的catch块
- with语句
这两个语句都会在作用域链的前端加上一个变量对象。with语句是添加指定的对象,而catch语句则是创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
我们以红宝书上的例子:
function buildURL()
{
var qs = "nvai";
with(location)
{
var url = href + qs;
}
return url;
}
with语句接收的是location对象,因此其变量对象中就包含了location对象的所有属性和方法,而这个变量对象被添加到作用域链的前端。
用with从对象中创建出的作用域仅在with声明中而非外部作用域有效
with语句块中作用域的‘变量对象’是只读的,不能存储标识符,只能存储在其上一层。 因此,url就成了函数执行环境中的一部分。
意思是说,在with内部,我们不能添加新的变量到with关联的对象中。即 with的对象变量是只读的,不能添加变量,但是可以修改或删除关联对象的属性。
上面的代码中,当执行到with语句时,作用域链为:
- location对象
with的变量对象就是location 对象
- buildUrl()环境的变量对象
- window环境的变量对象
无块级作用域
在类C的语言中,由花括号括起来的代码块一般都有块级作用域(类似于ECMAScript中的执行环境),但是在ECMAScript中,却没有这种概念。
if(a)
{
var b =10;
}
在if条件语句之外,也是可以访问到b的。
声明变量
- 使用var声明的变量会自动添加到最近的执行环境中。在函数内部,最近的执行环境就是函数的局部执行环境。在with语句中,最近的执行环境也是函数的执行环境。(with语句块中作用域的‘变量对象’是只读的,不能存储标识符,只能存储在其上一层)
- 没有使用var声明的变量,该变量会自动添加到全局环境中。
查询标识符
前面也说过了,查询标识符时,是沿着作用域链逐级向上的。首先会在当前执行环境的变量对象中查找,若找到,则停止查找,不管父环境中是不是也有同一个标识符。若没找到,则继续沿着作用域链向上查找,直到找到标识符或者找不到,浏览器报错。