执行环境、作用域链及闭包
执行环境(execution context)定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量、形参和函数声明都保存在这个对象中。编码时无法访问这个变量对象,解析器在处理数据时会在后台使用。
在Web浏览器中,全局执行环境被认为是window对象(全局执行环境所关联的对象就是window对象),因此所有的全局变量和函数都是作为window对象的属性和方法来创建的。某执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数也随之销毁(全局执行环境直到应用程序退出时才会被销毁)。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的执行环境就会被推入一个环境栈中。而在函数执行完毕后,栈将其环境弹出,把控制权返回给之前的执行环境(后进先出)。ECMAScript程序中的执行流正是由这个方便的机制控制着。
当代码在一个执行环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object:函数调用中的变量对象)作为变量对象。活动对象最开始时只包含一个变量,即arguments对象。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境:全局执行环境的变量对象始终都是作用域链中的最后一个对象。
标示符解析是沿着作用域链一级一级地搜索标示符的过程。搜索过程始终从作用链的前端开始,然后逐级地向后回溯,直至找到标示符为止(找不到标示符就会导致错误,通常是未定义的错误,找到了就不再往后继续找)。这些环境之间的联系是线性的、有序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。
在定义函数时,会创建一个从包含函数的活动变量对象到全局变量对象的作用域链,这个作用域链被保存在内部的 [[Scope]] 属性中,该属性不能被开发者访问。即JS函数的作用域链是在函数定义时就被确定好,而不是执行时才确定的。当调用函数时,会为函数创建一个执行环境,然后通过复制函数的 [[Scope]] 属性中的对象构建起执行环境的作用域链。最后,会有一个当前函数的活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的最前端。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
每个函数在定义时就有内部属性[[scope]],保存了从包含环境的变量对象,一层一层往外扩,直到全局环境的变量对象的作用域链。
每个函数执行时,引擎都会创建与之对应的执行环境压入EC栈中。同时执行环境通过复制[[Scope]]中的变量对象构建作用域链,初始化活动对象AO(包括arguments和其他命名的变量)并压入作用域链的最前端,指定this的值。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。如果内部函数被返回出外部函数而继续存在,则当外部函数被销毁时,其活动对象因为仍然被内部函数的内部属性[[Scope]]作用域链所引用而继续留在内存中。
function outer(){ var temp=5; return function (){ … } }
如果outer不调用,内部的匿名函数不算被定义,对应的[[Scope]]属性也不存在。如果outer被调用,但不返回匿名函数,[[Scope]]会出现,但随着匿名函数被回收也被销毁。只有匿名函数被返回后,[[Scope]]才会一直存在,对应的outer的活动变量也会因为被匿名函数的作用域链引用而一直存在。