标识符解析在闭包中理解

 闭包使用时一个常出现的错误,现分析一下,给例子:

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 bar = ‘Jberry’;             // '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

看了上面的分析,根据闭包的定义:

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了下,欢迎拍砖~)

posted @ 2013-04-17 14:23  蓝莓调调  阅读(1457)  评论(13编辑  收藏  举报