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