|
|
|
|
|
在一個(gè)函數(shù)外面加一個(gè)括號(hào),就成為了一個(gè)閉包,說實(shí)在的,閉包函數(shù)在實(shí)際編程中用得并不多,但是這卻是學(xué)習(xí)JavaScript必須掌握的知識(shí),并且,閉包是前端考試/面試的必考題。因此,我們要學(xué)會(huì)和懂得使用這個(gè)知識(shí)點(diǎn)。在本文中,將通過一個(gè)示例,讓你了解閉包的功能作用是什么。
示例
場景設(shè)計(jì):有個(gè)函數(shù)體是一個(gè)計(jì)數(shù)器,我現(xiàn)在要讓計(jì)數(shù)器初始值增加3,如何編寫代碼?
普通函數(shù)實(shí)現(xiàn)的思路
使用普通函數(shù)實(shí)現(xiàn)的思路是這樣:
var counter = 0;
function add() {
return counter += 1;
}
add();
add();
add();// 計(jì)數(shù)器現(xiàn)在為 3
輸出
現(xiàn)在我們已經(jīng)達(dá)到了目的,可是問題來了,代碼中的任何一個(gè)函數(shù)都可以隨意改變counter
的值,因?yàn)?code>counter是一個(gè)全局變量,所以這個(gè)計(jì)數(shù)器并不完美。那我們把counter
放在add
函數(shù)里面不就好了么?
function add() {
var counter = 0;
return counter += 1;
}
add();
add();
add();// 本意是想輸出 3, 但輸出的都是 1
輸出
所以這樣做的話,每次調(diào)用add
函數(shù),counter
的值都要被初始化為0,還是達(dá)不到我們的目的。
使用閉包的實(shí)現(xiàn)思路
這時(shí)候我們可以用閉包去解決這個(gè)問題了,先看代碼。
var add = (function () {
var counter = 0;
return function () {return counter += 1;}
})();
add();
add();
add();// 計(jì)數(shù)器為 3
這時(shí)候我們完美實(shí)現(xiàn)了計(jì)數(shù)器。這段非常精簡,可以拆分成如下等價(jià)代碼。
function outerFunction () {
var counter = 0;
function innerFunction (){
return counter += 1;
}
return innerFunction;
}
var add = outerFunction();
add();
add();
add();// 計(jì)數(shù)器為 3
輸出
這時(shí)候的add
就形成了一個(gè)閉包。一個(gè)閉包由兩部分組成,函數(shù)和創(chuàng)建該函數(shù)的環(huán)境。環(huán)境是由環(huán)境中的局部變量組成的。對于閉包add
來說,它由函數(shù)innerFunction
和變量counter
組成,所以這時(shí)候add
是可以訪問變量counter
的。
結(jié)論
所以閉包的功能就是使一個(gè)函數(shù)能訪問另一個(gè)函數(shù)作用域中的變量。形成閉包之后,該變量不會(huì)被垃圾回收機(jī)制回收。
閉包的原理其實(shí)還是作用域。
知識(shí)擴(kuò)展
一個(gè)函數(shù)和對其周圍狀態(tài)(lexical environment,詞法環(huán)境)的引用捆綁在一起(或者說函數(shù)被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個(gè)內(nèi)層函數(shù)中訪問到其外層函數(shù)的作用域。在 JavaScript 中,每當(dāng)創(chuàng)建一個(gè)函數(shù),閉包就會(huì)在函數(shù)創(chuàng)建的同時(shí)被創(chuàng)建出來。
請看下面的代碼:
function init() {
var name = "WebKaka"; // name 是一個(gè)被 init 創(chuàng)建的局部變量
function displayName() { // displayName() 是內(nèi)部函數(shù),一個(gè)閉包
console.log(name); // 使用了父函數(shù)中聲明的變量
}
displayName();
}
init();
輸出
init()
創(chuàng)建了一個(gè)局部變量 name
和一個(gè)名為 displayName()
的函數(shù)。displayName()
是定義在 init()
里的內(nèi)部函數(shù),并且僅在 init()
函數(shù)體內(nèi)可用。請注意,displayName()
沒有自己的局部變量。然而,因?yàn)樗梢栽L問到外部函數(shù)的變量,所以 displayName()
可以使用父函數(shù) init()
中聲明的變量 name
。
運(yùn)行該代碼后發(fā)現(xiàn), displayName()
函數(shù)內(nèi)的 log()
語句成功顯示出了變量 name
的值(該變量在其父函數(shù)中聲明)。這個(gè)詞法作用域的例子描述了分析器如何在函數(shù)嵌套的情況下解析變量名。詞法(lexical)一詞指的是,詞法作用域根據(jù)源代碼中聲明變量的位置來確定該變量在何處可用。嵌套函數(shù)可訪問聲明于它們外部作用域的變量。
現(xiàn)在來看以下例子 :
function makeFunc() {
var name = "WebKaka";
function displayName() {
console.log(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
輸出
運(yùn)行這段代碼的效果和之前 init()
函數(shù)的示例完全一樣。其中不同的地方(也是有意思的地方)在于內(nèi)部函數(shù) displayName()
在執(zhí)行前,從外部函數(shù)返回。
第一眼看上去,也許不能直觀地看出這段代碼能夠正常運(yùn)行。在一些編程語言中,一個(gè)函數(shù)中的局部變量僅存在于此函數(shù)的執(zhí)行期間。一旦 makeFunc()
執(zhí)行完畢,你可能會(huì)認(rèn)為 name
變量將不能再被訪問。然而,因?yàn)榇a仍按預(yù)期運(yùn)行,所以在 JavaScript 中情況顯然與此不同。
原因在于,JavaScript 中的函數(shù)會(huì)形成了閉包。 閉包是由函數(shù)以及聲明該函數(shù)的詞法環(huán)境組合而成的。該環(huán)境包含了這個(gè)閉包創(chuàng)建時(shí)作用域內(nèi)的任何局部變量。在本例子中,myFunc
是執(zhí)行 makeFunc
時(shí)創(chuàng)建的 displayName
函數(shù)實(shí)例的引用。displayName
的實(shí)例維持了一個(gè)對它的詞法環(huán)境(變量 name
存在于其中)的引用。因此,當(dāng) myFunc
被調(diào)用時(shí),變量 name
仍然可用,其值 WebKaka
就被傳遞到log
中。