標識符解析在閉包中理解


 閉包使用時一個常出現的錯誤,現分析一下,給例子:

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了下,歡迎拍磚~)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM