什么是閉包
- 了解什么是閉包之前浮定,要先了解變量的作用域征讲,變量的作用域無非就是兩種:全局變量和局部變量。
Javascript語言的特殊之處欺殿,就在于函數(shù)內(nèi)部可以直接讀取全局變量寄纵。
var n=999;
function f1(){
alert(n);
}
f1(); // 999
另一方面,在函數(shù)外部自然無法讀取函數(shù)內(nèi)的局部變量脖苏。
function f1(){
var n=999;
}
alert(n); // error
這里有一個地方需要注意程拭,函數(shù)內(nèi)部聲明變量的時候,一定要使用var命令棍潘。如果不用的話恃鞋,你實際上聲明了一個全局變量!
function f1(){
n=999;
}
f1();
alert(n); // 999
- 如何從外部讀取局部變量
出于種種原因亦歉,我們有時候需要得到函數(shù)內(nèi)的局部變量恤浪。但是,前面已經(jīng)說過了鳍徽,正常情況下资锰,這是辦不到的,只有通過變通方法才能實現(xiàn)阶祭。
那就是在函數(shù)的內(nèi)部绷杜,再定義一個函數(shù)。
function f1(){
var n=999;
function f2(){ alert(n); // 999 }
}
在上面的代碼中濒募,函數(shù)f2就被包括在函數(shù)f1內(nèi)部鞭盟,這時f1內(nèi)部的所有局部變量,對f2都是可見的瑰剃。但是反過來就不行齿诉,f2內(nèi)部的局部變量,對f1就是不可見的。這就是Javascript語言特有的"鏈?zhǔn)阶饔糜?結(jié)構(gòu)(chain scope)粤剧,子對象會一級一級地向上尋找所有父對象的變量歇竟。所以,父對象的所有變量抵恋,對子對象都是可見的焕议,反之則不成立。
既然f2可以讀取f1中的局部變量弧关,那么只要把f2作為返回值盅安,我們不就可以在f1外部讀取它的內(nèi)部變量了嗎!
function f1(){
var n=999;
function f2(){ alert(n); }
return f2;
}
var result=f1();
result(); // 999
上一節(jié)代碼中的f2函數(shù)世囊,就是閉包别瞭。
各種專業(yè)文獻上的"閉包"(closure)定義非常抽象,很難看懂株憾。我的理解是蝙寨,閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。
由于在Javascript語言中号胚,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量籽慢,因此可以把閉包簡單理解成"定義在一個函數(shù)內(nèi)部的函數(shù)"。
所以猫胁,在本質(zhì)上箱亿,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁。
- 我們來看這么一段代碼
function add(){
var i = 0;
return function(){
alert(i++);
}
}
var f = add();
f();
f();
首先我們從作用域的角度來描述它弃秆,我們先定義一個全局的作用域届惋,然后我們看add的詞法環(huán)境。
當(dāng)我們執(zhí)行add的時候菠赚,它實際上返回了一個函數(shù)脑豹,這個函數(shù)對象在JS里面怎么描述?
我們知道衡查,當(dāng)一個函數(shù)創(chuàng)建的時候瘩欺,在引擎內(nèi)部會創(chuàng)建一個函數(shù)對象,這個函數(shù)里面會包含形參拌牲,函數(shù)體的內(nèi)容以及會保存當(dāng)前的應(yīng)用環(huán)境俱饿。
也就是說,當(dāng)我們返回的這個函數(shù)塌忽,它會保存當(dāng)前add的作用域環(huán)境拍埠,即它的scope會指向add environment。
當(dāng)我們執(zhí)行f函數(shù)的時候土居,首先也會創(chuàng)建這個函數(shù)的詞法作用域枣购,我們暫且成為closure environment嬉探。同時,我們也會執(zhí)行i++的操作棉圈。此時我們需要尋找變量i涩堤,實際上就在它的外層(outer)的作用域里面,也就說分瘾,當(dāng)我們執(zhí)行i++的時候定躏,它順著作用域鏈找到了add environment里面的i,找到之后給它加了1芹敌。
同理,繼續(xù)執(zhí)行f后垮抗,i就變成2氏捞。
一般來說,函數(shù)執(zhí)行完以后冒版,內(nèi)部的局部變量會被釋放掉液茎,等同于不存在了,但是JS里面比較特殊辞嗡,它允許函數(shù)作為返回值捆等,此時函數(shù)有可能引用外部變量,這時它就保存了對外部詞法環(huán)境的引用续室,這就是閉包的特性栋烤。
- 總結(jié)如下
- 閉包由函數(shù)和其相關(guān)的引用環(huán)境的組合而成
- 閉包允許函數(shù)訪問其引用環(huán)境中的變量(又稱為自由變量)
- 廣義上來說,所有JS函數(shù)都可以稱為閉包挺狰,因為JS
- 函數(shù)在創(chuàng)建時保存了當(dāng)前的詞法環(huán)境
閉包的應(yīng)用-保存變量現(xiàn)場
-
我們首先來看這段代碼
這段代碼是用來做事件注冊明郭,在addHandeler里面呢,它會為一組元素去注冊onclick事件丰泊,當(dāng)onclick觸發(fā)的時候薯定,它可以把元素索引打出來,即希望當(dāng)我點到第零個元素的時候瞳购,alert出來0话侄,第一個元素的時候,alert出來1学赛。
我們還是從作用域開始年堆,我們看到onclick這里的i其實是addHandler里面的i,不管循環(huán)體執(zhí)行多少次罢屈,onclick產(chǎn)生的函數(shù)訪問的i都是最外層的i嘀韧,也就是說,當(dāng)一個函數(shù)改變里面i的值缠捌,i的值就會跟這邊锄贷,那么最后我們?nèi)ピL問i的時候译蒂,i的值就已經(jīng)變了,因為這個循環(huán)在不斷的對i++谊却,最后我們onclick的時候得到的i值柔昼,會變成node.length而不是01234。 -
我們再來看這段代碼
這里我們把node[i].onclick賦給了helper函數(shù)炎辨,helper返回了一個函數(shù)捕透,可以看出helper是閉包的作用,i是形參碴萧,可以被內(nèi)部函數(shù)訪問乙嘀,也就是可以被里面的函數(shù)所訪問,helper都會產(chǎn)生閉包保存當(dāng)前的值破喻,當(dāng)我們執(zhí)行函數(shù)的時候虎谢,就會彈出當(dāng)時傳進去的值,從而到達保存變量現(xiàn)場的目的
閉包的應(yīng)用-封裝
-
做信息隱藏曹质,有些變量我們不能暴露給外部婴噩,或者說,我們通過自執(zhí)行函數(shù)+閉包方法減少一些不必要的全局變量羽德,但是同時又能夠使這些變量在各自的自執(zhí)行函數(shù)內(nèi)有效几莽。
- 這里我們observer函數(shù)里面返回了一組屬性,這些屬性都是接口宅静,正常來說我們是通過外部是訪問不到observer里面的observerList這個變量的章蚣,然而我們可以通過這些接口來訪問observerList這個變量
使用閉包的注意點
- 由于閉包會使得函數(shù)中的變量都被保存在內(nèi)存中,內(nèi)存消耗很大坏为,所以不能濫用閉包究驴,否則會造成網(wǎng)頁的性能問題,在IE中可能導(dǎo)致內(nèi)存泄露匀伏。解決方法是洒忧,在退出函數(shù)之前,將不使用的局部變量全部刪除够颠。
- 閉包會在父函數(shù)外部熙侍,改變父函數(shù)內(nèi)部變量的值。所以履磨,如果你把父函數(shù)當(dāng)作對象(object)使用蛉抓,把閉包當(dāng)作它的公用方法(Public Method),把內(nèi)部變量當(dāng)作它的私有屬性(private value)剃诅,這時一定要小心巷送,不要隨便改變父函數(shù)內(nèi)部變量的值。
思考題
如果你能理解下面兩段代碼的運行結(jié)果矛辕,應(yīng)該就算理解閉包的運行機制了笑跛。
- 代碼片段一
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
- 代碼片段二
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());