# 定義
? 閉包 是指有權訪問另一個函數作用域中的變量的函數。注意別混淆匿名函數和閉包的概念。
? 創建閉包 需要達到兩個條件,如果不滿足第二條,也只能稱作是匿名函數
(1)在一個函數內部創建另一個函數
(2)內部函數訪問外部函數的變量
function createCompareFn(attr) {
return function(obj1, obj2) {
return obj1[attr] >= obj2[attr]
}
}
? 上例中,內部函數(一個匿名函數) 訪問了包含函數(即外部函數)的變量attr
,即使這個內部函數被返回且在其他地方被調用了,它仍然可以訪問變量attr
。之所以能夠訪問,是因為內部函數的作用域鏈中含有包含函數的作用域。
# 作用域鏈
? 當某個函數被調用時,會創建一個執行環境(execution context)及相應的作用域鏈。然后,使用arguments
和其他命名參數的值來初始化函數的活動對象(activation object)。但在作用域鏈中,外部函數的活動對象始終處于第二位,包含函數的活動對象處于第二位,包含函數的包含函數的活動對象處于第三位……,直到找到全局環境為止。這些活動對象使用鏈表來連接,形成作用域鏈。
? 在函數執行過程中,為了讀取和寫入變量的值,需要在作用域鏈中查找變量??匆粋€簡單的函數聲明及調用來解釋:
function compare(val1, val2) {
return val1 >= val2
}
var result = compare(5, 10)
? 上述代碼定義了compare
函數,然后又在全局作用域中調用。當調用compare()
時,會創建一個包含argument, val1, val2
的活動對象。而全局環境的變量對象處在作用域鏈的第二位
? 后臺的每個執行環境都有一個標識變量的對象——變量對象。全局環境的變量對象始終存在,而像
compare()
這樣的局部環境的變量對象,只有在函數執行過程中菜戶存在。在創建compare()
函數時,會創建一個包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]
屬性中,當執行compare()
函數時,回味函數創建一個執行環境,然后通過復制函數的[[Scope]]
屬性中的對象構建起執行的作用域鏈,以此類推形成作用域鏈。如圖所示,作用域鏈本身只是一個指向變量對象的指針列表,它只引用但不實際包含對象。? 無論什么時候在函數中訪問一個白能量時,就會從作用域鏈中搜索具有相應名字的變量。通常當函數執行完成后,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象)
# 閉包的作用域鏈
? 由于閉包存在函數內部定義函數,內部定義的函數將包含函數的活動對象到它的作用域鏈中。假設有閉包如下:
function createCompareFn(attr) {
return function(obj1, obj2) {
return obj1[attr] >= obj2[attr]
}
}
var compare = createCompareGn('name')
var result = compare({ name: 'Nic' }, { name: 'Goe' })
? 則它的作用域鏈關系為
由于閉包會攜帶它包含函數的作用域鏈,因此會比其他函數占用更多內存,過度使用閉包容易導致占用內存過多,需謹慎。
?
# 閉包與變量
? 由于作用域鏈本身只是一個指向變量對象的指針列表,它只引用并不真正存儲它們。而這種配置機制引出了一個副作用,即閉包只能取到包含函數中任何變量的最終值。
? 因為閉包作用域鏈與包含函數的活動對象之間只是引用關系,當包含函數中由于某些運算導致它活動對象中的屬性發生更新時,該更新會被帶到閉包作用域中,當閉包再訪問變量時,取到的就是被更新后的變量的值。經典例子如下:
function createFunctions() {
var result = new Array()
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i
}
}
return result // 10 個 10
}
? 該例中,在閉包中返回包含函數的變量i
并壓入給結果數組。表象上看應該得到1~10
的數組。打印發現是10個10。原因是:每個函數的作用域鏈中,都保存著createFunctions()
函數的活動對象,引用的都是同一個變量i
;當createFunctions()
函數返回后,變量i
的值是10,此時每個函數都引用者保存變量i
的同一個變量對象。所以在每個函數內部i
的值都是10。作用域引用關系如下:
? 我們可以通過創建另一個匿名函數強制讓閉包的行為符合預期
function createFunction() {
var result = new Array()
for (var i = 0; i < 10; i++) {
result[i] = function(num) {
return function() {
return num
}
}(i)
}
return result // 1~10
}
? 經過如上改造,我們沒有閉包直接賦值給數組,取而代之的是定義了一個匿名函數,并將立即執行該函數的結果賦給數組。匿名函數有個參數num
,存在于匿名函數的作用域鏈中。程序執行for循環調用每個匿名函數時,由于函數參數是按值傳遞的,在其內部我們創建了一個直接訪問num的閉包。這樣一來,result數組中存儲的值就是每次執行的num的一個副本了。
# 閉包的this變量
? 我們知道,this
對象是在運行時基于函數的執行環境綁定的。在全局函數中,this
等于window
。而當函數被某個對象的方法調用時,this
等于那個對象。
? 由于匿名函數的執行環境具有全局性,因此其this
對象通常指向window(除使用call()
或apply()
來改變函數執行環境外)??匆韵吕樱?/p>
var name = 'the window'
var object = {
name: 'my object',
getNameFn: function() {
return function() {
return this.name
}
}
}
console.log(object.getNameFn()()) // the window
? 本例中,getNameFn是對象中的一個方法屬性。object.getNameFn()
返回一個函數,object.getNameFn()()
立即執行該函數得到一個字符串。
? 我們知道,每個函數在被調用時都會自動取得兩個特殊變量:this
和arguments
。內部函數在搜索這兩個變量時,只會搜索到它自己的活動對象為止(它們自身能獲取到不必去包含函數活動對象中獲?。?/strong>。在本段代碼執行時,程序發現需要返回邏輯想要返回的this.name
,于是搜索匿名函數自身的作用域,取到自己的this
對象,該對象因匿名函數執行環境全局性的特征指向了window
,從而輸出了"the window"
。
? 注意本例的稱呼是匿名函數而不是閉包!原因就是匿名函數內部有自身的
this
變量,它無需也無法獲取到外部object
的this
,沒有達到內部函數訪問外部函數的這么一個行為,因此不稱呼為閉包。以下改造后就符合了閉包的特征
? 如果我們想獲取到object中的name
,只需如下簡單改造即可
var name = 'the window'
var object = {
name: 'my object',
getNameFn: function() {
var that = this
return function() {
return that.name
}
}
}
console.log(object.getNameFn()()) // myobject
? 經過如上改造后,但執行object.getNameFn()()
調用內部閉包函數時,需要搜索that
,而在自身作用域內并沒有找到that
,于是順著作用域鏈查找包含函數的作用域,得到結果。
# 閉包與內存泄漏
? 由于閉包作用域鏈包含著包含函數的作用域,因此會比普通函數占用更多的內存,當使用閉包不當,且未得到合適的釋放情況下,就容易造成大量內存空間的占用??匆粋€例子
function assignHandler() {
var element = document.getElementById('someElement')
element.onclick = function() {
alert(element.id)
}
}
? 以上代碼實現了對某個元素進行點擊時的點擊響應事件。onclick
是一個閉包,在這個閉包內循環引用了element.id
。因此,只要匿名函數存在,element
的引用數至少是1
。那么,在垃圾回收機制規則中就無法判定element
是一個需要被回收的元素。導致其一直占用在內存空間中。解決辦法如下
function assignHandler() {
var element = document.getElementById('someElement')
var id = element.id
element.onclick = function() {
alert(id)
}
element = null
}
? 如此改造有兩點:(1)對element.id
保存副本目的是在閉包中取消對元素變量的循環引用。(2)由于閉包會引用包含函數的整個活動對象,其中還包含著element
,因此包含函數的活動對象中也會保存有一個引用。因此有必要把element
變量設置為null
# 閉包與應用場景
(1)模仿塊級作用域
? 在閉包中創建的變量,不受外部變量的影響。看以下函數,在for
循環這個塊級空間中定義了變量,但在外部依然能訪問alert(i)
依然能訪問到該變量。
function Counter(count) {
for (var i = 0; i < count; i ++) {
console.log(i)
}
var i // 重新聲明變量
alert(i) // 計數
}
? 如何讓該變量私有化?當然可以使用ES6的let
定義。本例筆者不想說這個,我們來看以下這個更熟悉的函數格式
(function() {
// 塊級作用域
})()
? 我們都稱它為自執行函數,初學者可能覺得這個格式很難理解,我們做如下拆解來幫助理解它。
一個正常函數表達式定義及調用如下
var func = function() {
// 塊級作用域
}
func()
? 普通的函數調用使用函數名加圓括號來執行一個函數,如果改成函數對象,即如下
function() {
// 塊級作用域
}() // 報錯。。
? 執行報錯了。因為JS將 function
關鍵字當做一個函數聲明的開始,而函數聲明后面是不允許跟圓括號,而函數表達式后面可以跟圓括號,因此需要把匿名函數加一個圓括號來告訴JS這是一個函數表達式,從而形成了自執行函數的表現形式,也實現了塊級作用域的目的。
? 利用自執行函數的特征,我們可以改寫outputNumber
函數如下
function outputNumber() {
(function() {
for(var i = 0; i < number; i++) {
console.log(i)
}
})()
console.log(i) // 報錯
}
? 以上代碼中,匿名函數是一個閉包,他能夠訪問包含函數作用域鏈中的所有變量,而外部無法訪問比包內的變量。
(2)創建私有變量
? 創建一個可以訪問私有變量和私有函數的共有方法。該方法也稱作特權方法。
? 創建私有變量需要從一個計數器來說起:
function Counter() {
var count = 0
this.clear = function () {
this.count = 0
}
this.add = function() {
this.count++
}
this.decrease = function () {
this.count--
}
}
? 這個構造函數中,有一個私有變量count
,它只能在函數內部被訪問,函數中有三個特權方法用于清空,累加和累減計數。除了它們外,滅有別的方法可以訪問到count
變量。而特權方法可以被實例化的實例訪問。count
就是一個私有變量
【缺點】每次實例化的時候,都需要重新生成一次特權方法
以上方法為構造函數法創建私有變量,除此之外,還有通過私有作用域定義靜態私有變量,為單例創建私有變臉個特權方法的模塊模式等,這里不展開說明。