談?wù)凧avaScript中的函數(shù)與閉包
閉包這東西,說難也難,說不難也不難,下面我就以自己的理解來說一下閉包
一、閉包的解釋說明
對于函數(shù)式語言來說,函數(shù)可以保存內(nèi)部的數(shù)據(jù)狀態(tài)。對于像C#這種編譯型命令式語言來說,由于代碼總是在代碼段中執(zhí)行,而代碼段是只讀的,因此函數(shù)中的數(shù)據(jù)只能是靜態(tài)數(shù)據(jù)。函數(shù)內(nèi)部的局部變量存放在棧上,在函數(shù)執(zhí)行結(jié)束以后,所占用的棧被釋放,因此局部變量是不能保存的。
Javascript采用詞法作用域,函數(shù)的執(zhí)行依賴于變量作用域,這個(gè)作用域是在定義函數(shù)時(shí)確定的。因此Javascript中函數(shù)對象不僅保存代碼邏輯,還必須引用當(dāng)前的作用域鏈。Javascript中函數(shù)內(nèi)部的局部變量可以被修改,而且當(dāng)再次進(jìn)入到函數(shù)內(nèi)部的時(shí)候,上次被修改的狀態(tài)仍然持續(xù)。這是因?yàn)橐驗(yàn)榫植孔兞坎⒉槐4嬖跅I?,而是通過一個(gè)對象來保存。
決定使用哪個(gè)變量是由作用域鏈決定的,每次生成函數(shù)實(shí)例時(shí),都會為之創(chuàng)建一個(gè)對象用來保存局部變量,并且把這個(gè)用于保存局部變量的對象加入作用域鏈中。不同函數(shù)對象可以通過作用域鏈關(guān)聯(lián)起來。Javascript中所有函數(shù)都是閉包,我們不能避免“產(chǎn)生”閉包。
引用一張《Javascript高級程序設(shè)計(jì)》中的圖來說明,雖然這張圖并不完全說明所有情況。圖中的activation object就是用于保存變量的對象。
簡而言之,在Javascript中:
閉包:函數(shù)實(shí)例保存著在執(zhí)行時(shí)所需要的變量的引用,而不會復(fù)制保存當(dāng)時(shí)變量的值。(在Object C的實(shí)現(xiàn)中,我們可以選擇保存當(dāng)時(shí)的值或者是引用)
作用域鏈:解析變量時(shí)查找變量所在的方式,以var作為終止符號,如果鏈上一直沒有var,則一直追溯到全局對象為止。
C#中的閉包特性是由編譯器把局部變量轉(zhuǎn)換成引用類型的對象成員實(shí)現(xiàn)的。
二、閉包的使用
下面通過一些具體例子來說明如何利用閉包這一特性:
1.閉包是在定義的時(shí)候產(chǎn)生的
function Foo(){ function A(){} function B(){} function C(){}}
我們每次執(zhí)行Foo()的時(shí)候,都有有A,B,C這三個(gè)函數(shù)實(shí)例(閉包)產(chǎn)生,當(dāng)Foo執(zhí)行完畢,生成的實(shí)例沒有其他引用,因此會被當(dāng)成垃圾隨之銷毀(不一定是馬上銷毀)。
我們來證實(shí)一下作用域鏈?zhǔn)窃诤瘮?shù)定義時(shí)確定的,所以這里顯示的應(yīng)該是'local scope'
var scope = "global scope"; function checkscope() { var scope = "local scope"; function f() { return scope; } return f;}checkscope()()
同樣道理:
(function(){ function A(){} function B(){} function C(){}}())
上面的表達(dá)式執(zhí)行完后也會有A,B,C這三個(gè)函數(shù)實(shí)例(閉包)產(chǎn)生,因?yàn)檫@是一個(gè)立即執(zhí)行的匿名函數(shù),這三個(gè)閉包只能產(chǎn)生一次。生成的閉包沒有其他引用,因此會被當(dāng)成垃圾隨之銷毀(不一定是馬上銷毀)。
我們之所以這么寫,目地有兩個(gè)
1.避免污染全局對象
2.避免多次產(chǎn)生相同的函數(shù)實(shí)例
對比下面兩個(gè)例子,閉包是如何保存作用域鏈的:
function A(){} //比較省內(nèi)存的寫法,創(chuàng)建對象速度快,開銷小 (function(prototype){ var name = "a"; function sayName () { alert(name); } function ChangeName() { name += "_changed" } prototype.sayName = sayName;//引用通過執(zhí)行匿名函數(shù)產(chǎn)生的閉包,閉包只會產(chǎn)生一次 prototype.changeName = ChangeName; }(A.prototype)) var a1 = new A(); var a2 = new A();
a1.sayName(); a1.changeName(); a2.sayName();
--------------------------------------------------------------------------------
function B(){ //原型鏈比較短的做法,找到方法的速度快,但是比較耗內(nèi)存,每次new 調(diào)用構(gòu)造器都有2個(gè)函數(shù)實(shí)例和1個(gè)變量產(chǎn)生。 var name = "b"; function sayName () { alert(name); } function changeName() { name += "_changed"; } this.sayName = sayName;//引用閉包,每次調(diào)用函數(shù)B都會產(chǎn)生新的閉包 this.changeName = changeName; }//如果函數(shù)調(diào)用之前帶有new關(guān)鍵字,則函數(shù)作為構(gòu)造器使用。//本質(zhì)上來說作為構(gòu)造器和作為普通函數(shù)調(diào)用沒區(qū)別。如果直接調(diào)用B(),那么this對象會綁定到全局對象,新生成的閉包會代替舊的閉包賦給全局對象的changeName和sayName屬性上,因此舊的閉包會被當(dāng)成垃圾回收。//如果作為構(gòu)造器使用,new 關(guān)鍵字會生成一個(gè)新的對象(this指向這個(gè)新對象)并初始化這個(gè)新對象的sayName和changeName屬性,因此每次生成的閉包都會因?yàn)橛幸枚A粝聛怼?var b1 = new B(); b1.sayName(); b1.changeName(); b1.sayName(); var b2 = new B(); b2.sayName(); b1.sayName();
三、泄漏問題:在編譯語言中,函數(shù)體總在文件的代碼段中,并在運(yùn)行期被裝入標(biāo)志為可執(zhí)行的內(nèi)存區(qū)。事實(shí)上我們不認(rèn)為函數(shù)自身會有生命周期。我們在大多數(shù)情況下會認(rèn)為“引用類型的數(shù)據(jù)結(jié)構(gòu)”具有生存周期和泄漏的問題,如指針、對象等。
JavaScript中內(nèi)存的泄漏本質(zhì)上就是定義函數(shù)時(shí)生成的保存局部變量的對象因?yàn)榇嬖谝枚槐划?dāng)成垃圾被回收。
1.存在循環(huán)引用
2.有些對象總不能銷毀,如IE6在DOM中的內(nèi)存泄漏,或者在銷毀時(shí)不能通知到Javascript引擎,因此也就有些Javascript閉包總不能被銷毀。這些情況通常是發(fā)生在Javascript宿主對象和Javascript中原生對象溝通不暢導(dǎo)致。
相關(guān)文章
javascript學(xué)習(xí)筆記(十一) 正則表達(dá)式介紹
javascript學(xué)習(xí)筆記之正則表達(dá)式介紹,需要的朋友可以參考下2012-06-06javascript之typeof、instanceof操作符使用探討
typeof和instanceof這兩個(gè)操作符時(shí)不時(shí)就會用到,堪稱必用,這兩個(gè)操作符或許是javascript中最大的設(shè)計(jì)缺陷,因?yàn)閹缀醪豢赡軓乃麄兡抢锏玫较胍慕Y(jié)果2013-05-05JavaScript高級程序設(shè)計(jì)(第3版)學(xué)習(xí)筆記2 js基礎(chǔ)語法
這一篇復(fù)習(xí)一下ECMAScript規(guī)范中的基礎(chǔ)語法,英文好的朋友可以直接閱讀官方文檔。JavaScript本質(zhì)上也是一種類C語言,熟悉C語言的朋友,可以非常輕松的閱讀這篇文章,甚至都可以跳過,不過建議你最好還是看一看,在介紹的同時(shí),我可能會引用一些自認(rèn)為不易理解且比較流行的用法。2012-10-10