??在正常情況下仗岖,如果定義了一個函數(shù)慰丛,就會產(chǎn)生一個函數(shù)作用域悲幅,在函數(shù)體中的變量會在這個作用域中使用套鹅。一旦函數(shù)執(zhí)行完成站蝠,函數(shù)所占空間就會被回收,存在于函數(shù)中的局部變量同樣被回收卓鹿,回收后將不能被訪問到菱魔。那么如果我們期望在函數(shù)執(zhí)行完成后,函數(shù)中的局部變量仍然可以被訪問到吟孙,該怎么辦呢澜倦?閉包可以實現(xiàn)這個目標,在學(xué)習(xí)閉包前杰妓,我們需要掌握一個概念:執(zhí)行上下文環(huán)境藻治。
1. 執(zhí)行上下文環(huán)境
??JavaScript每段代碼的執(zhí)行都會存在于一個執(zhí)行上下文環(huán)境中,而任何一個執(zhí)行上下文環(huán)境都會存在于整體的執(zhí)行上下文環(huán)境中巷挥。根據(jù)棧先進后出的特點桩卵,全局環(huán)境產(chǎn)生的執(zhí)行上下文會最先壓入棧中,存在于棧底倍宾。當心的函數(shù)產(chǎn)生調(diào)用時吸占,會產(chǎn)生心的執(zhí)行上下文環(huán)境,也會壓入棧中凿宾。當函數(shù)調(diào)用完成后矾屯,這個上下文環(huán)境及其中的數(shù)據(jù)都會被銷毀,并彈出棧初厚,從而進入之前的執(zhí)行上下文環(huán)境中件蚕。
??需要注意的是,處理活躍狀態(tài)的執(zhí)行上下文環(huán)境只能同時有一個产禾,如下圖深色背景部分排作。
??我們通過以下代碼了解執(zhí)行上下文環(huán)境的變化過程。
var a = 10;//1.進入全局執(zhí)行上下文環(huán)境
var fn = function (x) {
var c = 10;
console.info(c + x);
}
var bar = function (y) {
var b = 5;
fn(y + b)//3.進入fn()函數(shù)執(zhí)行上下文環(huán)境
}
bar(20);//2.進入bar()函數(shù)執(zhí)行上下文環(huán)境
從第一行代碼開始亚情,進入全局執(zhí)行上下文環(huán)境妄痪,此時執(zhí)行上下文環(huán)境中只存在全局執(zhí)行上下文環(huán)境。
當代碼執(zhí)行到第十行時楞件,調(diào)用bar()函數(shù)衫生,進入bar()函數(shù)執(zhí)行上下文環(huán)境中。
執(zhí)行到10行后土浸,進入bar()函數(shù)罪针,執(zhí)行到第八行時,執(zhí)行fn()函數(shù)黄伊,進入fn()函數(shù)執(zhí)行上下文環(huán)境中泪酱。
進入fn()中執(zhí)行第五行代碼后,fn()函數(shù)執(zhí)行上下文環(huán)境會被銷毀,從而彈出棧墓阀。
fn()函數(shù)執(zhí)行上下文環(huán)境被銷毀后毡惜,回到bar()函數(shù)執(zhí)行上下文環(huán)境中,執(zhí)行完成第九行后,bar()函數(shù)執(zhí)行上下文環(huán)境也將被銷毀斯撮,從而彈出棧经伙。
最后全局上下文環(huán)境執(zhí)行完畢,棧被清空吮成,流程執(zhí)行結(jié)束橱乱。
上面的這種代碼執(zhí)行完畢辜梳,執(zhí)行上下文環(huán)境將會被銷毀的場景粱甫,是一種比較理想的情況。
有一種情況作瞄,雖然代碼執(zhí)行完畢茶宵,但執(zhí)行上下文環(huán)境卻無法被感覺地銷毀,這就是講到的閉包宗挥。
2. 閉包的概念
??對于閉包的概念乌庶,官方有一個通用的解釋:一個擁有許多變量和綁定了這些變量的執(zhí)行上下文環(huán)境的表達式,通常是函數(shù)契耿。
閉包有兩個明顯特點:
- 函數(shù)擁有外邊變量的引用瞒大,在函數(shù)返回時,該變量仍處于活躍狀態(tài)搪桂。
- 閉包作為一個函數(shù)返回時透敌,其執(zhí)行上下文環(huán)境不會被銷毀,仍處于執(zhí)行上下文環(huán)境中踢械。
在JavaScript中存在一種內(nèi)部函數(shù)酗电,即函數(shù)聲明和函數(shù)表達式可以處于另一個函數(shù)的函數(shù)體內(nèi),在內(nèi)部函數(shù)中可以訪問外部函數(shù)聲明的變量内列,在這個內(nèi)部函數(shù)在包含他們的外部函數(shù)之外被調(diào)用時撵术,機會形成閉包。
?? 我們來看下以下代碼话瞧。
function fn() {
var max = 10;
return function bar(x) {
if (x > max) {
console.info(x)
}
}
}
var f1 = fn();
f1(11);//11
代碼執(zhí)行后嫩与,生成全局上下文環(huán)境,并壓入棧中交排。
代碼執(zhí)行到第九行時蕴纳,進入fn()函數(shù)中,生成fn()函數(shù)執(zhí)行上下文環(huán)境个粱,并將其壓入棧中古毛。
fn()函數(shù)返回一個bar()函數(shù),并將其賦給變量f1。
當代碼執(zhí)行到第10行時稻薇,調(diào)用f1()函數(shù)嫂冻,注意此時是一個關(guān)鍵節(jié)點,f1()函數(shù)包含了對max變量的引用塞椎,而max變量存在于外部函數(shù)fn()中的桨仿,此時fn()函數(shù)執(zhí)行上下文環(huán)境并不會被直接銷毀,依然存在于執(zhí)行上下文環(huán)境中案狠。
等到第10行代碼執(zhí)行結(jié)束后服傍,bar()函數(shù)執(zhí)行完畢,bar()函數(shù)執(zhí)行上下文環(huán)境也被銷毀骂铁,同時因為max變量引用會被釋放吹零,fn()函數(shù)執(zhí)行上下文環(huán)境也一同被銷毀。
最后全局執(zhí)行上下文環(huán)境執(zhí)行完畢拉庵,棧被清空灿椅,流程執(zhí)行結(jié)束。
閉包所存在最大的問題就是消耗內(nèi)存钞支,如果閉包使用越來越多茫蛹,內(nèi)存消耗將越來越大。
3. 閉包的用途
??在了解閉包之后烁挟,我們可以結(jié)合閉包的特點婴洼,寫出一些更加簡潔優(yōu)雅的代碼,并且能在某些方面提升代碼的執(zhí)行效率撼嗓。
- 結(jié)果緩存
在開發(fā)過程中柬采,我們可能會遇到這樣的場景,假如有一個處理很耗時的函數(shù)對象静稻,每次調(diào)用都會消耗很長時間警没。
我們可以將其處理結(jié)果在內(nèi)存中緩存起來。這樣在代碼執(zhí)行時振湾,如果內(nèi)存中有杀迹,則直接返回;如果內(nèi)存中沒有押搪,則調(diào)用函數(shù)進行計算树酪,更新緩存并返回結(jié)果。
因為閉包不會釋放外部變量的引用大州,所以能將外部變量值緩存在內(nèi)存中续语。
var checkedBox = (function (){
//緩存的容器
var cache = {};
return {
searchBox: function (id){
// 如果再內(nèi)存中,則直接返回
if(id in cache){
return `查找的緩存結(jié)果為:${cache[id]}`
}
//經(jīng)過一段很耗時的dealFn()函數(shù)處理
var result = dealFn(id);
//更新緩存結(jié)果
cache[id] = result;
//返回計算的結(jié)果
return `查找的結(jié)果為:${result}`
}
}
})()
//處理很耗時的函數(shù)
function dealFn(id) {
console.info('這是很耗時的操作')
return id;
}
//兩次調(diào)用searchBox函數(shù)
console.info(checkedBox.searchBox(1))
console.info(checkedBox.searchBox(1))
在上面的代碼中厦画,末尾兩次調(diào)用searchBox(1)()函數(shù)疮茄,在第一次調(diào)用時滥朱,id為1的值并未在緩存對象cache中,因為會執(zhí)行很耗時的函數(shù)力试,輸出的結(jié)果為“1”徙邻。
這是很耗時的操作
查找的結(jié)果為:1
而第二次執(zhí)行searchBox(1)函數(shù)時,由于第一次已經(jīng)將結(jié)果更新到cache對象中畸裳,并且該對象引用并未被回收缰犁,因此會直接從內(nèi)存的cache對象中讀取,直接返回“1”怖糊,最后輸出的結(jié)果為“1”帅容。
查找的緩存結(jié)果為:1
這樣并沒有執(zhí)行很耗時的函數(shù),還間接提高了執(zhí)行效率伍伤。
- 封裝
??在JavaScript中提倡的模塊化思想是希望將具有一定特征的屬性封裝到一起并徘,只需要對外暴露對應(yīng)的函數(shù),并不關(guān)心內(nèi)部邏輯的實現(xiàn)嚷缭。
例如饮亏,我們可以借助數(shù)組實現(xiàn)一個棧耍贾,只對外暴露出表示入棧和出棧的push()函數(shù)和pop()函數(shù)阅爽,以及表示棧長度的size()函數(shù)。
var stack = (function () {
//使用數(shù)組模仿棧的實現(xiàn)
var arr = [];
//棧
return{
push:function (value){
arr.push(value)
},
pop:function () {
return arr.pop()
},
size:function () {
return arr.length
}
}
})()
stack.push('abc');
stack.push('def');
console.info(stack.size())//2
stack.pop();
console.info(stack.size())//1
上面的代碼中存在一個立即執(zhí)行函數(shù)荐开,在函數(shù)內(nèi)部會產(chǎn)生一個執(zhí)行上下文環(huán)境付翁,最后返回一個表示棧的對象并賦給stack變量。在匿名函數(shù)執(zhí)行完畢后晃听,其執(zhí)行上下文環(huán)境并不會被銷毀百侧,因為在對象的push()、pop()能扒、size()等函數(shù)中包含了對arr變量的引用佣渴,arr變量會繼續(xù)存在于內(nèi)存中,所以后面幾次對stack變量的操作會使stack變量的長度產(chǎn)生變化初斑。
接下來我們將通過幾道練習(xí)題加深大家對閉包的理解辛润。
1. ul中有若干個li,每次但擊li,輸出li的索引值
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
var lis = document.getElementsByTagName('ul')[0].children;
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
console.log(i);
};
}
</script>
但是真正運行后卻發(fā)現(xiàn)见秤,結(jié)果并不如自己所想砂竖,每次單擊后輸出的并不是索引值,而一直都是“5”鹃答。
這是為什么呢乎澄?因為在我們單擊li,觸發(fā)li的click事件之前测摔,for循環(huán)已經(jīng)執(zhí)行結(jié)束了置济,而for循環(huán)結(jié)束的條件就是最后一次i++執(zhí)行完畢,此時i的值為5,所以每次單擊li后返回的都是“5”浙于。
采取使用閉包的方法可以很好地解決這個問題修噪。
var lis = document.getElementsByTagName('ul')[0].children;
for (let i = 0; i < lis.length; i++) {
(function (index) {
lis[i].onclick = function () {
console.info(index)
}
})(i)
}
在每一輪的for循環(huán)中,我們將索引值i傳入一個匿名立即執(zhí)行函數(shù)中路媚,在該匿名函數(shù)中存在對外部變量lis的引用黄琼,因此會形成一個閉包。而閉包中的變量index整慎,即外部傳入的i值會繼續(xù)存在于內(nèi)存中脏款,所以當單擊li時,就會輸出對應(yīng)的索引index值裤园。
2. 定時器問題
定時器setTimeout()函數(shù)和for循環(huán)在一起使用撤师,總會出現(xiàn)一些意想不到的結(jié)果,我們看看下面的代碼拧揽。
var arr = ['one', 'two', 'three'];
for(var i = 0; i < arr.length; i++) {
setTimeout(function () {
console.log(arr[i]);
}, i * 1000);
}
在這道題目中剃盾,我們期望通過定時器從第一個元素開始往后,每隔一秒輸出arr數(shù)組中的一個元素。
但是運行過后碌识,我們卻會發(fā)現(xiàn)結(jié)果是每隔一秒輸出一個“undefined”是整,這是為什么呢?
setTimeout()函數(shù)與for循環(huán)在調(diào)用時會產(chǎn)生兩個獨立執(zhí)行上下文環(huán)境积蔚,當setTimeout()函數(shù)內(nèi)部的函數(shù)執(zhí)行時,for循環(huán)已經(jīng)執(zhí)行結(jié)束烦周,而for循環(huán)結(jié)束的條件是最后一次i++執(zhí)行完畢尽爆,此時i的值為3,所以實際上setTimeout()函數(shù)每次執(zhí)行時读慎,都會輸出arr[3]的值漱贱。而因為arr數(shù)組最大索引值為2,所以會間隔一秒輸出“undefined”夭委。
通過閉包可以解決這個問題幅狮,代碼如下所示。
var arr = ['one', 'two', 'three'];
for(var i = 0; i < arr.length; i++) {
(function (time) {
setTimeout(function () {
console.log(arr[time]);
}, time * 1000);
})(i);
}
通過立即執(zhí)行函數(shù)將索引i作為參數(shù)傳入闰靴,在立即函數(shù)執(zhí)行完成后彪笼,由于setTimeout()函數(shù)中有對arr變量的引用,其執(zhí)行上下文環(huán)境不會被銷毀蚂且,因此對應(yīng)的i值都會存在內(nèi)存中配猫。所以每次執(zhí)行setTimeout()函數(shù)時,i都會是數(shù)組對應(yīng)的索引值0杏死、1泵肄、2捆交,從而間隔一秒輸出“one”“two”“three”。
3. 作用域鏈問題
閉包往往會涉及作用域鏈問題腐巢,尤其是包含this屬性時品追。
var name = 'outer';
var obj = {
name: 'inner',
method: function () {
return function () {
return this.name;
}
}
};
console.log(obj.method()()); // outer
在調(diào)用obj.method()函數(shù)時,會返回一個匿名函數(shù)冯丙,而該匿名函數(shù)中返回的是this.name肉瓦,因為引用到了this屬性,在匿名函數(shù)中胃惜,this相當于一個外部變量泞莉,所以會形成一個閉包。
在JavaScript中船殉,this指向的永遠是函數(shù)的調(diào)用實體鲫趁,而匿名函數(shù)的實體是全局對象window,因此會輸出全局變量name的值“outer”利虫。
如果想要輸出obj對象自身的name屬性挨厚,應(yīng)該如何修改呢?簡單來說就是改變this的指向糠惫,將其指向obj對象本身疫剃。
var name = 'outer';
var obj = {
name: 'inner',
method: function () {
// 用_this保存obj中的this
var _this = this;
return function () {
return _this.name;
}
}
};
console.log(obj.method()()); // inner
在method()函數(shù)中利用_this變量保存obj對象中的this,在匿名函數(shù)的返回值中再去調(diào)用_this.name寞钥,此時_this就指向obj對象了慌申,因此會輸出“inner”陌选。
4. 多個相同函數(shù)名問題
// 第一個foo()函數(shù)
function foo(a, b) {
console.log(b);
return {
// 第二個foo()函數(shù)
foo: function (c) {
// 第三個foo()函數(shù)
return foo(c, a);
}
}
}
var x = foo(0); x.foo(1); x.foo(2); x.foo(3);
var y = foo(0).foo(1).foo(2).foo(3);
var z = foo(0).foo(1); z.foo(2); z.foo(3);
在上面的代碼中理郑,出現(xiàn)了3個具有相同函數(shù)名的foo()函數(shù),返回的第三個foo()函數(shù)中包含了對第一個foo()函數(shù)參數(shù)a的引用咨油,因此會形成一個閉包您炉。
在完成這道題目之前,我們需要搞清楚這3個foo()函數(shù)的指向役电。
首先最外層的foo()函數(shù)是一個具名函數(shù)赚爵,返回的是一個具體的對象。
第二個foo()函數(shù)是最外層foo()函數(shù)返回對象的一個屬性法瑟,該屬性指向一個匿名函數(shù)冀膝。
第三個foo()函數(shù)是一個被返回的函數(shù),該foo()函數(shù)會沿著原型鏈向上查找霎挟,而foo()函數(shù)在局部環(huán)境中并未定義窝剖,最終會指向最外層的第一個foo()函數(shù),因此第三個和第一個foo()函數(shù)實際是指向同一個函數(shù)酥夭。
理清3個foo()函數(shù)的指向后赐纱,我們再來看看具體的執(zhí)行過程脊奋。
var x = foo(0); x.foo(1); x.foo(2); x.foo(3);
(1)在執(zhí)行foo(0)時,未傳遞b值疙描,所以輸出“undefined”诚隙,并返回一個對象,將其賦給變量x起胰。
在執(zhí)行x.foo(1)時久又,foo()函數(shù)閉包了外層的a值,就是第一次調(diào)用的0效五,此時c=1籽孙,因為第三層和第一層為同一個函數(shù),所以實際調(diào)用為第一層的的foo(1, 0)火俄,此時a為1犯建,b為0,輸出“0”瓜客。
執(zhí)行x.foo(2)和x.foo(3)時适瓦,和x.foo(1)是相同的原理,因此都會輸出“0”谱仪。
第一行輸出結(jié)果為“undefined玻熙,0,0疯攒,0”嗦随。
var y = foo(0).foo(1).foo(2).foo(3);
(2)在執(zhí)行foo(0)時,未傳遞b值敬尺,所以輸出“undefined”枚尼,緊接著進行鏈式調(diào)用foo(1),其實這部分與(1)中的第二部分分析一樣砂吞,實際調(diào)用為foo(1, 0)署恍,此時a為1,b為0蜻直,會輸出“0”盯质。
foo(1)執(zhí)行后返回的是一個對象,其中閉包了變量a的值為1概而,當foo(2)執(zhí)行時呼巷,實際是返回foo(2, 1),此時的foo()函數(shù)指向第一個函數(shù)赎瑰,因此會執(zhí)行一次foo(2, 1)王悍,此時a為2,b為1乡范,輸出“1”配名。
foo(2)執(zhí)行后返回一個對象啤咽,其中閉包了變量a的值為2,當foo(3)執(zhí)行時渠脉,實際是返回foo(3, 2)宇整,因此會執(zhí)行一次foo(3, 2),此時a為3芋膘,b為2鳞青,輸出“2”。
第二行輸出結(jié)果為“undefined为朋,0臂拓,1,2”习寸。
var z = foo(0).foo(1); z.foo(2); z.foo(3);
(3)前兩步foo(0).foo(1)的執(zhí)行結(jié)果與(1)胶惰、(2)的分析相同,輸出“undefined”和“0”霞溪。
foo(0).foo(1)執(zhí)行完畢后孵滞,返回的是一個對象,其中閉包了變量a的值為1鸯匹,當調(diào)用z.foo(2)時坊饶,實際是返回foo(2, 1),因此會執(zhí)行foo(2, 1)殴蓬,此時a為2匿级,b為1,輸出“1”染厅。
執(zhí)行z.foo(3)時痘绎,與z.foo(2)一樣,實際是返回foo(3, 1)糟秘,因此會執(zhí)行foo(3, 1)简逮,此時a為3,b為1尿赚,輸出“1”。
第三行輸出結(jié)果為“undefined蕉堰,0凌净,1,1”屋讶。
4. 小結(jié)
閉包如果使用合理冰寻,在一定程度上能提高代碼執(zhí)行效率;如果使用不合理皿渗,則會造成內(nèi)存浪費斩芭,性能下降轻腺。接下來總結(jié)閉包的優(yōu)點和缺點。
1. 閉包的優(yōu)點
- 保護函數(shù)內(nèi)變量的安全划乖,實現(xiàn)封裝贬养,防止變量流入其他環(huán)境發(fā)生命名沖突,造成環(huán)境污染琴庵。
- 在適當?shù)臅r候误算,可以在內(nèi)存中維護變量并緩存,提高執(zhí)行效率迷殿。
2. 閉包的缺點
- 消耗內(nèi)存:通常來說儿礼,函數(shù)的活動對象會隨著執(zhí)行上下文環(huán)境一起被銷毀,但是庆寺,由于閉包引用的是外部函數(shù)的活動對象蚊夫,因此這個活動對象無法被銷毀,這意味著懦尝,閉包比一般的函數(shù)需要消耗更多的內(nèi)存这橙。
- 泄漏內(nèi)存: 在IE9之前,如果閉包的作用域鏈中存在DOM對象导披,則意味著該DOM對象無法被銷毀屈扎,造成內(nèi)存泄漏。
function closure() {
var element = document.getElementById("elementID");
element.onclick = function () {
console.log(element.id);
};
}
??在closure()函數(shù)中撩匕,給一個element元素綁定了click事件鹰晨,而在這個click事件中,輸出了element元素的id屬性止毕,即在onclick()函數(shù)的閉包中存在了對外部元素element的引用模蜡,那么該element元素在網(wǎng)頁關(guān)閉之前會一直存在于內(nèi)存之中,不會被釋放扁凛。
??如果這樣的事件處理的函數(shù)很多忍疾,將會導(dǎo)致大量內(nèi)存被占用,進而嚴重影響性能谨朝。
??對應(yīng)的解決辦法是:先將需要使用的屬性使用臨時變量進行存儲卤妒,然后在事件處理函數(shù)時使用臨時變量進行操作;此時閉包中雖然不直接引用element元素字币,但是對id值的調(diào)用仍然會導(dǎo)致element元素的引用被保存则披,此時應(yīng)該手動將element元素設(shè)置為null。
function closure() {
var element = document.getElementById("elementID");
// 使用臨時變量存儲
var id = element.id;
element.onclick = function () {
console.log(id);
};
// 手動將元素設(shè)置為null
element = null;
}
閉包既有好處洗出,也有壞處士复。我們應(yīng)該合理評估,適當使用翩活,盡可能地發(fā)揮出閉包的最大用處阱洪。