首頁 >web前端 >js教程 >JavaScript中的閉包(Closure)詳細介紹_javascript技巧

JavaScript中的閉包(Closure)詳細介紹_javascript技巧

WBOY
WBOY原創
2016-05-16 16:23:32912瀏覽

閉包是JavaScript中重要的特性,最大的功能在於保存函數運作過程中的資訊。在JavaScript中,閉包的諸多特性源自於函數呼叫過程中的作用域鏈上。

 

函數呼叫物件與變數的作用域鏈

 

對於JavaScript中的每一次函數調用,JavaScript都會創建一個局部物件以儲存在該函數中定義的局部變數;如果在該函數內部還有一個嵌套定義的函數(nested function),那麼JavaScript會在已經定義的局部物件之上再定義一個嵌套局部物件。對於一個函數,其內部有多少層的巢狀函數定義,也就有多少層的巢狀局部物件。這個局部物件稱為“函數呼叫物件”(ECMAScript 3中的“call object”,ECMAScript 5中改名為“declarative environment record”,但個人認為還是ECMAScript 3中的名稱更容易理解一些)。以下面的函數呼叫為例:


複製程式碼 程式碼如下:

function f(x){
  var a = 10;
  return a*x;
}
console.log(f(6));//60

在這個簡單的例子中,當呼叫f()函數時,JavaScript會建立一個f()函數的呼叫物件(姑且稱為f_invokeObj),在f_invokeObj物件內部有兩個屬性:a和x;運行f()時,a值為10而x值為6,因此最後的回傳結果為60。圖示如下:

當存在函數巢狀時,JavaScript將建立多個函數呼叫物件:


複製程式碼 程式碼如下:

function f(x){
  var a = 10;
  return a*g(x);
  function g(b){
    return b*b;
  }
}
console.log(f(6));//360


在這個範例中,當呼叫f()函數時,JavaScript會建立一個f()函數的呼叫物件(f_invokeObj),其內部有兩個屬性a和x,a值為10而x值為6;執行f ()時,JavaScript會對f()函數中的g()函數進行解析定義,並建立g()的呼叫物件(g_invokeObj),其內部有一個屬性b,b值與傳入參數x相同為6 ,因此最後的回傳結果為360。圖示如下:

可以看到,函數呼叫物件形成了一條鏈。當內嵌函數g()運行,需要取得變數值的時候,會從最近的函數呼叫物件開始進行搜索,如果無法搜尋到,則沿函數呼叫物件鏈在更遠的呼叫物件中進行搜尋,此即所謂的「變數的作用域鏈」。如果兩個函數呼叫物件中出現相同的變量,則函數會取離自己最近的那個呼叫物件中的變數值:


複製程式碼 程式碼如下:

function f(x){
  var a = 10;
  return a*g(x);
  function g(b){
    var a = 1;
    return b*b*a;
  }
}
console.log(f(6));//360, not 3600


在上面的例子中,g()函數的呼叫物件(g_invokeObj)和f()函數的呼叫物件(f_invokeObj)中均存在變數a且a的值不同,當執行g()函數時,在g()函數內部所使用的a值為1,而在g()函數外部所使用的a值則為10。圖示此時的函數呼叫物件鏈如下:

什麼是閉包?

在JavaScript中所有的函數(function)都是對象,而定義函數時都會產生對應的函數呼叫對象鏈,一次函數定義對應一個函數呼叫對象鏈。只要函數物件存在,對應的函數呼叫物件就存在;一旦某函數不再被使用,對應的函數呼叫物件就會被垃圾回收掉;而這種函數物件和函數呼叫物件鏈之間的一一組合,就稱為「閉包」。在上面f()函數和g()函數的例子中,就存在兩個閉包:f()函數物件和f_invokeObj物件組成了一個閉包,而g()函數物件和g_invokeObj-f_invokeObj物件鏈一起組成了第二個閉包。當g()函數執行完畢後,由於g()函數不再被使用,因此g()閉包被垃圾回收了;之後,當f()函數執行完畢後,由於同樣的原因,f()閉包包也被垃圾回收了。

從閉包的定義可以得出結論:所有的JavaScript函數在定義後都是閉包 – 因為所有的函數都是對象,所有的函數在執行後也都有其對應的呼叫對象鏈。

不過,令閉包真正發揮作用的是巢狀函數的情況。由於內嵌函數是在外部函數運行的時候才開始定義的,因此內嵌函數的閉包中所保存的變數值(尤其是外部函數的局部變數值)是這次運行過程中的值。只要內嵌函數物件依然存在,那麼其閉包就依然存在(閉包中的變數值不會發生任何改變),從而也就實現了保存函數運行過程的資訊這個目的。考慮以下這個例子:


複製程式碼 程式碼如下:

var a = "outside";
function f(){
  var a = "inside";
  function g(){return a;}
  return g;
}
var result = f();
console.log(result());//inside


在這個例子中,當運行f()函數時,g()函數被定義,同時建立了g()函數的閉包,g()閉包包含了g_invokeObj-f_invokeObj物件鏈,因此保存了f()函數執行過程中的變數a的值。當執行console.log()語句時,由於g函數物件仍然存在,因此g()閉包也依然存在;當執行這個仍然存在的g函數物件時,JavaScript會使用依然存在的g()閉包並從中取得變數a的值(“inside”)。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn