前面我说到过,执行环境是js中最为重要的一个概念。执行环境定义了变量和函数有权访问的其余数据,决定了它们各自的行为。(接下来的概述主要来自《高性能JavaScript》一书,以及本人的一些简单的理解。)javascript
1、函数做用域html
在JavaScript中,每个函数都表示为一个对象,更确切地说,是Function对象的一个实例。Function对象与其余的对象同样,都拥有能够编程访问的属性,和一系列不能经过代码访问而仅提供了JavaScript引擎存取的内部属性。譬如[[Call]]属性,表示这个对象能够被执行,其中有一个内部属性是[[Scope]],由ECMA-262标准第三版定义。前端
内部属性[[Scope]]包含了一个函数被建立的做用域中的对象的集合。这个集合被称为函数的做用域链,它决定了哪些数据能被函数访问。函数做用域中的每一个对象都被称为可变对象,每一个可变对象都以“键值对”的形式存在。当一个函数建立后,它的做用域链会被建立此函数的做用域中可访问的数据对象所填充。java
上面这句画的意思就是函数被建立的时候会有一个咱们没法访问的[[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。而后,这个激活对象被推入做用域链的前端。看成用域链被销毁时,激活对象也一同被销毁。以下图所示。this
上图显示了函数在被调用时做用域的变化,关于变量声明和函数声明初始化以及函数参数初始化的值以及冲突的解决方案。能够看我以前的博客。 http://www.cnblogs.com/miracle-t/p/5484420.htmlspa
在函数执行过程当中,没遇到一个变量,都会经历一次标识符解析的过程,以肯定从哪里获取或存储数据。这个过程会搜索执行环境的做用域链,查找同名的标识符(变量名)。搜索的过程从做用域的头部开始,也就是当前运行函数的活动对象。若是找到了,就是用这个标识符对应的变量;若是没有找到,就继续搜索做用域链中的下一个对象。若是整个做用域链中全部对象都没有该标识符,那么表示该标识符是未定义的。code
2、改变做用域
通常来讲,一个执行环境的做用域链是不会发生改变的。可是,有两个语句能够在执行时临时改变做用域链。
第一个语句是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子句执行完毕,做用域链就会返回以前的状态。
3、动态做用域
不管是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标识符的真实值是没法预知的。
所以,只有在确实有必要时才推荐使用动态做用域。