JavaScript中閉包的概念和作用

1. 閉包的概念

要想理解什麼是閉包,首先要了解變量作用域和作用域鏈的概念。

1.1 變量作用域

一個變量的作用域(scope)是程序源代碼中定義這個變量的區域。在ES6之前,只有全局變量和局部變量,全局變量擁有全局作用域,在JavaScript代碼中的任何地方都是有定義。局部變量在函數內聲明,作用域是局部性的,只在函數體內有定義,函數參數也是局部變量。

在函數體內,局部變量的優先級高於同名的全局變量。如果在函數內聲明的一個局部變量(或者函數參數中帶有的變量)與全局變量同名,那麼全局變量就被局部變量所覆蓋。
看下面一段代碼:

var scope="global";//聲明一個全局變量
function checkscope(){
var scope="local";//聲明一個同名的局部變量
return scope;//返回局部變量的值,而不是全局變量的值
}
checkscope()//返回"local"

checkscope()會返回字符串"local"

1.2 變量作用域鏈

函數也可以看作是對象。如果將一個局部變量看做是自定義實現的對象屬性的話,那麼可以換個角度來解讀變量作用域。在JavaScript的最頂層代碼中(也就是不包含在任何函數定義內的代碼),作用域鏈由一個全局對象組成。在不包含嵌套的函數體內,作用域鏈上有兩個對象,第一個是函數(定義函數參數和局部變量的對象),第二個是全局對象。在一個嵌套的函數體內,作用域鏈上至少有三個對象。

每一段JavaScript代碼(全局代碼或函數)都有一個與之關聯的作用域鏈(scope chain)。這個作用域鏈是一個對象列表或者鏈表,這組對象定義了這段代碼“作用域中”的變量

var scope="global";//位於全局對象(一個對象)
function local(){
    var scope="local";//位於不含嵌套的函數體內(兩個對象)
}
function local(){
    function local_local(){
        var scope="local_local";//位於嵌套的函數體內(三個對象)
    }
}

當JavaScript需要查找變量x的值的時候(這個過程稱做“變量解析”(variable resolution)),它會遍歷作用域鏈中所有對象的所有屬性,如果某個對象有一個名爲x的屬性,則會直接使用這個屬性的值。如果作用域鏈上沒有任何一個對象含有屬性x,那麼就認爲這段代碼的作用域鏈上不存在x,並最終拋出一個引用錯誤(ReferenceError)異常。

當定義一個函數時,它實際上保存一個作用域鏈。當調用這個函數時,它創建一個新的對象來存儲它的局部變量,並將這個對象添加至保存的那個作用域鏈上,同時創建一個新的更長的表示函數調用作用域的“鏈”。對於嵌套函數來講,事情變得更加有趣,每次調用外部函數時,內部函數又會重新定義一遍。因爲每次調用外部函數的時候,作用域鏈都是不同的。內部函數在每次定義的時候都有微妙的差別——在每次調用外部函數時,內部函數的代碼都是相同的,而且關聯這段代碼的作用域鏈也不相同。

1.3 “閉包”

下面進入正題。

函數對象可以通過作用域鏈相互關聯起來,函數體內部的變量都可以保存在函數作用域內,這種特性在計算機科學文獻中稱爲“閉包”。

當調用函數時閉包所指向的作用域鏈和定義函數時的作用域鏈不是同一個作用域鏈時,事情就變得非常微妙。

JavaScript函數在定義的時候創建了作用域鏈,之後的調用不會使作用域鏈發生變化。這句話很重要,揭示了JS函數的一個重要特點:函數在定義它的作用域中執行,而不是在調用它的作用域中執行

請看如下代碼:

var scope="global scope";//全局變量
function checkscope(){
var scope="local scope";//局部變量
function f(){return scope;}//在作用域中返回這個值
return f;
}
checkscope()()//返回"local scope"

嵌套的函數f()定義在function checkscope()裏,其中的變量scope是局部變量,值爲"local scope",不管在何時何地執行函數f(),這種綁定在執行f()時依然有效。因此最後一行代碼返回"local scope"。

通過閉包可以捕捉到局部變量(和參數),並一直保存下來,看起來像這些變量綁定到了在其中定義它們的外部函數。下面一段是比較權威的解釋。

我們將作用域鏈描述爲一個對象列表,不是綁定的棧。每次調用JavaScript函數的時候,都會爲之創建一個新的對象用來保存局部變量,把這個對象添加至作用域鏈中。當函數返回的時候,就從作用域鏈中將這個綁定變量的對象刪除。如果不存在嵌套的函數,也沒有其他引用指向這個綁定對象,它就會被當做垃圾回收掉。如果定義了嵌套的函數,每個嵌套的函數都各自對應一個作用域鏈,並且這個作用域鏈指向一個變量綁定對象。但如果這些嵌套的函數對象在外部函數中保存下來,那麼它們也會和所指向的變量綁定對象一樣當做垃圾回收。但是如果這個函數定義了嵌套的函數,並將它作爲返回值返回或者存儲在某處的屬性裏,這時就會有一個外部引用指向這個嵌套的函數。它就不會被當做垃圾回收,並且它所指向的變量綁定對象也不會被當做垃圾回收。

下面看一個例子:

function counter(){
var n=0;
return{
count:function(){return n++;},
reset:function(){n=0;}
};
}
var c=counter(),d=counter();//創建兩個計數器
c.count()//=>0
d.count()//=>0:它們互不干擾
c.reset()//reset()和count()方法共享狀態
c.count()//=>0:因爲重置了c
d.count()//=>1:而沒有重置d

counter()函數返回了一個“計數器”對象,這個對象包含兩個方法:count()返回下一個整數,reset()將計數器重置爲內部狀態。首先要理解,這兩個方法都可以訪問私有變量n。再者,每次調用counter()都會創建一個新的作用域鏈和一個新的私有變量。因此,如果調用counter()兩次,則會得到兩個計數器對象,而且彼此包含不同的私有變量,調用其中一個計數器對象的count()或reset()不會影響到另外一個對象。

2. 閉包的作用

根據第一部分的分析,不難發現,通過閉包這一技術手段,可以在函數外部間接調用函數內部的局部變量,可以讓這些變量的值始終保持在內存中(因此要注意不能濫用閉包),還可以給變量開闢私密空間,避免外部污染

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章