重學(xué)JavaScript(函數(shù))閉包
序言
學(xué)習(xí)JavaScript切勿好高騖遠(yuǎn)。正所謂貪多嚼不爛,前端標(biāo)準(zhǔn)和工具這幾年的飛速發(fā)展,以及不時(shí)冒出的“新鮮玩意”讓眾多前端從業(yè)者驚呼:“學(xué)不動(dòng)啦學(xué)不動(dòng)啦!學(xué)習(xí)速度跟不上技術(shù)發(fā)展速度!我感到手忙腳亂、力不從心……"如果你有以上“癥狀”,請(qǐng)勿著急,這不過(guò)是你內(nèi)心不安造成的。你為何追新?你又何苦追新?在根基不牢的情況下,就算蓋樓蓋到18層,再往上堆一塊磚,都可能導(dǎo)致大樓坍塌!這結(jié)果絕非你預(yù)期。所以,此時(shí)你應(yīng)該沉下心來(lái)苦練基礎(chǔ)。而非死鉆牛角尖。硬要及時(shí)掌握那些業(yè)界最新冒出來(lái)的“玩意兒”對(duì)你無(wú)益處。
前言
我們知道,作用域鏈查找標(biāo)識(shí)符的順序是從當(dāng)前作用域開(kāi)始一級(jí)一級(jí)往上查找。因此,通過(guò)作用域鏈,JavaScript函數(shù)內(nèi)部可以讀取函數(shù)外部的變,但反過(guò)來(lái),函數(shù)的外部通常則無(wú)法讀取函數(shù)內(nèi)部的變量。在實(shí)際應(yīng)用中,有時(shí)需要真正在函數(shù)外部訪問(wèn)函數(shù)內(nèi)部的局部變量,此時(shí)最常用的方法就是使用閉包。
那么什么是閉包?所謂閉包,就是同時(shí)含有對(duì)函數(shù)對(duì)象以及作用域?qū)ο笠玫膶?duì)象。閉包主要是用來(lái)獲取作用域鏈或原型鏈上的變量或值。創(chuàng)建閉包最常見(jiàn)的方式是在一個(gè)函數(shù)中聲明內(nèi)部函數(shù)(也稱嵌套函數(shù)),并返回內(nèi)部函數(shù)。此時(shí)在函數(shù)外部就可以通過(guò)調(diào)用函數(shù)得到內(nèi)部函數(shù)。雖然按照閉包的概念,所有訪問(wèn)了外部變量的JavaScript函數(shù)都是閉包。但我們平常絕大部分時(shí)候所謂的閉包其實(shí)指的就是內(nèi)部函數(shù)閉包。
閉包可以將一些數(shù)據(jù)封裝私有屬性以確保這些變量的安全訪問(wèn),這個(gè)功能給應(yīng)用帶來(lái)了極大的好處。需要注意的是,閉包如果使用不當(dāng),也會(huì)帶來(lái)一些意想不到的問(wèn)題。下面就通過(guò)幾個(gè)示例來(lái)演示一下閉包的創(chuàng)建、使用和可能存在的問(wèn)題及其解決方法。
示例1: 創(chuàng)建閉包。
<!DOCTYPE html>
<html>
<head>
<title>閉包</title>
</head>
<body>
<script type="text/javascript">
function outer(argument) {
var b=0;
return function inner (){
b++;
console.log("內(nèi)部的b:"+b);
}
}
var func = outer();//1 通過(guò)外部變量引用函數(shù)返回的內(nèi)部函數(shù)
console.log(func);//2 輸出內(nèi)部函數(shù)定義代碼
func();//3 通過(guò)閉包訪問(wèn)局部變量b,此時(shí)b=1;
console.log("外部函數(shù)中b:"+b); //4 出錯(cuò),報(bào)引用錯(cuò)誤。
</script>
</body>
</html>
上述代碼在外部函數(shù)outer中聲明內(nèi)部函數(shù)inner,并返回內(nèi)部函數(shù),同時(shí)在outer函數(shù)外面,變量func引用了outer函數(shù)返回的內(nèi)部函數(shù),所以內(nèi)部函數(shù)inner是一個(gè)閉包。該閉包訪問(wèn)了外部函數(shù)的局部變量b。1處代碼通過(guò)調(diào)用外部函數(shù)返回內(nèi)部函數(shù)并賦給外部變量func,使func變量引用內(nèi)部函數(shù),所以2處代碼將輸出inner函數(shù)的整個(gè)定義代碼。3處代碼通過(guò)對(duì)外部變量func添加一對(duì)小括號(hào)后調(diào)用內(nèi)部函數(shù)inner,從而達(dá)到在函數(shù)外部訪問(wèn)局部變量b的目的。執(zhí)行4處的代碼時(shí)將報(bào)ReferenceError錯(cuò)誤,因?yàn)閎是局部變量,不能在函數(shù)外部直接訪問(wèn)局部變量。
我們知道函數(shù)執(zhí)行完畢時(shí),運(yùn)行期上下文會(huì)被銷(xiāo)毀,與之關(guān)聯(lián)的活動(dòng)對(duì)象也會(huì)隨之銷(xiāo)毀,因此離開(kāi)函數(shù)后,屬于活動(dòng)對(duì)象的局部變量將不能被訪問(wèn)。但是為什么上述示例中的outer函數(shù)執(zhí)行完后,它的局部變量還能被內(nèi)部函數(shù)訪問(wèn)呢?這個(gè)問(wèn)題我們可以用作用域鏈來(lái)解釋。
當(dāng)執(zhí)行1處代碼調(diào)用outer函數(shù)時(shí),JavaScript引擎會(huì)創(chuàng)建outer函數(shù)執(zhí)行上下文的作用域鏈,這個(gè)作用域鏈包含了outer函數(shù)執(zhí)行時(shí)的活動(dòng)對(duì)象,同時(shí)JavaScript引擎也會(huì)創(chuàng)建一個(gè)閉包,而閉包因?yàn)樾枰L問(wèn)outer函數(shù)的局部變量,因而其作用鏈也會(huì)引用outer的活動(dòng)對(duì)象。這樣,當(dāng)outer函數(shù)執(zhí)行完后,它的作用域?qū)ο笠驗(yàn)橛虚]包的引用而依然存在,固而可以提供給閉包訪問(wèn)。
上述示例中的內(nèi)部函數(shù)雖然有名稱,但在調(diào)用是并沒(méi)有用到這個(gè)名稱,所以內(nèi)部函數(shù)的名稱可以缺省,即可以將內(nèi)部函數(shù)修改為匿名函數(shù),從而簡(jiǎn)化代碼。
示例2: 經(jīng)典閉包問(wèn)題
<!DOCTYPE html>
<html>
<head>
<title>經(jīng)典閉包問(wèn)題</title>
<script type="text/javascript">
window.onload=function () {
var abtn = document.getElementsByTagName("button");
for (var i = 0; i<abtn.length; i++) {
abtn[i].onclick=function(){
alert("按鈕"+(i+1));
}
}
}
</script>
</head>
<body>
<button>按鈕1</button>
<button>按鈕2</button>
<button>按鈕3</button>
</body>
</html>
該示例期望實(shí)現(xiàn)的功能是,單擊每個(gè)按鈕時(shí),在彈出的警告對(duì)話框中顯示相應(yīng)的標(biāo)簽內(nèi)容,即單擊3個(gè)按鈕時(shí)將分別顯示“按鈕1”、“按鈕2”、“按鈕3”。
上述示例頁(yè)面加載完后觸發(fā)窗口加載事件,從而執(zhí)行外層匿名函數(shù),外層匿名函數(shù)執(zhí)行完循環(huán)語(yǔ)句后使活動(dòng)對(duì)象中的局部變量i的值修改為3。外層匿名函數(shù)執(zhí)行完后撤銷(xiāo),但由于其活動(dòng)對(duì)象中的abtn和i變量被內(nèi)層匿名函數(shù)引用,因而外層匿名函數(shù)的活動(dòng)對(duì)象仍然存在堆中供內(nèi)層匿名函數(shù)訪問(wèn)。每執(zhí)行一次循環(huán)都將創(chuàng)建一個(gè)閉包,這些閉包都引用了外層匿名函數(shù)的活動(dòng)對(duì)象,因而訪問(wèn)變量i時(shí)都得到3,這樣最后的結(jié)果是單擊每個(gè)按鈕,在警告對(duì)話框中顯示的文字都是“按鈕4” (i+1=3+1),與期望的功能不一致。造成這個(gè)問(wèn)題的原因是,每個(gè)閉包都引用一個(gè)變量,如果我們使不同的閉包引用不同的變量,就可以實(shí)現(xiàn)輸出的結(jié)果不一樣。這個(gè)需求可使用多種方法實(shí)現(xiàn),在此介紹使用立即調(diào)用函數(shù)表達(dá)式(IIFE)和ES6中的let創(chuàng)建塊即變量的方法。
IIFE指的是:在定義函數(shù)的時(shí)候直接執(zhí)行,即此時(shí)函數(shù)定義變成了一個(gè)函數(shù)調(diào)用的語(yǔ)句。要讓一個(gè)函數(shù)定義語(yǔ)句變成函數(shù)調(diào)用語(yǔ)句,就需要將定義語(yǔ)句變?yōu)橐粋€(gè)函數(shù)表達(dá)式,然后在該表達(dá)式后面再加一對(duì)圓括號(hào)()即可。將函數(shù)定義語(yǔ)句變?yōu)橐粋€(gè)函數(shù)表達(dá)式的最常用方法就是將整個(gè)定義語(yǔ)句放在一對(duì)圓括號(hào)中。
1、IIFE中的函數(shù)為一個(gè)匿名函數(shù)
(function(name){
console.log("hello,"+name);
})("maomin");
JS引擎執(zhí)行上述代碼時(shí),會(huì)調(diào)用匿名,同時(shí)將后面圓括號(hào)中的參數(shù)maomin傳給name虛參,結(jié)果得到:“hello,maomin”。
2、IIFE中的函數(shù)為一個(gè)有名函數(shù)
(function func (name) {
console.log("I am"+name);
})("maomin")
上述代碼跟匿名函數(shù)完全一樣。
示例3: 使用立即調(diào)用函數(shù)表達(dá)式解決經(jīng)典閉包問(wèn)題
<!DOCTYPE html>
<html>
<head>
<title>使用立即調(diào)用表達(dá)式解決經(jīng)典閉包問(wèn)題</title>
<script type="text/javascript">
window.onload=function () {
var abtn = document.getElementsByTagName("button");
for (var i = 0; i<abtn.length; i++) {
(function(num){
abtn[num].onclick=function(){
alert("按鈕"+(num+1));
}
})(i)
}
}
</script>
</head>
<body>
<button>按鈕1</button>
<button>按鈕2</button>
<button>按鈕3</button>
</body>
</html>
上述代碼中第二個(gè)匿名函數(shù)為IIFE,每次調(diào)用該匿名函數(shù)時(shí)將生成一個(gè)對(duì)應(yīng)該函數(shù)的活動(dòng)對(duì)象。該對(duì)象中包含可一個(gè)函數(shù)參數(shù),值為當(dāng)次循環(huán)的循環(huán)變量值。上述示例中,IIFE共執(zhí)行了3次,因而共生成了3個(gè)活動(dòng)對(duì)象,活動(dòng)對(duì)象中包含的參數(shù)值分別為0、1和2,依次對(duì)應(yīng)IIFE的3次執(zhí)行。
每次執(zhí)行IIFE時(shí),將會(huì)產(chǎn)生一個(gè)閉包,該閉包會(huì)引用對(duì)應(yīng)按鈕索引順序執(zhí)行IIFE的活動(dòng)對(duì)象,而閉包引用的活動(dòng)對(duì)象中的參數(shù)值剛好等于按鈕的索引值,因而單擊3個(gè)按鈕將在彈出的警告框中分別顯示"按鈕1"、“按鈕2”、“按鈕3”。
示例4:使用ES6中的let關(guān)鍵字創(chuàng)建塊級(jí)變量解決經(jīng)典閉包問(wèn)題
<!DOCTYPE html>
<html>
<head>
<title>使用ES6中的let關(guān)鍵字解決經(jīng)典閉包問(wèn)題</title>
<script type="text/javascript">
window.onload=function () {
var abtn = document.getElementsByTagName("button");
for (let i = 0; i<abtn.length; i++) {
abtn[i].onclick=function(){
alert("按鈕"+(i+1));
}
}
}
</script>
</head>
<body>
<button>按鈕1</button>
<button>按鈕2</button>
<button>按鈕3</button>
</body>
</html>
上述代碼中循環(huán)變量使用let聲明,因而每次循環(huán)時(shí),都會(huì)產(chǎn)生一個(gè)新的塊級(jí)變量,所以在頁(yè)面加載完,執(zhí)行外層匿名函數(shù)時(shí)產(chǎn)生的活動(dòng)對(duì)象中包含了3個(gè)對(duì)應(yīng)循環(huán)變量的塊級(jí)變量,變量值分為0、1和2。每執(zhí)行一次循環(huán),將會(huì)產(chǎn)生一個(gè)閉包,該閉包中的變量i會(huì)引用外層匿名函數(shù)的活動(dòng)對(duì)象對(duì)應(yīng)按鈕索引的塊級(jí)變量,因而單擊3個(gè)按鈕時(shí)將在彈出的警告對(duì)話框中分別顯示“按鈕1”、“按鈕2”、“按鈕3”。
下一期更精彩
結(jié)語(yǔ)
歡迎關(guān)注我的公眾號(hào),回復(fù)關(guān)鍵詞【電子書(shū)】,即可獲取近十幾本前端熱門(mén)電子書(shū)。更有精品文章等著你哦。
你還可以加我微信,我拉攏了很多IT大佬,創(chuàng)建了一個(gè)技術(shù)交流、文章分享群,歡迎你的加入。
作者:Vam的金豆之路
主要領(lǐng)域:前端開(kāi)發(fā)
我的微信:maomin9761
微信公眾號(hào):前端歷劫之路