# JavaScript语言编程核心(三)--- 执行上下文与作用域 ### 执行上下文(EC) **执行上下文**:可以抽象为一个简单的对象,在一个函数或者eval( )被执行时创建,对象里面包含着一些代码运行所需的属性,统称为 **上下文状态**。一个上下文状态由三个结构组成: * 变量对象(variable Object); * 作用域链(scope chain); * this指向(thisValue); ### 执行上下文堆栈 一般可以认为有三种执行上下文: * 全局执行上下文; * 函数执行上下文; * eval执行上下文; 一个执行上下文可以触发另一个上下文,以栈的形式实现,称为执行上**下文堆栈**。 触发其它上下文的叫做 `caller`,而被触发的上下文叫做 `callee`; >1.当代码执行的时候,首先生成一个全局执行上下文(`global EC`)。 >2.当执行到函数作用域时,全局执行上下文(`caller`)会触发该函数执行上下文(`callee`),将控制权传递给`callee`,而此时`caller`则暂缓执行。`callee`被push到栈的最顶层,拥有控制权,成为当前运行的执行上下文(`active EC`)。 >3.当`callee`执行结束后,控制权会返还给`caller`,`callee`从栈顶层被pop除,`caller`继续往下执行。 ### 变量对象(VO) **变量对象**是执行上下文中的一个数据作用域,存储着上下文中所定义的变量和函数声明(不包括函数表达式)。 > 全局上下文的变量对象就全局对象本身。 ### 活动对象(AO) 当函数被`caller`触发时,该函数的变量对象(VO)则会被激活成为活动对象(AO),里面加入了形参和arguments对象。 ### 作用域 **作用域**:通俗点可以理解为是一段代码或者一个变量的作用范围,是程序源代码中定义这个变量的区域,可以简单的理解为一个对象,其实,变量对象便是作用域的实体。比如,一个全局变量,就拥有全局的作用域,在JavaScript代码的所有地方都有定义,都可以进行访问,理解为一个全局对象中定义着这个全局属性。 在许多编程语言中,每个花括号中的代码都具有各自的作用域,而在花括号的外部,是不可见的,这称为 **块级作用域(block scope)**。但在JavaScript中,虽然语法上也是使用花括号,但并不存在块级作用域,在花括号内定义的变量,在外部都是可见的。取而代之的是**函数作用域(function scope)**:在一个函数中声明的变量,在该函数,包括其内部嵌套函数中,都是有定义,可见的。但在函数的外部是不可见的。 > 因此,这里就引出了另一个概念-- **声明提前**。 > 根据函数作用域的定义,一个声明在函数体内的任何地方都是始终可见的,甚至在声明之前。因此,就算在声明一个变量之前,也是可以访问到这个变量的(但不涉及到赋值)。 var scope = "global"; (function(){ // 并不是输出global,这是因为声明提前,等于在该语句前加了一句,var scope。但并不涉及赋值; console.log(scope); // undefined; var scope = "local"; console.log(scope); // local; })(); ### 作用域链 在定义一个函数作用域时,会随之产生一个与之关联的**作用域链(scope chain)**。这个作用域链是一个对象列表,列表内包含所有父级变量对象和自身的活动对象,另外,通过`with语句`和`catch语句`也可动态的延长作用域链。 > 作用域链其实是由两部分组成的:函数内部属性 `[[scope]]` + 自身活动对象(`AO`); > 在`[[scope]]`属性中,保存着父级函数的作用域链。 当在函数内访问一个变量时,首先会从该函数自身的活动对象中查找,如果不存在,则会沿着作用域链往上一级一级查找父级变量对象。 ### 闭包 由于在JavaScript中,函数属于第一级对象,即可以当成参数传入一个函数,也可以从函数中返回出一个函数。此时,便会出现一个问题,当一个父函数执行后返回另一个子函数后,便会被摧毁,而此时,返回的子元素的[[scope]]属性中仍然保存着父函数的作用域链,以此来解决访问父函数中变量的问题。 这种类型的作用域,成为静态(词法)作用域。 var afun = (function(){ var a = 1; return function afun(){ alert(a); } })(); var a = 2; afun(); // 1 静态作用域是闭包存在的一个必需条件,因此 >**闭包**可以定义为: >一个函数,和以静态方式/词法方式进行存储的所有父作用域的一个集合体。所以,通过这些存储的作用域,函数可以很容易的找到自由变量。 >Tips:从理论上讲,每一个函数都可以称为闭包,因为都包含了`[[scopte]]`属性。 当在一个父函数中定义了两个子函数时,此时两个子函数拥有相同的[[scope]]属性,而且是共享的。也就是说,改变一个闭包中的变量,会影响到另一个闭包中的变量。这也就是我们最常见的闭包问题的根源所在: var a = []; for(var i = 0;i<10;i++){ a[i] = function(){ console.log(i); } } // 而不是预期中的 0~9,其原因就是这9个函数共享一样的[[scope]]属性; a[0](); // 10; a[1](); // 10; a[9](); // 10; 可以通过将参数`i`以参数的形式传入,让其不需要从`[[scope]]`属性中进行查找。 var a = []; for(var i = 0;i<10;i++){ a[i] = (function(x){ return function(){ console.log(x); } })(i); } a[0](); // 0; a[1](); // 1; a[9](); // 9;