标识符解析在闭包中理解
闭包使用时一个常出现的错误,现分析一下,给例子:
function foo(){
var i;
for(i = 0; i < 10; i++){
setTimeout(function(){
console.log(i);
},1000);
}
}
foo(); //10,10,10,10,10,10,10,10,10,10
这是秘密花园给的例子,在setTimeout方法里创建了一个闭包,调用了外层函数的 i 属性。连续10次调用setTimeout方法,在1秒后连续输出了10个数字。这里调用setTimeout方法主要是用来引入闭包的。
那么例子中,setTimeout里使用的 i 为什么不是循环中实时的 i 呢?
这里涉及到JS中函数调用时的标识符查找过程,例子中 i 就是匿名函数所要查找的标识符。
首先什么是标识符(Identify)?
var // 'name' is a identify
function foo(para){...} // 'foo' is a identify
// 'para' are identifies
变量的声明符号名 ‘bar’ 、函数声明的函数名 ‘foo’、函数的形参 ‘para'三者是标识符。
在《高性能Javascript》一书里我们知道,标识符的查找是一个延着活动链域(scope chain)从本地环境到全局环境的搜索过程。ECMAScript里写道:
The result of evaluating an identifier is always a value of type Reference with its referenced name component
equal to the Identifier String.
因此,当在查找到所需的标识符时,会返回最近活动对象里、以目标标识符为名称的引用类型对象,然后调用getValue(identify)方法来获取标识符的值。
如果没有找到标识符,那么返回ReferenceError,JS中显示该标识符值为 ’undefined‘。
那么为什么找到的是引用的对象而不是值的副本?
大家都知道,变量的范围与其所在的环境,也就是域 scope相关。
在C中有块级域(block-level scope,如 if、while块)、函数域(function-level scope)的概念,通过设置块(block)和定义函数可以决定同名变量的归属。
而在JS中没有块的概念,只有函数域(function-level scope)的概念,通过函数的定义来决定变量的归属。在JS中函数是一等(first-class)的,可以像普通数据一样,按字面上创建,像参数一样传递,或从其他函数中作为值返回,而在C中不行。
同时,函数的执行也与环境相关。
在C函数的调用中,通过调用栈(call-stack)的形式执行函数中的代码。当运行函数时,将函数的环境和代码段压入栈中,根据栈中的环境执行代码段,等函数执行完毕后,参数从栈中弹出。这里的环境,就是C函数所需的参数副本。
而在JS的函数的调用中,函数同样也有两个部分——环境和代码段。而这里的环境有两部分,一部分是函数在创建时的静态的词法环境,也就是scope chain。该环境里是一系列的变量对象,里面保存着外部环境的标识符和值。还有一部分是函数在执行的时候动态创建、并加在scope chain最前面的活动对象,里面包括函数执行期的参数、内部变量以及实时绑定的 'this'。两个环境加起来就是函数执行时的完整的scope chain。
可以看到,JS里的函数是在scope chain里查找标识符,实际上是一个在各个变量对象、活动对象里查找的过程。而对象是放在堆里,而不是栈里。因而与C中调用栈(call-stack) 的概念不同,这里更像是调用堆(call-heap)的概念。每次标识符的查找就是从堆中找对象的过程。堆中存的是表示环境的对象,只有用引用,而不是压栈的方式获取它;也只有用JS的回收机制,而不是出栈的方式清除它。
这可能从另一方面解释了JS中Everything is Object的概念吧。
回到例子中,当1秒钟后去执行setTimeout方法的匿名函数时,上层 foo 函数中的 for 循环已经结束,i 值此时为10。
而匿名函数在调用时,是去查找保存有 i 的变量对象,这个对象表示 foo 函数此时的运行环境。由于此时 foo 函数已运行结束,i 值已经变成10了。
因此,返回 i 标识符的引用对象里的值是10,而不是foo循环里 i 的副本了。
要解决的方法很简单,就是让匿名函数在外层函数里实时的运行,而不是等到外层函数结束后,才在变量对象里去查找需要标识符。
function foo(){
var i;
for(i = 0; i < 10; i++){
setTimeout((function(e){
return function(){
console.log(e);
}
})(i),1000);
}
}
/***********or************/
function foo(){
var i;
for(i = 0; i < 10; i++){
(function(e){
setTimeout(function(){
console.log(e);
},1000);
})(i);
}
}
foo(); //0,1,2,3,4,5,6,7,8,9
看了上面的分析,根据闭包的定义:
A closure is a pair consisting of the function code and the environment in which the function is created.
闭包是由函数体和函数创建时的环境组成。
相信也能对 “All functions in ECMAScript are first-class and closures“ 这句话有所理解了吧。
打完手工!
(对C理解的不深,有些地方YY了下,欢迎拍砖~)