再谈 JavaScript 执行环境与作用域
前面我说到过,执行环境是js中最为重要的一个概念。执行环境定义了变量和函数有权访问的其他数据,决定了它们各自的行为。(接下来的概述主要来自《高性能JavaScript》一书,以及本人的一些简单的理解。)
一、函数作用域
在JavaScript中,每一个函数都表示为一个对象,更确切地说,是Function对象的一个实例。Function对象与其他的对象一样,都拥有可以编程访问的属性,和一系列不能通过代码访问而仅提供了JavaScript引擎存取的内部属性。譬如[[Call]]属性,表示这个对象可以被执行,其中有一个内部属性是[[Scope]],由ECMA-262标准第三版定义。
内部属性[[Scope]]包含了一个函数被创建的作用域中的对象的集合。这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。函数作用域中的每个对象都被称为可变对象,每个可变对象都以“键值对”的形式存在。当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象所填充。
上面这句画的意思就是函数被创建的时候会有一个我们无法访问的[[scope]]属性,[[scope]]属性中包含了当前函数可以访问的作用域中的所有的对象的集合或者说是列表,而这个集合或者说列表被称为函数的作用域链,函数只能访问到这个作用域链中的数据。函数的作用域链中保存着变量对象,这些变量对象都以“键值对”(属性和值)的形式存储。当一个函数被创建后,它的作用域链中会有一个,保存了当前执行环境中的所有的变量和函数的对象(变量对象)。
function sum(num1, num2){ var num3 = 10; return num1 + num2 + num3; }
上面的sum()函数创建的时候,他的作用域链中插入了一个变量对象,这个变量对象包含了所有全局执行环境中定义的变量或函数。例如window、document、sum等等。如下图所示:
当sum()函数被调用(执行)的时候会创建一个被称为执行环境(execution context)的内部对象。函数每次调用时对应的执行环境都是独一无二的,所以多次调用同一个函数就会导致创建多个执行环境。当函数执行完毕时,执行环境就会被销毁。
function sum(num1, num2){ var num3 = 10; return num1 + num2 + num3; } var count = sum(5);
每个执行环境都有自己的作用域链,用于解析标识符(也就是查询变量是否存在)。当执行环境被创建时,它的作用域链就会被初始化,连同运行函数的[[Scope]]属性中所包含的对象(如上图所示)。这些值按照它们出现在函数中的顺序,被复制到执行环境的作用域链中。这项工作一旦完成,一个被称作“激活对象”的新对象就为执行环境创建好了。这个激活对象作为函数执行期间的一个变量对象,包含访问所有局部变量,命名参数,参数集合以及this。然后,这个激活对象被推入作用域链的前端。当作用域链被销毁时,激活对象也一同被销毁。如下图所示。
上图显示了函数在被调用时作用域的变化,关于变量声明和函数声明初始化以及函数参数初始化的值以及冲突的解决方案。可以看我之前的博客。 http://www.cnblogs.com/miracle-t/p/5484420.html
在函数执行过程中,没遇到一个变量,都会经历一次标识符解析的过程,以确定从哪里获取或存储数据。这个过程会搜索执行环境的作用域链,查找同名的标识符(变量名)。搜索的过程从作用域的头部开始,也就是当前运行函数的活动对象。如果找到了,就是用这个标识符对应的变量;如果没有找到,就继续搜索作用域链中的下一个对象。如果整个作用域链中所有对象都没有该标识符,那么表示该标识符是未定义的。
二、改变作用域
一般来说,一个执行环境的作用域链是不会发生改变的。但是,有两个语句可以在执行时临时改变作用域链。
第一个语句是with语句,with语句用来给对象的所有属性创建一个变量。在其他语言中,类似功能通常用来避免书写重复代码。请看下列代码:
var obj = {name : "MT", age : 24, sex : "men"} function checkObj(){ with(obj){ console.log(name); console.log(myName); var myName = name, myAge = age, mySex = sex; } console.log(mySex); console.log(name); console.log(sex); } checkObj();
上面的代码执行到with语句的时候,执行环境的作用域临时被改变了。一个新的变量对象被创建,它包含了参数指定的对象的所有属性。这个对象被推入作用域链的顶端,这意味着函数的所有局部变量现在处于作用域链中的第二个对象中,因此访问的代价更高了。如下图所示:
通过把obj对象传递给with语句,一个包含了全局的obj对象的所有属性的新的变量对象就被推入到作用域链的头部。这使得访问obj对象的属性非常快,而访问局部变量则变慢了,不如变量myName。因此,最好避免使用with语句。
在JavaScript中,并不是只有with语句能人为的修改作用域链,try-catch语句中的catch子句也具有同样的效果。当try代码块中发生错误,执行过程会自动跳转到catch子句,然后把异常对象推入一个变量对象并置于作用域的首位。在catch代码块内部,函数所有局部变量将会放在第二个作用域链对象中。请看下列代码:
try{ methodThatMightCuseAnError(); }catch(ex){ alert(ex.message); //作用域链在此处改变。 }
请注意,一旦catch子句执行完毕,作用域链就会返回之前的状态。
三、动态作用域
无论是with语句还是try-catch语句中的catch子句,或是包含eval()的函数,都被认为是动态作用域。动态作用域只存在于代码执行过程中,因此无法通过静态(查看代码结构)分析检测出来。例如:
function execute(code){ eval(code); function subroutine(){ return window; } var w = subroutine(); //w是什么? }
由于使用了eval(),函数execute()看上去像动态作用域。变量w的值会随着code的值改变。大部分情况下,w等同于全局的window对象,但是考虑如下情况:
execute("var window = {}");
以上diam中,execute()中的eval()创建了一个局部变量window,因此w等同于局部变量window,而非全局window对象。只有执行这段代码时才会发现问题,这意味着window标识符的真实值是无法预知的。
因此,只有在确实有必要时才推荐使用动态作用域。