閉包是JS中一個(gè)很重要的概念,閉包其實(shí)是基于詞法作用域規(guī)則實(shí)現(xiàn)的,詞法作用域規(guī)則會(huì)使函數(shù)在查找變量時(shí)從函數(shù)內(nèi)部再到函數(shù)定義時(shí)的作用域,而不是從函數(shù)內(nèi)部到函數(shù)使用時(shí)的作用域。所以無論函數(shù)在哪里被調(diào)用,也無論它如何被調(diào)用,它的詞法作用域都只由函數(shù)被聲明時(shí)所處的位置決定。
基于這個(gè)規(guī)則,那么函數(shù)在當(dāng)前詞法作用域之外執(zhí)行,也可以記住并訪問函數(shù)聲明時(shí)所在的詞法作用域,這時(shí)就產(chǎn)生了閉包。
高程定義閉包:閉包是指有權(quá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都能訪問f1的變量所以f1不會(huì)被回收
閉包產(chǎn)生條件
通過以上代碼,我們可以得到閉包產(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é)便是:無論使用何種方式對(duì)函數(shù)類型的值進(jìn)行傳遞,當(dāng)函數(shù)在別處被調(diào)用時(shí)都可以觀察到閉包。
// 無論通過何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外, 它都會(huì)持有對(duì)原始定義作用域的引用,無論在何處執(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是從哪里來的。 這個(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出來。
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í),仍能訪問到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ì)阻止這一過程雖然例子中的閉包被保存在了全局變量中,但是閉包的作用域鏈并不會(huì)發(fā)生任何改變。在閉包中,能訪問到的變量,仍然是作用域鏈上能夠查詢到的變量即閉包可以使得它誕生環(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)部變量。通過閉包,start的狀態(tài)被保留了,每一次調(diào)用都是在上一次調(diào)用的基礎(chǔ)上進(jìn)行計(jì)算。從中可以看到,閉包add使得函數(shù)addNum的內(nèi)部環(huán)境,一直存在。所以,閉包可以看作是函數(shù)內(nèi)部作用域的一個(gè)接口