什么是閉包
什么是閉包,你可能會(huì)搜出很多答案....
《JavaScript高級(jí)程序設(shè)計(jì)》這樣描述:
閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù)影兽。
《JavaScript權(quán)威指南》這樣描述:
從技術(shù)的角度講莱革,所有的JavaScript函數(shù)都是閉包:它們都是對(duì)象,它們都關(guān)聯(lián)到作用域鏈盅视。
《你不知道的JavaScript》這樣描述:
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí)闹击,就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。
我最認(rèn)同的是《你不知道的JavaScript》中的描述淆两,雖然前面的兩種說法都沒有錯(cuò)拂酣,但閉包應(yīng)該是基于詞法作用域書寫代碼時(shí)產(chǎn)生的自然結(jié)果,是一種現(xiàn)象剑勾!你也不用為了利用閉包而特意的創(chuàng)建赵颅,因?yàn)殚]包的在你的代碼中隨處可見,只是你還不知道當(dāng)時(shí)你寫的那一段代碼其實(shí)就產(chǎn)生了閉包捂刺。
講解閉包
上面已經(jīng)說到募寨,當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí),就產(chǎn)生了閉包苛谷,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行格郁。
看一段代碼:
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
fn2();
}
fn1();
如果是根據(jù)《JavaScript高級(jí)程序設(shè)計(jì)》和《JavaScript權(quán)威指南》來說独悴,上面的代碼已經(jīng)產(chǎn)生閉包了。fn2訪問到了fn1的變量决采,滿足了條件“有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù)”坟奥,fn2本身是個(gè)函數(shù),所以滿足了條件“所有的JavaScript函數(shù)都是閉包”晒喷。
這的確是閉包访敌,但是這種方式定義的閉包不太好觀察。
再看一段代碼:
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
return fn2;
}
var fn3 = fn1();
fn3();
這樣就清晰地展示了閉包:
1.fn2的詞法作用域能訪問fn1的作用域
2.將fn2當(dāng)做一個(gè)值返回
3.fn1執(zhí)行后爷抓,將fn2的引用賦值給fn3
4.執(zhí)行fn3,輸出了變量name
我們知道通過引用的關(guān)系果复,fn3就是fn2函數(shù)本身唉地。執(zhí)行fn3能正常輸出name,這不就是fn2能記住并訪問它所在的詞法作用域耘沼,而且fn2函數(shù)的運(yùn)行還是在當(dāng)前詞法作用域之外了。
正常來說菠隆,當(dāng)fn1函數(shù)執(zhí)行完畢之后狂秘,其作用域是會(huì)被銷毀的,然后垃圾回收器會(huì)釋放那段內(nèi)存空間破衔。而閉包卻很神奇的將fn1的作用域存活了下來钱烟,fn2依然持有該作用域的引用,這個(gè)引用就是閉包读第。
總結(jié):某個(gè)函數(shù)在定義時(shí)的詞法作用域之外的地方被調(diào)用拥刻,閉包可以使該函數(shù)極限訪問定義時(shí)的詞法作用域。
注意:對(duì)函數(shù)值的傳遞可以通過其他的方式吴汪,并不一定值有返回該函數(shù)這一條路蒸眠,比如可以用回調(diào)函數(shù):
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
fn3(fn2);
}
function fn3(fn) {
fn();
}
fn1();
本例中,將內(nèi)部函數(shù)fn2傳遞給fn3近刘,當(dāng)它在fn3中被運(yùn)行時(shí),它是可以訪問到name變量的介劫。
所以無論通過哪種方式將內(nèi)部的函數(shù)傳遞到所在的詞法作用域以外案淋,它都回持有對(duì)原始作用域的引用,無論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包踢京。
再次解釋閉包
以上的例子會(huì)讓人覺得有點(diǎn)學(xué)院派了瓣距,但是閉包絕不僅僅是一個(gè)無用的概念,你寫過的代碼當(dāng)中肯定有閉包的身影蹈丸,比如類似如下的代碼:
function waitSomeTime(msg, time) {
setTimeout(function () {
console.log(msg)
}, time);
}
waitSomeTime('hello', 1000);
定時(shí)器中有一個(gè)匿名函數(shù)逻杖,該匿名函數(shù)就有涵蓋waitSomeTime函數(shù)作用域的閉包,因此當(dāng)1秒之后闻伶,該匿名函數(shù)能輸出msg够话。
另一個(gè)很經(jīng)典的例子就是for循環(huán)中使用定時(shí)器延遲打印的問題:
for (var i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
在這段代碼中,我們對(duì)其的預(yù)期是輸出1~10霎箍,但卻輸出10次11澡为。這是因?yàn)閟etTimeout中的匿名函數(shù)執(zhí)行的時(shí)候景埃,for循環(huán)都已經(jīng)結(jié)束了,for循環(huán)結(jié)束的條件是i大于10拒啰,所以當(dāng)然是輸出10次11咯完慧。
究其原因:i是聲明在全局作用中的,定時(shí)器中的匿名函數(shù)也是執(zhí)行在全局作用域中册着,那當(dāng)然是每次都輸出11了。
原因知道了甲捏,解決起來就簡單了司顿,我們可以讓i在每次迭代的時(shí)候,都產(chǎn)生一個(gè)私有的作用域大溜,在這個(gè)私有的作用域中保存當(dāng)前i的值钦奋。
for (var i = 1; i <= 10; i++) {
(function () {
var j = i;
setTimeout(function () {
console.log(j);
}, 1000);
})();
}
這樣就達(dá)到我們的預(yù)期了呀,讓我們用一種比較優(yōu)雅的寫法改造一些疙教,將每次迭代的i作為實(shí)參傳遞給自執(zhí)行函數(shù)伞租,自執(zhí)行函數(shù)中用變量去接收:
for (var i = 1; i <= 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 1000);
})(i);
}
閉包的應(yīng)用
閉包的應(yīng)用比較典型是定義模塊,我們將操作函數(shù)暴露給外部裸弦,而細(xì)節(jié)隱藏在模塊內(nèi)部:
function module() {
var arr = [];
function add(val) {
if (typeof val == 'number') {
arr.push(val);
}
}
function get(index) {
if (index < arr.length) {
return arr[index]
} else {
return null;
}
}
return {
add: add,
get: get
}
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));