閉包使用時一個常出現的錯誤,現分析一下,給例子:
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
看了上面的分析,根據閉包的定義:
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了下,歡迎拍磚~)