JS閉包的理解
一、變量的作用域
二、如何從外部讀取局部變量
三、什么是閉包
四、深入理解閉包
五、閉包的用途
六、使用閉包注意情況
七、JavaScript的垃圾回收機制
八、一些思考題
一、變量作用域
1、要理解閉包,我們先來看一下JavaScript的特殊的變量作用域。
變量的作用域無非是兩種,全局變量和局部變量。
JavaScript語言的獨特之處是:函數(shù)內(nèi)部可以讀取所有的全局變量。
函數(shù)外部無法讀取函數(shù)內(nèi)部的局部變量。
這里有一個地方需要注意,函數(shù)內(nèi)部聲明變量的時候,一定要使用var命令。如果不用的話,你實際上聲明了一個全局變量!
再來看一個例子,理解一下變量作用域
注意:函數(shù)名只是一個標(biāo)識(指向函數(shù)的指針),而才是執(zhí)行函數(shù)
二、如何從外部讀取局部變量
從局部變量讀取外部全局變量是JavaScript語言的特點,但是有時我們需要從外部讀取函數(shù)內(nèi)部的局部變量,那怎么辦呢?解決辦法:在函數(shù)內(nèi)部定義一個函數(shù)。
在函數(shù)f1中定義一個f2函數(shù)。上述代碼中,函數(shù)f2倍包括早函數(shù)f1內(nèi)部,這時候,f1的內(nèi)部局部變量f2都可見,反過來,f2的布局變量,f1不可見。這個就是JavaScript語言特有的“鏈?zhǔn)阶饔糜颉钡慕Y(jié)構(gòu)---子對象會一級級的向上尋找所有的父對象的變量(即,所有的父對象的變量,子對象都是可見的)。
既然f2可以讀取f1中的局部變量,那么只要把f2作為返回值,我們就可以在f1的外部就可以讀取f1內(nèi)部變量了。
三、什么是閉包
閉包就是可以讀取其他函數(shù)內(nèi)部的變量。
由于,JavaScript語言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量,也可以說,閉包就是“定義在一個函數(shù)內(nèi)部的函數(shù)”。
所以,本質(zhì)上說,閉包就是講函數(shù)內(nèi)部很函數(shù)外部連接起來的一座橋梁。
JavaScript中所有的function都是一個閉包。不過一般來說,嵌套的function所產(chǎn)生的閉包更為強大,也是大部分時候我們所謂的“閉包”??聪旅孢@段代碼:
這段代碼有個特點:
1、函數(shù)b嵌套在函數(shù)a內(nèi)部
2、函數(shù)a返回函數(shù)b
執(zhí)行代碼c=a;實際c指向函數(shù)b
再執(zhí)行c;就會彈出一個窗口顯示 i 的值(第一次為1)。
這段代碼其實就是創(chuàng)建了一個閉包,因為函數(shù)a外的變量c引用了函數(shù)a內(nèi)的函數(shù)b,
當(dāng)函數(shù)a 的 內(nèi)部函數(shù)b 被 函數(shù)a 外的 一個變量引用的時候,就創(chuàng)建了一個閉包。
四、深入理解閉包
如果要深入理解閉包和嵌套函數(shù)b的關(guān)系,我們需要引入:函數(shù)的執(zhí)行環(huán)境(excution context)、活動對象c(call object)、作用域(scope)、作用域鏈(scope chain),以函數(shù)a為例,從定義到執(zhí)行過程
1、當(dāng)定義函數(shù)a的時候,js解釋器會將函數(shù)a的作用域鏈(scope chain)設(shè)置成 定義a時a所在的環(huán)境,如果a是一個全局函數(shù),則作用域鏈(scope chain)中只用window對象。
2、當(dāng)執(zhí)行函數(shù)a的時候,a會進(jìn)入到相應(yīng)的執(zhí)行環(huán)境(excution context)。
3、在創(chuàng)建執(zhí)行環(huán)境的過程中,首先會為a添加一個scope屬性,即a的作用域,其值為第1步的作用域鏈(scope chain)。即a.scope=a的作用域鏈。
4、然后執(zhí)行環(huán)境會創(chuàng)建一個活動對象(call object)?;顒訉ο笠彩且粋€擁有屬性的對象,但它不具有原型,而且不能通過JavaScript代碼直接訪問。創(chuàng)建完活動對象后,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動的對象和window對象。
5、下一步是在活動對象上添加一個arguments屬性,它保存著調(diào)用函數(shù)a時所傳遞的參數(shù)。
6、最后把所有的函數(shù)a的形參和內(nèi)部函數(shù)b的引用也添加到a的活動對象上。這一步中,完成了函數(shù)b的定義,因此,如同第3步,函數(shù)b的作用域鏈被設(shè)置成b所定義的環(huán)境,即a的作用域
到此,整個函數(shù)a從定義到執(zhí)行的步驟就完成了。此時a返回函數(shù)b的引用給c,又函數(shù)b的作用域鏈包含鏈包含了對函數(shù)a的活動對象的引用,也就是說b可以訪問a中的定義的所有變量和函數(shù)。函數(shù)b被c引用,函數(shù)b依賴函數(shù)a,一次函數(shù)a在返回后不會被gc回收。
當(dāng)函數(shù)b執(zhí)行的時候也會像以上步驟一樣。因此,執(zhí)行時b的作用域鏈包含3個對象:b的活動對象,a的活動對象和window對象,如下圖所示:
如圖所示,在當(dāng)函數(shù)b中訪問一個變量的時候,搜索順序是:
1、先搜索自身的活動對象,如果存在則返回,如果不存在將繼續(xù)搜索函數(shù)a的活動對象,依次查找,直到找到為止。
2、如果函數(shù)b存在prototype原型對象,則在查找完自身的活動對象后先查找自身的原型對象,再繼續(xù)查找。這就是JavaScript中的變量查找機制。
3、如果整個作用域鏈都無法找到,則返回undefined。
本節(jié)小結(jié),上文提到兩個重要的詞語:函數(shù)的定義與執(zhí)行。文中提到函數(shù)的作用域是在定義函數(shù)時確定的,而不是在執(zhí)行的時候確定的(參看步驟1和3),用一段代碼來說明這個:
這段代碼中,變量h指向了 f 中的那個匿名函數(shù)(由g返回)。
1、假設(shè)函數(shù)h的作用域是在執(zhí)行alert(h)確定的,那么此時h的作用域鏈?zhǔn)牵篽的活動對象-》alert活動對象-》window對象。
2、假設(shè)函數(shù)h的作用域是在定義時候確定的就是說h指向的那個匿名函數(shù)在定義的時候就已經(jīng)確定確定了作用域。那么在執(zhí)行的時候,h的作用域鏈為:h的活動對象-》f的活動對象-》window活動對象。
如果第一種假設(shè)成立,anemia輸出值就是undefined;如果第二種假設(shè)成立,則輸出值為1.
運行結(jié)果證明了第二個假設(shè)是正確的,說明函數(shù)的作用域確實在定義這個函數(shù)的時候就已經(jīng)確定了
五、閉包的用途
接上個例子,
閉包的作用就是在a執(zhí)行完并返回后,閉包使得Javascript的垃圾回收機制GC不會收回a所占用的資源,因為a的內(nèi)部函數(shù)b的執(zhí)行需要依賴a中的變量。由于閉包的存在使得函數(shù)a返回后,a中的i始終存在,這樣每次執(zhí)行c,i都是自加1后alert出i的值。
閉包有很多用途,最大的兩個好處:
(一)讀取函數(shù)內(nèi)部的變量
(二)讓這些變量始終保存在內(nèi)存中。
理解“(二)讓這些變量始終保存在內(nèi)存中”這句話,看一下下面的額代碼。
這段代碼中,result實際上是閉包f2函數(shù)。result一共運行了兩次,第一次是999,第二次是1000.
這說明,函數(shù)f1中的局部變量n一直保存在內(nèi)存中,并沒有在f1調(diào)用后進(jìn)行自動清除。
原因:f1是f2的父函數(shù),而f2被賦給一個全局變量,這導(dǎo)致f2一直在內(nèi)存中,f2依賴于f1,因此f1始終在內(nèi)存中,不會再調(diào)用結(jié)束之后,被垃圾回收機制回收。
注意:nAdd=function{n+=1}這個函數(shù),首先這個函數(shù)沒有用var關(guān)鍵字,因此nAdd是一個全局變量,而不是局部變量。其次,nAdd的值是一個匿名函數(shù),這個匿名函數(shù)本身就是一個閉包,可以在函數(shù)外部對函數(shù)的內(nèi)部今次進(jìn)行操作。
六、使用閉包的注意情況
(1)閉包會是的函數(shù)中的變量倍保存在內(nèi)存中,內(nèi)存消耗大,所以不能濫用閉包。否則會造成網(wǎng)頁性能問題,在IE中可能存在內(nèi)存泄漏,解決辦法,在函數(shù)退出之前,將不使用的局部變量全部刪除。
(2)閉包會在父函數(shù)的外部,改變父函數(shù)變量內(nèi)部的值。所以你把父函數(shù)當(dāng)做對象object使用,把閉包當(dāng)做她的公用方法,把內(nèi)部變量當(dāng)做他的私有屬性,不要隨便改變父函數(shù)內(nèi)部變量的值。
在JavaScript中,如果一個對象不再被引用,那么這個對象就會被GC回收。如果兩個對象相互引用,而不再被第三者所引用,那么這兩個相互引用的對象就會被回收。因為函數(shù)a被b引用,b又被a外的c引用,這就是為什么函數(shù)a執(zhí)行后不會被回收的原因。
八、一些思考題
1、使用對象的閉包使用
2、直接調(diào)用函數(shù)內(nèi)部的函數(shù),報錯,需要使用閉包。
上面的代碼是錯誤的.innerFun的作用域在outerFun內(nèi)部,所在outerFun外部調(diào)用它是錯誤的.
改成下面,就可行了
聯(lián)系客服