網上關於閉包的文章一搜一大堆,但是我還是要來說一下我的理解。 我理解的閉包,其實就是 訪問了外部變數的函數 : 可能和同學們平常看到的理解不太一樣,但 "維基百科" )的確是這樣描述的: a closure is a record storing a function together with a ...
網上關於閉包的文章一搜一大堆,但是我還是要來說一下我的理解。
我理解的閉包,其實就是訪問了外部變數的函數:
let a = 0
function b() {
console.log(a)
}
可能和同學們平常看到的理解不太一樣,但維基百科的確是這樣描述的:
a closure is a record storing a function together with an environment: a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope)
閉包就是一個引用了外部變數的函數以及其運行環境的統稱
平常我們提到的“閉包”,都是通過返回一個函數來讓外部能訪問函數內部的變數,但我認為這隻是閉包的一種應用罷了。前面的例子是閉包,但是它已經是全局環境了,不能再被指向給上一層環境,因此函數執行完成後該閉包便被銷毀了。
下麵讓我們以一個簡單的計數器函數為例,更加深入的去理解閉包:
/*不使用閉包*/
function add () {
let count = 0;
return counter += 1;
}
add() //1
add() //1
add() //1
/*使用閉包*/
let add = function plus () {
let count = 0
return function closure () {
return count += 1
}
}()
add() // 1
add() // 2
add() // 3
不使用閉包的情況下,每次執行add()函數時,局部變數count值都會被初始化為0,並不能起到計數器的作用。
使用閉包的情況,本身是兩個匿名函數,為了方便描述我給它們分別命名為plus()和closure()。plus內定義了一個變數count,然後返回了closure函數,這個函數引用了count變數。函數最後一個()讓其執行後,將其結果賦值給了一個全局變數add。
下麵我們看下調用add函數會發生什麼:調用add函數即調用closure,首先會執行count+=1,而closure函數內部是沒有定義‘count’這個變數的,於是它會循著作用域鏈往上查找,到plus 函數找到了count變數,然後取得count的值,計算並返回。再次執行,可以看到count值是繼續遞增的,說明count被保存在了記憶體中,但卻不能直接訪問。
讓我們看一下閉包是怎樣工作的:closure函數引用了其外部變數count,此時closure函數(以及其運行環境和外部變數)形成一個閉包,並將整個閉包返回,賦值給了一個全局變數。於是整個閉包隨著add留在記憶體中,直到add與該閉包的連接被清除(將add指向別處或者 add = null),該閉包占用的記憶體才會被回收。
閉包的應用1
閉包最常見的應用,是各種js庫用來封裝源碼。以jQuery為例,源碼核心結構如下:
(function (global, factory) {
...
})(window, function (window) {
var arr = []
var document = window.document
...
var jQuery = function (selector, context) {
...
}
...
window.jQuery = window.$ = jQuery
})
我們來解讀一下這裡的閉包:首先,匿名函數內定義了各種變數,然後jQuery對象(同時也是一個函數)及其屬性和方法引用到了這些變數。最後用window.$ = jQuery將jQuery對象和全局對象連接起來---因此這個閉包只會在其全局對象被銷毀(頁面或iframe被關閉),或者連接被切斷(window.$ = null)時才會銷毀。這就是閉包最常見的應用---利用匿名自執行函數的作用域,將內部變數封裝起來,防止被外部修改,也避免了污染全局變數環境。
看到這裡也明瞭,並不是“return一個函數才叫閉包”,前面計數器的例子也可以改成這樣:
(function closure(global) {
let count = 0
function plus() {
return count += 1
}
global.add = plus
})(window)
閉包的應用2
平常我們可能需要在window.onresize中改變頁面樣式,用戶輸入字元時ajax遠程搜索等。由於這類事件會在短時間內多次觸發,不加以控制則會頻繁調用處理程式,影響性能。我們可以利用閉包,來實現函數節流(throttle)和函數去抖(debounce),提高頁面性能。
函數節流:預先設定一個執行周期,當調用方法的間隔大於執行周期則執行該方法,然後記錄當前執行的時間併進入下一個新周期。
const throttle = function (fn, ms) {
let timestamp = 0
return function () {
let current = Date.now()
if (current - timestamp > ms) {
fn.apply(this, arguments)
timestamp = current
}
}
}
setInterval(throttle(function (arg) {
console.log(arg)
}, 2000).bind(this, 'hello'), 50)
利用閉包將timestamp 的值保存起來,以記錄函數上次的調用時間。如果小於時間間隔則不處理,如果大於間隔則執行函數並記錄這一次執行的時間。我們用setInterval模擬一下連續觸發的情況,可以看到雖然setInterval的間隔設置為50,但是函數的執行間隔仍然是由throttle設定的間隔控制的---2s觸發一次。這個方法也適用於防止用戶連續點擊按鈕發起重覆請求的情況。
函數防抖:有一個形象的比喻是,如果用手指一直按住一個彈簧,它將不會彈起,直到你鬆手為止。也就是說當調用函數n毫秒後,才會執行該函數,若在這n毫秒內又調用此函數,則將重新計算時間。
var debounce = function(fn, ms){
var timeoutID
return function(){
clearTimeout(timeoutID)
timeoutID = setTimeout(() => {
fn.apply(this, arguments)
}, ms)
}
}
setInterval(debounce(function (arg) {
console.log(arg)
},300).bind(this,'world'),50)
這次我們用閉包保存了setTimeout返回的ID。每當函數執行的時候,就重新設置timeout,因此50ms間隔的interval遇上300ms間隔的debounce,函數將永不會執行,直到取消interval。這種方法適用於頁面resize,文字輸入遠程搜索等情況,但是由於是延遲觸發,不適合用於按鈕點擊等交互體驗明顯的地方。
總結
作用域的限制,讓外部環境不能訪問內部變數,而內部環境可以訪問其作用域鏈上的外部變數;當一個函數訪問了其外部變數,這個函數就同這些被訪問的變數形成了閉包;如果將這個閉包同外層環境連接起來,這個閉包將會一直存在記憶體中,直到外層環境銷毀;而外層環境可以利用閉包函數,訪問閉包中保存的變數。
由於閉包會讓函數及變數一直留在記憶體中,不能被GC機制回收,因此當不再使用該閉包時,應當及時將該閉包與當前環境的連接清除,以便記憶體被系統回收。