「函數(shù)」和「函數(shù)內(nèi)部能訪問到的變量」的總和,就是一個(gè)閉包。
閉包的目的——隱藏變量
閉包常常用來「間接訪問一個(gè)變量」。換句話說,「隱藏一個(gè)變量」。
求下面函數(shù)的執(zhí)行結(jié)果
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
},0)
}
答案是:10個(gè)10,此題考查的就是JS的異步和閉包。
異步
JS是單線程的,即一次只能執(zhí)行一個(gè)命令,不能同時(shí)進(jìn)行多個(gè)命令。
JS的異步實(shí)現(xiàn):JS會(huì)在執(zhí)行時(shí)生成一個(gè)主任務(wù)隊(duì)列(M),所有的任務(wù)都會(huì)放進(jìn)M里面去排隊(duì)等待執(zhí)行,先進(jìn)去的先執(zhí)行。當(dāng)使用setTimeout時(shí),setTimeout會(huì)將里面的函數(shù)放到異步隊(duì)列中,當(dāng)前面的任務(wù)都執(zhí)行完成后,js通過eventloop事件循環(huán),發(fā)現(xiàn)異步列隊(duì)列中有任務(wù)在等待,于是將其添加到主隊(duì)列中開始執(zhí)行。
所以上面的代碼,實(shí)際上是for循環(huán)完成之后才會(huì)執(zhí)行setTimeout的function,而這個(gè)時(shí)候i的值是10,所以打印出來的都是10。
當(dāng)調(diào)用setTimeout( func, xx )的時(shí)候,JS引擎會(huì)啟動(dòng)定時(shí)器timer,大約 xx ms以后執(zhí)行 func,不過事實(shí)上,func 會(huì)在在瀏覽器不忙的時(shí)候才會(huì)真正執(zhí)行。
閉包
閉包(closure) 是Javascript語言的一個(gè)難點(diǎn),很多人說JS初級(jí)與高級(jí)工程師的分水嶺就在于對(duì)閉包的理解。
其實(shí)所有的JS函數(shù)都是閉包,只是在平時(shí)開發(fā)中,嵌套在函數(shù)內(nèi)的閉包更能發(fā)揮作用。
談閉包之前,先來談?wù)凧S變量的作用域:
- 全局變量
var n=999;
function f1(){
alert(n);
}
f1(); // 999
這里的 n 為全局變量,函數(shù)內(nèi)部可以讀取。
- 局部變量
function f1(){
var n = 999; // 局部變量一定要聲明,不能就變成全局變量了
}
alert(n) // n is not defined
這里的 n 為局部變量,函數(shù)外部不能讀取。
那么,如果我們想要訪問和修改函數(shù)內(nèi)的局部變量,應(yīng)該怎么做呢?
那就是在函數(shù)的內(nèi)部,再定義一個(gè)函數(shù):
function f1() {
var n = 999;
function f2() {
return n = n + 1
};
return f2;
}
var a = f1();
a(); // 1000
a(); // 1001
a(); // 1002
上述代碼中的 f2 就是閉包,可以看出,通過閉包,不僅可以訪問和修改函數(shù)內(nèi)部變量,還能使其一直存在內(nèi)存中,不被回收。
實(shí)現(xiàn)的原理:由于 f2 中引用了 相對(duì)于自己的全局變量 n ,所以 f2 會(huì)一直存在內(nèi)存中,又因?yàn)?n 是 f1 中的局部變量,也就是說 f2 依賴 f1,所以說 f1 也會(huì)一直存在內(nèi)存中,并不像普通函數(shù)那樣,調(diào)用后變量便被垃圾回收了。
所以說,在setTimeout中的函數(shù)引用了外層 for循環(huán)的變量 i,導(dǎo)致 i 一直存在內(nèi)存中,不被回收,所以等到JS隊(duì)列執(zhí)行 函數(shù)時(shí),i 已經(jīng)是 10了,所以最終打印 10個(gè)10。