對js的廣大初學(xué)者來說芹血,閉包絕對是個難點。而且經(jīng)常出現(xiàn)今天感覺懂了楞慈,明天就又不懂了的情況祟牲。本文就嘗試從我自己的學(xué)習(xí)體會出發(fā),嘗試把這個概念講清楚抖部。
簡單來說说贝,閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。
下面這個函數(shù)是一個根據(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
上面就是一個閉包的例子乡恕。count函數(shù)在執(zhí)行完之后返回了內(nèi)部匿名函數(shù),并賦值給f1和f2俯萎,f1和f2依然可以訪問count函數(shù)中init變量傲宜,f1和f2就是兩個閉包。
要搞清楚其中的細(xì)節(jié)夫啊,我們就必須理解f1和f2在第一次調(diào)用的時候到底發(fā)生了什么函卒。我們首先來看兩個基本觀念:執(zhí)行環(huán)境及作用域。
執(zhí)行環(huán)境及作用域
執(zhí)行環(huán)境
執(zhí)行環(huán)境(execution context撇眯,有時直接簡稱為“環(huán)境”)是ECMAScirpt中最為重要的一個概念报嵌,用來描述js代碼執(zhí)行的抽象概念。執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù)熊榛,決定了它們各自的行為锚国。換句話說,所有的js都是在某個執(zhí)行環(huán)境中運行的玄坦,我們可以把執(zhí)行環(huán)境想成一個執(zhí)行js代碼的盒子血筑。每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的變量對象(variable object),環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中煎楣。
全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境豺总,根據(jù)ECMAScript實現(xiàn)所在的宿主環(huán)境的不同,表示執(zhí)行環(huán)境的對象也不一樣择懂。在Web瀏覽器中喻喳,全局執(zhí)行環(huán)境被認(rèn)為是window對象,因此所有全局變量和函數(shù)都是作為window對象的屬性和方法創(chuàng)建的休蟹。某個執(zhí)行環(huán)境的所有代碼執(zhí)行完畢后沸枯,該環(huán)境被銷毀,保存在其中的所有變量和函數(shù)定義也隨之銷毀赂弓。
每個函數(shù)都有自己的執(zhí)行環(huán)境绑榴。當(dāng)執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境就會被推入到環(huán)境棧中盈魁。而在函數(shù)執(zhí)行之后翔怎,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境杨耙。
作用域鏈
當(dāng)js代碼在一個環(huán)境中執(zhí)行時赤套,會創(chuàng)建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途珊膜,是保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問容握。作用域鏈的前端, 始終是當(dāng)前執(zhí)行代碼所在環(huán)境的變量對象. 如果這個環(huán)境是一個函數(shù), 則將其活動對象(activation object)作為變量對象. 活動對象在最開始時只包含一個變量, 即arguments
對象(這個對象在全局環(huán)境中是不存在的). 作用域鏈中的下一個變量對象來自包含(外部)環(huán)境, 而再下一個變量對象則來自下一個包含環(huán)境. 這樣一直延續(xù)到全局執(zhí)行環(huán)境.
標(biāo)識符解析是沿著作用域鏈一級一級地搜索標(biāo)識符的過程. 搜索過程始終從作用域鏈的前端開始, 然后逐級地向后回溯, 直至找到標(biāo)識符為止(如果找不到標(biāo)識符, 通常導(dǎo)致錯誤發(fā)生)
閉包
我們再來看看我們的demo
function count(init) {
return function() {
init++;
return init;
}
}
var f1 = count(1);
console.log(f1()); //2
console.log(f1()); //3
f1之所以還能訪問 變量 init, 是因為f1函數(shù)的作用域鏈包含 count函數(shù)的作用域.
下面是最關(guān)鍵的部分:
- 在創(chuàng)建count()函數(shù)時,會創(chuàng)建一個預(yù)先包含全局變量對象的作用域鏈车柠,這個作用域鏈被保存在內(nèi)部的[[Scope]]屬性中剔氏。
- 當(dāng)調(diào)用count()函數(shù)時,會為函數(shù)創(chuàng)建一個執(zhí)行環(huán)境竹祷,然后通過復(fù)制函數(shù)的[[Scope]]屬性中的對象構(gòu)建起執(zhí)行環(huán)境的作用域鏈. 此后, count()函數(shù)的活動對象被創(chuàng)建, 并被推入到執(zhí)行環(huán)境作用域鏈的前端.
- 在count()函數(shù)內(nèi)部的匿名函數(shù)會將count()函數(shù)的執(zhí)行環(huán)境的作用域鏈初始化成自己的作用域鏈中. 這樣匿名函數(shù)就可以訪問count()函數(shù)中的所有變量了.
- 當(dāng)count()函數(shù)中的匿名函數(shù)最終返回并賦值給f1, f1的作用域鏈就包含全局變量對象和count()函數(shù)的活動對象, 所以count()函數(shù)的活動對象不會被銷毀. 換句話說, count()函數(shù)執(zhí)行完畢后, count()函數(shù)的執(zhí)行環(huán)境被銷毀, 但是count()函數(shù)的活動對象直到f1被銷毀后, 才會被銷毀.
到這里我們就明白了, 只要你在一個函數(shù)內(nèi)部定義了另一個函數(shù), 閉包就產(chǎn)生了.
this對象
在閉包中使用this對象會遇到一些問題. 我們知道this對象指向了當(dāng)前代碼的執(zhí)行環(huán)境. 也就是說, 在全局環(huán)境中this等于window(瀏覽器環(huán)境), 當(dāng)被當(dāng)做某個對象的方法調(diào)用時, this指向的就是那個方法.
當(dāng)然, 也可以通過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()());
這時候return回來的是"The Window", 而不是"My Object"
我們分解一下來看:
- object.getNameFunc()執(zhí)行時, getNameFunc()是作為object的方法執(zhí)行的, this指向object, 然后返回一個匿名函數(shù).
- 這個匿名函數(shù)在調(diào)用的時候, 實際上是在全局環(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變量中, 這樣閉包也可以訪問that變量.
模仿塊級作用域
我們知道Javascript中沒有塊級作用域, 也就是定義塊中變量, 它的作用域是當(dāng)前函數(shù), 和塊沒有關(guān)系. 我們可以利用函數(shù)的作用域來模仿塊級作用域.
!function() {
var i = 10;
console.log(i); //10
}();
console.log(i+1); //i is not defined
我們創(chuàng)建了一個函數(shù)并立即調(diào)用它, 這樣其中的代碼執(zhí)行了, 而且因為函數(shù)執(zhí)行完畢, 它的執(zhí)行環(huán)境和其中的變量對象都會被銷毀, 所以下面的代碼提示i is not defined
封裝
面向?qū)ο蟮娜蠡痪褪欠庋b. 封裝簡單來說就是只公開代碼單元的對外接口, 而隱藏內(nèi)部的具體實現(xiàn).
Javascript是面向?qū)ο蟮恼Z言, 那它如何實現(xiàn)封裝呢? 我們知道Javascript中沒有私有成員的概念, 所有對象的屬性都是公開的. 但是呢, Javascript有私有變量的概念, 函數(shù)內(nè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
例子是個銀行賬戶對象, 對外公開了存錢和取錢兩種操作. 這里用工廠模式來創(chuàng)建對象, 用構(gòu)造函數(shù)也是同樣的道理. 我們把有權(quán)訪問私有變量和方法的公有方法成為特權(quán)方法(Save和Draw方法)
呼呼, 好像我想說的都說完了, 下面開始一分鐘滿分作文時間, 來回顧一下我們都學(xué)到了什么:
- 當(dāng)在函數(shù)內(nèi)部定義了其他函數(shù)時, 就創(chuàng)建了閉包. 閉包有權(quán)訪問函數(shù)內(nèi)部的所有變量.
-閉包的作用域鏈, 包含著自己的作用域, 包含函數(shù)的作用域和全局的作用域
-通常, 函數(shù)的作用域和變量會在函數(shù)調(diào)用結(jié)束后銷毀.
-但是, 當(dāng)函數(shù)返回了閉包時, 函數(shù)的作用域會一直保存直到閉包不存在為止 - 創(chuàng)建并立即調(diào)用函數(shù)可以模仿塊級作用域
- 閉包可以實現(xiàn)封裝