對(duì)js的廣大初學(xué)者來(lái)說(shuō),閉包絕對(duì)是個(gè)難點(diǎn)。而且經(jīng)常出現(xiàn)今天感覺(jué)懂了,明天就又不懂了的情況。本文就嘗試從我自己的學(xué)習(xí)體會(huì)出發(fā),嘗試把這個(gè)概念講清楚。
簡(jiǎn)單來(lái)說(shuō),閉包是指有權(quán)訪(fǎng)問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)。
下面這個(gè)函數(shù)是一個(gè)根據(jù)初始值自加的函數(shù)。
function count(init) {
return function() {
init++;
return init;
}
}
var f1 = count(1);
console.log(f1()); //2
console.log(f1()); //3
var f2 = count(11);
console.log(f2()); //12
console.log(f2()); //13
上面就是一個(gè)閉包的例子。count函數(shù)在執(zhí)行完之后返回了內(nèi)部匿名函數(shù),并賦值給f1和f2,f1和f2依然可以訪(fǎng)問(wèn)count函數(shù)中init變量,f1和f2就是兩個(gè)閉包。
要搞清楚其中的細(xì)節(jié),我們就必須理解f1和f2在第一次調(diào)用的時(shí)候到底發(fā)生了什么。我們首先來(lái)看兩個(gè)基本觀(guān)念:執(zhí)行環(huán)境及作用域。
執(zhí)行環(huán)境及作用域
執(zhí)行環(huán)境
執(zhí)行環(huán)境(execution context,有時(shí)直接簡(jiǎn)稱(chēng)為“環(huán)境”)是ECMAScirpt中最為重要的一個(gè)概念,用來(lái)描述js代碼執(zhí)行的抽象概念。執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪(fǎng)問(wèn)的其他數(shù)據(jù),決定了它們各自的行為。換句話(huà)說(shuō),所有的js都是在某個(gè)執(zhí)行環(huán)境中運(yùn)行的,我們可以把執(zhí)行環(huán)境想成一個(gè)執(zhí)行js代碼的盒子。每個(gè)執(zhí)行環(huán)境都有一個(gè)與之關(guān)聯(lián)的變量對(duì)象(variable object),環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對(duì)象中。
全局執(zhí)行環(huán)境是最外圍的一個(gè)執(zhí)行環(huán)境,根據(jù)ECMAScript實(shí)現(xiàn)所在的宿主環(huán)境的不同,表示執(zhí)行環(huán)境的對(duì)象也不一樣。在Web瀏覽器中,全局執(zhí)行環(huán)境被認(rèn)為是window對(duì)象,因此所有全局變量和函數(shù)都是作為window對(duì)象的屬性和方法創(chuàng)建的。某個(gè)執(zhí)行環(huán)境的所有代碼執(zhí)行完畢后,該環(huán)境被銷(xiāo)毀,保存在其中的所有變量和函數(shù)定義也隨之銷(xiāo)毀。
每個(gè)函數(shù)都有自己的執(zhí)行環(huán)境。當(dāng)執(zhí)行流進(jìn)入一個(gè)函數(shù)時(shí),函數(shù)的環(huán)境就會(huì)被推入到環(huán)境棧中。而在函數(shù)執(zhí)行之后,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境。
作用域鏈
當(dāng)js代碼在一個(gè)環(huán)境中執(zhí)行時(shí),會(huì)創(chuàng)建變量對(duì)象的一個(gè)作用域鏈(scope chain)。作用域鏈的用途,是保證對(duì)執(zhí)行環(huán)境有權(quán)訪(fǎng)問(wèn)的所有變量和函數(shù)的有序訪(fǎng)問(wèn)。作用域鏈的前端, 始終是當(dāng)前執(zhí)行代碼所在環(huán)境的變量對(duì)象. 如果這個(gè)環(huán)境是一個(gè)函數(shù), 則將其活動(dòng)對(duì)象(activation object)作為變量對(duì)象. 活動(dòng)對(duì)象在最開(kāi)始時(shí)只包含一個(gè)變量, 即arguments對(duì)象(這個(gè)對(duì)象在全局環(huán)境中是不存在的). 作用域鏈中的下一個(gè)變量對(duì)象來(lái)自包含(外部)環(huán)境, 而再下一個(gè)變量對(duì)象則來(lái)自下一個(gè)包含環(huán)境. 這樣一直延續(xù)到全局執(zhí)行環(huán)境.
標(biāo)識(shí)符解析是沿著作用域鏈一級(jí)一級(jí)地搜索標(biāo)識(shí)符的過(guò)程. 搜索過(guò)程始終從作用域鏈的前端開(kāi)始, 然后逐級(jí)地向后回溯, 直至找到標(biāo)識(shí)符為止(如果找不到標(biāo)識(shí)符, 通常導(dǎo)致錯(cuò)誤發(fā)生)
閉包
我們?cè)賮?lái)看看我們的demo
function count(init) {
return function() {
init++;
return init;
}
}
var f1 = count(1);
console.log(f1()); //2
console.log(f1()); //3
f1之所以還能訪(fǎng)問(wèn) 變量 init, 是因?yàn)閒1函數(shù)的作用域鏈包含 count函數(shù)的作用域.
下面是最關(guān)鍵的部分:
- 在創(chuàng)建count()函數(shù)時(shí),會(huì)創(chuàng)建一個(gè)預(yù)先包含全局變量對(duì)象的作用域鏈,這個(gè)作用域鏈被保存在內(nèi)部的[[Scope]]屬性中。
- 當(dāng)調(diào)用count()函數(shù)時(shí),會(huì)為函數(shù)創(chuàng)建一個(gè)執(zhí)行環(huán)境,然后通過(guò)復(fù)制函數(shù)的[[Scope]]屬性中的對(duì)象構(gòu)建起執(zhí)行環(huán)境的作用域鏈. 此后, count()函數(shù)的活動(dòng)對(duì)象被創(chuàng)建, 并被推入到執(zhí)行環(huán)境作用域鏈的前端.
- 在count()函數(shù)內(nèi)部的匿名函數(shù)會(huì)將count()函數(shù)的執(zhí)行環(huán)境的作用域鏈初始化成自己的作用域鏈中. 這樣匿名函數(shù)就可以訪(fǎng)問(wèn)count()函數(shù)中的所有變量了.
- 當(dāng)count()函數(shù)中的匿名函數(shù)最終返回并賦值給f1, f1的作用域鏈就包含全局變量對(duì)象和count()函數(shù)的活動(dòng)對(duì)象, 所以count()函數(shù)的活動(dòng)對(duì)象不會(huì)被銷(xiāo)毀. 換句話(huà)說(shuō), count()函數(shù)執(zhí)行完畢后, count()函數(shù)的執(zhí)行環(huán)境被銷(xiāo)毀, 但是count()函數(shù)的活動(dòng)對(duì)象直到f1被銷(xiāo)毀后, 才會(huì)被銷(xiāo)毀.
到這里我們就明白了, 只要你在一個(gè)函數(shù)內(nèi)部定義了另一個(gè)函數(shù), 閉包就產(chǎn)生了.
this對(duì)象
在閉包中使用this對(duì)象會(huì)遇到一些問(wèn)題. 我們知道this對(duì)象指向了當(dāng)前代碼的執(zhí)行環(huán)境. 也就是說(shuō), 在全局環(huán)境中this等于window(瀏覽器環(huán)境), 當(dāng)被當(dāng)做某個(gè)對(duì)象的方法調(diào)用時(shí), this指向的就是那個(gè)方法.
當(dāng)然, 也可以通過(guò)apply()和call()改變函數(shù)的執(zhí)行環(huán)境
我們看一下下面的例子:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function () {
return function () {
return this.name;
};
}
};
console.log(object.getNameFunc()());
這時(shí)候return回來(lái)的是"The Window", 而不是"My Object"
我們分解一下來(lái)看:
- object.getNameFunc()執(zhí)行時(shí), getNameFunc()是作為object的方法執(zhí)行的, this指向object, 然后返回一個(gè)匿名函數(shù).
- 這個(gè)匿名函數(shù)在調(diào)用的時(shí)候, 實(shí)際上是在全局環(huán)境中執(zhí)行的, 所以this指向全局環(huán)境, 返回this.name就是"The Window"
如果我們想返回"My Object"該咋辦? 那我們就得想著怎么把第一步中的this傳到第二步的匿名函數(shù)中.
getNameFunc : function () {
var that = this;
return function () {
return that.name;
};
}
在定義匿名函數(shù)前, 我們把this保存在that變量中, 這樣閉包也可以訪(fǎng)問(wèn)that變量.
模仿塊級(jí)作用域
我們知道Javascript中沒(méi)有塊級(jí)作用域, 也就是定義塊中變量, 它的作用域是當(dāng)前函數(shù), 和塊沒(méi)有關(guān)系. 我們可以利用函數(shù)的作用域來(lái)模仿塊級(jí)作用域.
!function() {
var i = 10;
console.log(i); //10
}();
console.log(i+1); //i is not defined
我們創(chuàng)建了一個(gè)函數(shù)并立即調(diào)用它, 這樣其中的代碼執(zhí)行了, 而且因?yàn)楹瘮?shù)執(zhí)行完畢, 它的執(zhí)行環(huán)境和其中的變量對(duì)象都會(huì)被銷(xiāo)毀, 所以下面的代碼提示i is not defined
封裝
面向?qū)ο蟮娜蠡痪褪欠庋b. 封裝簡(jiǎn)單來(lái)說(shuō)就是只公開(kāi)代碼單元的對(duì)外接口, 而隱藏內(nèi)部的具體實(shí)現(xiàn).
Javascript是面向?qū)ο蟮恼Z(yǔ)言, 那它如何實(shí)現(xiàn)封裝呢? 我們知道Javascript中沒(méi)有私有成員的概念, 所有對(duì)象的屬性都是公開(kāi)的. 但是呢, Javascript有私有變量的概念, 函數(shù)內(nèi)部的變量外部是無(wú)法訪(fǎng)問(wèn)的. 這里, 我們就可以利用閉包來(lái)完成封裝.
function Account() {
var balance = 0;
function save(money){
balance += money;
query();
}
function draw(money){
if(money > balance){
balance = 0;
}
else{
balance -= money;
}
query();
}
function query(){
console.log("Your balance is " + balance);
}
return {
Save : function(money){
save(money);
},
Draw : function(money){
draw(money);
}
}
}
var acount = new Account();
acount.Save(10);
acount.Draw(5);
acount.save(10); //save is not a function
console.log(acount.balance); //undefined
例子是個(gè)銀行賬戶(hù)對(duì)象, 對(duì)外公開(kāi)了存錢(qián)和取錢(qián)兩種操作. 這里用工廠(chǎng)模式來(lái)創(chuàng)建對(duì)象, 用構(gòu)造函數(shù)也是同樣的道理. 我們把有權(quán)訪(fǎng)問(wèn)私有變量和方法的公有方法成為特權(quán)方法(Save和Draw方法)
呼呼, 好像我想說(shuō)的都說(shuō)完了, 下面開(kāi)始一分鐘滿(mǎn)分作文時(shí)間, 來(lái)回顧一下我們都學(xué)到了什么:
- 當(dāng)在函數(shù)內(nèi)部定義了其他函數(shù)時(shí), 就創(chuàng)建了閉包. 閉包有權(quán)訪(fǎng)問(wèn)函數(shù)內(nèi)部的所有變量.
-閉包的作用域鏈, 包含著自己的作用域, 包含函數(shù)的作用域和全局的作用域
-通常, 函數(shù)的作用域和變量會(huì)在函數(shù)調(diào)用結(jié)束后銷(xiāo)毀.
-但是, 當(dāng)函數(shù)返回了閉包時(shí), 函數(shù)的作用域會(huì)一直保存直到閉包不存在為止 - 創(chuàng)建并立即調(diào)用函數(shù)可以模仿塊級(jí)作用域
- 閉包可以實(shí)現(xiàn)封裝