閉包是JS中一個(gè)很重要的概念蟹漓,閉包其實(shí)是基于詞法作用域規(guī)則實(shí)現(xiàn)的,詞法作用域規(guī)則會(huì)使函數(shù)在查找變量時(shí)從函數(shù)內(nèi)部再到函數(shù)定義時(shí)的作用域源内,而不是從函數(shù)內(nèi)部到函數(shù)使用時(shí)的作用域牧牢。所以無(wú)論函數(shù)在哪里被調(diào)用,也無(wú)論它如何被調(diào)用姿锭,它的詞法作用域都只由函數(shù)被聲明時(shí)所處的位置決定塔鳍。
基于這個(gè)規(guī)則,那么函數(shù)在當(dāng)前詞法作用域之外執(zhí)行呻此,也可以記住并訪問(wèn)函數(shù)聲明時(shí)所在的詞法作用域轮纫,這時(shí)就產(chǎn)生了閉包。
高程定義閉包:閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)焚鲜。
function f1() {
var a = 1; // 3.調(diào)用的函數(shù)內(nèi)部使用了父級(jí)作用域的內(nèi)部變量
function f2() { // 1.調(diào)用的函數(shù)是父級(jí)作用域內(nèi)部聲明的
console.log(a);
}
return f2;
}
var f3 = f1(); // 2.調(diào)用的函數(shù)是在父級(jí)作用域之外進(jìn)行調(diào)用掌唾,foo()執(zhí)行后將bar 函數(shù)本身當(dāng)作一個(gè)值類型進(jìn)行傳遞給baz。
f3(); // 這就是閉包的效果忿磅。執(zhí)行之后糯彬,輸出f1中的a,因?yàn)椴徽摵螘r(shí)何處調(diào)用f2都能訪問(wèn)f1的變量所以f1不會(huì)被回收
閉包產(chǎn)生條件
通過(guò)以上代碼葱她,我們可以得到閉包產(chǎn)生的條件:
- 調(diào)用的函數(shù)是父級(jí)作用域內(nèi)部聲明的撩扒;
- 調(diào)用的函數(shù)是在父級(jí)作用域之外進(jìn)行調(diào)用;
- 調(diào)用的函數(shù)內(nèi)部使用了父級(jí)作用域的內(nèi)部變量吨些;
總結(jié)便是:無(wú)論使用何種方式對(duì)函數(shù)類型的值進(jìn)行傳遞搓谆,當(dāng)函數(shù)在別處被調(diào)用時(shí)都可以觀察到閉包。
// 無(wú)論通過(guò)何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外豪墅, 它都會(huì)持有對(duì)原始定義作用域的引用泉手,無(wú)論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包。
function foo1() {
var a = 1;
function baz1() {
console.log(a); // 1
}
bar1(baz1); // baz1被作為參數(shù)傳遞到外部函數(shù)bar1中
}
function bar1(fn) {
fn(); // 這就是閉包偶器!
}
foo1();
var fn2;
function foo2() {
var a = 2;
function baz2() {
console.log(a);
}
fn2 = baz2; // 將 baz2分配給全局變量斩萌,也相當(dāng)于傳遞到外部
}
function bar2() {
fn2(); // 這就是閉包!
}
foo2();
bar2(); // 2
// 主要看看是否是外部調(diào)用屏轰。因?yàn)橛脩酎c(diǎn)擊時(shí)觸發(fā)事件颊郎,不是在foo3中內(nèi)部調(diào)用的。
var foo3 = function () {
var btn = document.querySelector("#myBtn");
var a = 3;
btn.onclick = function () {
alert(a);
}
}
foo3();
下面是一個(gè)關(guān)于閉包的金典例子:
for (var i = 1; i <= 5; i++) { // 只有一個(gè)全局作用域亭枷,運(yùn)行timer是尋找變量i只有全局的i = 6
setTimeout(function timer() {
console.log(i); // 運(yùn)行時(shí)會(huì)以每秒一次的頻率輸出五次 6
}, i * 1000);
}
// 首先解釋6是從哪里來(lái)的袭艺。 這個(gè)循環(huán)的終止條件是i不再<=5。 條件首次成立時(shí)i的值是6叨粘。因此猾编,輸出顯示的是循環(huán)結(jié)束時(shí)i的最終值瘤睹。延遲函數(shù)的回調(diào)會(huì)在循環(huán)結(jié)束時(shí)才執(zhí)行。事實(shí)上答倡,當(dāng)定時(shí)器運(yùn)行時(shí)即使每個(gè)迭代中執(zhí)行的是setTimeout(.., 0)轰传,所有的回調(diào)函數(shù)依然是在循環(huán)結(jié)束后才會(huì)被執(zhí)行,因此會(huì)每次輸出一個(gè)6出來(lái)瘪撇。
for (var i = 1; i <= 5; i++) {
// 每次循環(huán)創(chuàng)建一個(gè)立即函數(shù)获茬,產(chǎn)生一個(gè)新的作用域
(function (j) { // 利用立即函數(shù),每次循環(huán)創(chuàng)建單獨(dú)的函數(shù)作用域并捕獲每次循環(huán)的i作為參數(shù)傳入倔既,timer函數(shù)是一個(gè)閉包恕曲,它在立即函數(shù)中聲明,在setTimeOut回調(diào)使用渤涌,它會(huì)保留傳入的參數(shù)i的值佩谣,當(dāng)延遲函數(shù)在作用域之外調(diào)用時(shí),仍能訪問(wèn)到i
setTimeout(function timer() {
console.log(j); // 能夠正常輸出1, 2, 3, 4, 5
}, j * 1000);
})(i);
}
閉包作用
閉包的最大用處有兩個(gè)实蓬,一個(gè)是可以讀取函數(shù)內(nèi)部的變量茸俭,另一個(gè)就是讓這些變量始終保持在內(nèi)存中。函數(shù)的執(zhí)行上下文安皱,在執(zhí)行完畢之后调鬓,生命周期結(jié)束,那么該函數(shù)的執(zhí)行上下文就會(huì)失去引用酌伊。其占用的內(nèi)存空間很快就會(huì)被垃圾回收器釋放腾窝。可是閉包的存在腺晾,會(huì)阻止這一過(guò)程雖然例子中的閉包被保存在了全局變量中燕锥,但是閉包的作用域鏈并不會(huì)發(fā)生任何改變辜贵。在閉包中悯蝉,能訪問(wèn)到的變量,仍然是作用域鏈上能夠查詢到的變量即閉包可以使得它誕生環(huán)境一直存在托慨。請(qǐng)看下面的例子鼻由,閉包使得內(nèi)部變量記住上一次調(diào)用時(shí)的運(yùn)算結(jié)果:
function addNum(num) {
return function () {
return num++;
};
}
var add = addNum(1);
add() // 1
add() // 2
add() // 3
// 上面代碼中,num是函數(shù)addNum的內(nèi)部變量厚棵。通過(guò)閉包蕉世,start的狀態(tài)被保留了,每一次調(diào)用都是在上一次調(diào)用的基礎(chǔ)上進(jìn)行計(jì)算婆硬。從中可以看到狠轻,閉包add使得函數(shù)addNum的內(nèi)部環(huán)境,一直存在彬犯。所以向楼,閉包可以看作是函數(shù)內(nèi)部作用域的一個(gè)接口