閉包和做用域鏈是JavaScript中比較重要的概念,首先,看看幾段簡單的代碼。javascript
代碼1:html
1 var name = "stephenchan"; 2 var age = 23; 3 function myFunc() { 4 alert(name); 5 var name = "endlesscode"; 6 alert(name); 7 alert(age); 8 alert(weight); 9 } 10 myFunc(); 11 myFunc();
上述代碼1中,兩次調用myFunc()的輸出是一致的。可能你會認爲輸出是:java
可是結果倒是:編程
代碼2:數據結構
1 var i = 10; 2 function myFunc() { 3 var i = 20; 4 function innerFunc() { 5 alert(i); 6 } 7 return innerFunc; 8 } 9 var func = myFunc(); 10 func();
上面的代碼2會輸出20,但爲何不輸出10或者是輸出undefined?閉包
代碼3:less
1 var name = "stephenchan"; 2 function callMePlz() { 3 alert(name); 4 } 5 6 function myFunc() { 7 var name = "endlesscode"; 8 callMePlz(); 9 } 10 11 myFunc();
上面的代碼3輸出的會是endlesscode、stephenchan仍是undefined?函數
代碼4:this
1 function callMePlz() { 2 var name = "stephenchan"; 3 var intro = function() { 4 alert("I am " + name); 5 } 6 return intro; 7 } 8 9 function showMe(arg) { 10 var name = arg; 11 var func = callMePlz(); 12 func(); 13 } 14 showMe("endlesscode");
上面的代碼4與代碼3不一樣的是,從callMePlz返回的函數引用,而後再執行函數。spa
代碼5:
1 var name = "stephenchan"; 2 function callMePlz() { 3 var intro = function() { 4 alert("I am " + name); 5 } 6 return intro; 7 } 8 9 function showMe(arg) { 10 var name = arg; 11 var func = callMePlz(); 12 func(); 13 } 14 showMe("endlesscode");
上面的代碼5與代碼4不一樣的是原來在callMePlz函數中的變量name在全局環境中聲明瞭,但輸出的結果是:
先不對上面的代碼進行說明,講述一下閉包和做用域鏈的概念。
閉包(closure)是什麼?閉包與函數有着緊密的關係。「在JavaScript中,一個函數只是一段靜態的代碼、腳本文件,所以函數是一個代碼書寫時,以及編譯期的、靜態的概念;而閉包則是函數的代碼在運行過程當中的一個動態環境,是一個運行期的、動態的概念」。這是《JavaScript語言精髓和編程實踐》中對函數和閉包的描述,實際上咱們常說的閉包卻是能夠表現爲如上面代碼2中的innerFunc同樣,在myFunc的函數執行後返回的是一個在其內部定義的、外部可調用的函數引用,這個函數語言的特性在C和C++是沒有的。爲何在myFunc結束以後innerFunc還能正常訪問到myFunc裏面的數據呢?這就涉及到函數執行環境與閉包的相關概念,閉包中所保留着函數運行的實例,環境以及做用域鏈等等,並在myFunc調用以後沒有將函數實例直接丟棄,所以在調用innerFunc的時候可以引用到myFunc中聲明的i。
做用域鏈(scope chain)是什麼?顧名思義,就是由做用域組成的鏈,是一個相似鏈狀的數據結構。做用域就是對上下文環境的數據描述。閉包和做用域鏈是緊密關係的,函數實例執行時的閉包是構成做用域鏈的基本元素。JavaScript代碼在執行前會進行語法分析,在語法分析階段,會記錄全局環境中的變量聲明和函數定義,構造函數的調用對象(Call Oject、Activation Object、Activate Object、活動對象,不一樣稱呼罷了)和在全局環境下的做用域鏈。
圖1是《JavaScript語言精髓和編程實踐》一書中對閉包相關元素的內部數據結構的描述。咱們主要關注其中的ScriptObject,ScriptObject是對調用對象的一種描述。ScriptObject在語法分析階段就已經構造好了,其中的varDecls是保存着函數中的變量聲明,funcDecls保存着內部的函數聲明。
在語法分析階段,varDecls保存在函數中用var進行顯示聲明的局部變量,而且置默認值爲undefined,這裏就是在代碼1中"alert(name)"輸出爲undefined的緣由,因爲myFunc在語法分析階段就已經保留了標記符name在varDecls,在賦值語句"var name='endlesscode'"執行以前name的值都是undefined,所以在"alert(name)"的時候就顯示爲undefined了。
而函數定義在語法分析階段工做就稍微有點不一樣。在語法分析階段,發現有函數定義的時候,除了記錄函數的聲明外,還會建立一個函數對象,並將當前的做用域鏈賦值給此函數對象的[[scope]]屬性(這個屬性是JavaScript引擎內部維護的,可是Firefox倒是能夠經過私有屬性__parent__來訪問它),這裏要注意的是在語法分析階段將做用域鏈賦值給[[scope]]屬性,而不是在執行階段。若是是在全局環境下,但當前的做用域鏈爲只有一個元素,就是全局的調用對象(Windows Object, Global Object,不一樣稱呼罷了)。這就是爲何在《JavaScript權威指南》中提到「JavaScript中的函數運行在它們被定義的做用域裏,而不是它們被執行的做用域裏。」
下面以一段代碼的大概處理流程來進行說明:
1 var outerVar1 = "var in global code"; 2 function outerFunc(arg1, arg2) { 3 var innerVar1 = "var in function code"; 4 function innerFunc() { return outerVar1 + "-" + innerVar1 + "-" + (arg1 + arg2); } 5 return innerFunc(); 6 } 7 var outerVar2 = outerFunc(10, 20);
執行處理過程大體以下:(精華1)
咱們再拿前面代碼4的例子對做用域鏈進行簡單的分析:(精華2)
代碼4:
1 function callMePlz() { 2 var name = "stephenchan"; 3 var intro = function() { 4 alert("I am " + name); 5 } 6 return intro; 7 } 8 9 function showMe(arg) { 10 var name = arg; 11 var func = callMePlz(); 12 func(); 13 } 14 showMe("endlesscode");
假如全局的語法分析已經結束,已經開始執行"showMe('endlesscode')"了。在進入執行showMe的執行上下文時,咱們能夠看到"showMe"函數中[[scope]]屬性記錄中的做用域鏈爲:
建立showMe的調用對象後,則新的做用域鏈爲showMe的調用對象和全局調用對象組成:
也就是"showMe調用對象->Global調用對象"這樣的鏈式關係。接着,忽略showMe函數中語法分析等過程,到執行callMePlz()函數時,callMePlz函數的[[scope]]屬性爲
callMePlz函數的[[scope]]屬性指示的做用域鏈也只包括了全局調用對象,由於callMePlz也是在全局環境下定義的。建立callMePlz的調用對象後,則新的做用域鏈爲callMePlz的調用對象和全局調用對象組成:
也就是"callMePlz調用對象->Global調用對象"這樣的鏈式關係。能夠看到,在callMePlz的做用域鏈中,並無包括showMe的調用對象。當callMePlz進行語法分析的時候,找到intro函數時,將intro函數的[[scope]]屬性賦值爲(上例分析中第2條中的第2條):
callMePlz返回的是intro函數對象的引用,當在showMe函數中執行intro函數時,建立intro函數的調用對象,此時intro函數的做用域鏈爲:
因爲在intro函數中沒有聲明變量和函數,因此看到的也只是一些內置的屬性成員,此時intro函數的做用域鏈則爲:"intro調用對象->callMePlz調用對象->Global調用對象",所以,當執行intro函數時,則以"intro調用對象->callMePlz調用對象->Global調用對象"的順序去搜索"name"變量,發如今callMePlz調用對象上找到了,所以在代碼4中輸出的是"I am stephenchan"而不是"I am endlesscode"。
以上面的分析方法來分析上述的其餘代碼,就容易理解其輸出了。
另外,函數閉包內的標識符系統有優先順序,其優先級從高到低爲:this > 局部變量(varDecls) > 函數形式參數名(argsName) > arguments關鍵字 > 函數名(funcNames)。
1 //輸出'hi',說明argsName > funcNames。 2 function foo(foo) { 3 alert(foo); 4 } 5 foo('hi'); 6 7 //輸出100的類型"number",說明argsName > arguments。 8 function foo2(arguments) { 9 alert(typeof arguments); 10 } 11 foo2(100); 12 13 //輸出arguments的類型爲'object‘,說明arguments > funcNames。 14 function arguments() { 15 alert(typeof arguments); 16 } 17 arguments(); 18 19 //輸出'test',形式參數名與未賦值局部變量重複時,取形式參數值。 20 function foo3(str) { 21 var str; 22 alert(str); 23 } 24 foo3('test'); 25 26 //輸出'member',形式參數與有值的局部變量重複時,取局部變量。 27 function foo4(str) { 28 var str = 'member'; 29 alert(str); 30 } 31 foo4('test');
原文連接:http://blog.endlesscode.com/2010/01/20/javascript-closure-scope-chain/
推薦閱讀:http://www.cnblogs.com/lhb25/archive/2011/09/06/javascript-scope-chain.html