JS中的閉包(closure)
閉包(closure)是Javascript語言的一個難點,也是它的特色,很多高級應用都要依靠閉包實現。
下面就是我的學習筆記,對於Javascript初學者應該是很有用的。
一.什么是閉包
JS中,在函數內部可以讀取函數外部的變量
function outer(){ var localVal = 30; return localVal; } outer();//30
但,在函數外部自然無法讀取函數內的局部變量
function outer(){ var localVal = 30; } alert(localVal);//error
這里有個需要注意的地方,函數內部聲明變量的時候,一定要使用var命令。如果不用的話,實際上是聲明了一個全局變量。
function outer(){ localVal = 30; return localVal; } outer();
alert(localVal);//30
以上的表述,是JS變量的作用域的知識,它包括全局變量和局部變量。
Javascript語言的特殊之處,就在於函數內部可以直接讀取全局變量。
function outer(){ var localVal = 30;
function inner(){ alert(localVal); }
return inner; } var func = outer(); func();//30
我們看到在上面的代碼中,outer函數內又定義一個函數inner,outer函數的返回值是inner函數,inner函數把localVal alert出來。
我們可以看出以上代碼的特點:函數嵌套函數,內部函數可以引用外部函數的參數和變量,參數和變量不會被垃圾回收機制收回。
代碼中的inner函數,就是閉包。簡單的說,閉包(closure)就是能夠讀取其他函數內部變量的函數。
由於在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成“定義在一個函數內部的函數”。所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
在上面的代碼中,函數inner被包含在函數outer內部,這時outer內部的所有局部變量,對inner都是可見的。但是inner內部的局部變量,對oute 是不可見的。這是Javascript語言特有的“鏈式作用域”結構(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。
補充--JS中的函數定義
JS中定義一個函數,最常用的就是函數聲明和函數表達式
Js中的函數聲明是指下面的形式:
function functionName(){ }
函數表達式則是類似表達式那樣來聲明一個函數:
var functionName = function(){ }
我們可以使用函數表達式創建一個函數並馬上執行它,如:
(function() { var a, b // local variables // ... // and the code })()
()();第一個括號里放一個無名的函數。
二者區別:js的解析器對函數聲明與函數表達式並不是一視同仁地對待的。對於函數聲明,js解析器會優先讀取,確保在所有代碼執行之前聲明已經被解析,而函數表達式,如同定義其它基本類型的變量一樣,只在執行到某一句時也會對其進行解析,所以在實際中,它們還是會有差異的,具體表現在,當使用函數聲明的形式來定義函數時,可將調用語句寫在函數聲明之前,而后者,這樣做的話會報錯。
二.閉包的應用
使用閉包的好處:
-希望一個變量長期駐扎在內存當中;
-避免全局變量的污染;
-私有成員的存在
1.模塊化代碼
使用自執行的匿名函數來模擬塊級作用域
(function(){ // 這里為塊級作用域 })();
該方法經常在全局作用域中被用在函數外部,從而限制向全局作用域中添加過多的變量和函數影響全局作用域。也可以減少如閉包這樣的對內存的占用,由於匿名函數沒有變量指向,執行完畢就可以立即銷毀其作用域鏈。
示例:
var test = (function(){ var a= 1; return function(){ a++; alert(a); } })(); test();//2 test();//3
實現a的自加,不污染全局。
2.循環閉包
循環給每個li注冊一個click事件,點擊alert序號。代碼如下:
var aLi = document.getElementByClassName("test"); function showAllNum( aLi ){ for( var i =0,len = aLi.length ;i<len;i++ ){ aLi[i].onclick = function(){ alert( i );//all are aLi.length! } } }
點擊后會一直彈出同一個值 aLi.length 而不是123。當點擊之前,循環已經結束,i值為aLi.length。
利用閉包,建一個匿名函數,將每個i存在內存中,onclick函數用的時候提取出外部匿名函數的i值。代碼如下:
var aLi = document.getElementByClassName("test"); function showAllNum( aLi ){ for( var i =0,len = aLi.length ;i<len;i++ ){ (function(i){ aLi[i].onclick = function(){ alert( i ); } })(i); } }
或者:
function showAllNum( aLi ){ for( var i =0,len = aLi.length ;i<len;i++ ){ aLi[i].onclick = (function(i){ return function(){ alert( i ); } })(i); } }
實現解釋:
1.作用域鏈
2.閉包函數的賦值與運行
實際上只是通過函數的賦值表式方式付給了標簽點擊事件,並沒有運行;當遍歷完后,i變成標簽組的長度,根據作用域的原理,向上找到for函數里的i,所以點擊執行的時候都會彈出標簽組的長度。閉包可以使變量長期駐扎在內存當中,我們在綁定事件的時候讓它自執行一次,把每一次的變量存到內存中;點擊執行的時候就會彈出對應本作用域i的序號。
3.封裝
外部無法直接獲取函數內的變量,可通過暴露的方法獲取
var info = function(){ var _userId = 23492; var _typeId = 'item'; function getUserId(){ alert(_userId); } function getTypeId(){ alert(_typeId); } }; info.getUserId();//23492 info.getTypeId();//item info._userId//undefined info._typeId//undefined
但是這種方式會使我們在每一次創建新對象的時候都會創建一個這種方法。使用原型來創建一個這種方法,避免每個實例都創建不同的方法。在這里不做深究(一般構造函數加屬性,原型加方法)。
4.關於 this 對象
this 對象是在運行時基於函數的執行環境綁定的(匿名函數中具有全局性)(this:當前發生事件的元素),有時候在一些閉包的情況下就有點不那么明顯了。
代碼1:
var name = "The Window"; var obj = { name : "The object", getNameFunc : function(){ return function(){ return this.name; } } } alert( obj. getNameFunc()() )//The Window
代碼2:
var name="The Window" var obj = { name : "The object", getNameFunc : function(){ var _this = this; return function(){ return _this.name; } } } alert(object.getNameFunc()());//The object
javascript是動態(或者動態類型)語言,this關鍵字在執行的時候才能確定是誰。所以this永遠指向調用者,即對‘調用對象‘者的引用。第一部分通過代碼:執行代碼object.getNameFunc()之后,它返回了一個新的函數,注意這個函數對象跟object不是一個了,可以理解為全局函數;它不在是object的屬性或者方法,此時調用者是window,因此輸出是 The Window。
第二部分,當執行函數object.getNameFunc()后返回的是:
function( ) { return _this.name; }
此時的_this=this。而this指向object,所以that指向object。他是對object的引用,所以輸出My Object。
總結:關於js中的this,記住誰調用,this就指向誰;要訪問閉包的this,要定義個變量緩存下來。一般喜歡var _this = this。
5.閉包在IE下內存泄露問題
IE9之前,JScript對象和COM對象使用不同的垃圾收集例程,那么閉包會引起一些問題。
創建一個閉包,而后閉包有創建一個循環引用,那么該元素將無法銷毀。常見的就是dom獲取的元素或數組的屬性(或方法)再去調用自己屬性等。例如:
function handler(){ var ele = document.getElementById("ele"); ele.onclick = function(){ alert(ele.id); } }
閉包會引用包含函數的整個活動對象,即是閉包不直接引用ele,活動對象依然會對其保存一個引用,那么設置null就可以斷開保存的引用,釋放內存。代碼如下:
function handler(){ var ele = document.getElementById("ele"); var id = ele.id; ele.onclick = function(){ alert(id); } ele = null; }
當然還有其他方法,推薦此法。
三.閉包的原理
當某個函數第一次被調用時,會創建一個執行環境(execution context)及相應的作用域鏈,並把作用域鏈賦值給一個特殊的內部屬性(即[[Scope]])。然后,使用this、arguncmts 和其他命名參數的值來初始化函數的活動對象(activation object)。但在作用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,……直至作為作用域鏈終點的全局執行環境。
在函數執行過程中,為讀取和寫入變量的值,就需要在作用域鏈中查找變量。來看下面的例子:
function compare(valael, value2){
if (valuel < value2){
return -1; } else if (vaiuel > value2){
return 1; } else { return 0; } } var result = compare(5, 10);
以上代碼先定義了compare()函數,然后又在全局作用域中調用了它。當第一次調用compare()時,會創建一個包含this、arguments、valuel和value2的活動對象。全局執行環境的變量對象 (包含this、result和compare)在compare()執行環境的作用域鏈中則處於第二位。圖展示了包含上述關系的compare()函數執行時的作用域鏈。
后台的每個執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象,則只在函數執行的過程中存在。在創建compare()函數時,會創建一個預先包含全局變童對象的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]屬性中。當調用compare()函數時,會為函數創建一個執行環境,然后通過復制函數的[[Scope]]屬性中的對象構建起執行環境的作用域鏈。此后,又有一個活動對象(在此作為變量對象使用)被創建並被推入執行環境作用域鏈的前端。對於這個例子中compare()函數的執行環境而言,其作用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
無論什么時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢后,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象)。 但是,閉包的情況又有所不同。
function createComparisonFunction(propertyName) { return function(object1, object2){ var valuel = objectl[propertyName]; var value2 = object2[propertyName]; if (valuel < value2){ return -1; } else if (valuel > value2){ return 1; } else { return 0; } }; }
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在createComparisonFunction()涵數內部定義的匿名函數的作用域鏈中,實際上將會包含外部函數createComparisonFunction()的活動對象。圖展示了當下列代碼執行時,包含函數與內部匿名函數的作用域鏈。
var compare = createComparisonFunction("name"); var result = compare({ name: "Nicholas" }, { naine: BGreg" });
在匿名函數從createComparisonFunction()中被返冋后,它的作用域鏈被初始化為包含createComparisonFunction()函數的活動對象和全局變量對象。這樣,匿名函數就可以訪問在createComparisonFunction()中定義的所有變量。更重要的是,createCoir.parisonFunction() 函數在執行完畢后,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。換句話說,當createComparisonFunction()函數返回后,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中;直到匿名函數被銷毀后,createComparisonFunction()的活動對象才會被銷毀,例如:
var compareNames = createComparisonFunction("name"); //調用函數 var result = compareNames({ name: "Nicholas" ), { name:"Greg" }); //解除對匿名函數的引用(以便釋放內存) compareNanies = null;
首先,創建的比較函數被保存在變量coinpareNames中。而通過將compareNames設置為等於null解除該函數的引用,就等於通知垃圾問收例程將其清除。隨着匿名函數的作用域鏈被銷毀,其他作用域 (除r全局作用域)也都可以安全地銷毀了。圖 展示了調用conpareNamesO的過程中產生的作用域鏈之間的關系。
-------------------------------------------------------------------------------------------------------------------------------------
閉包無處不在,弄懂它很重要。
完
轉載需注明轉載字樣,標注原作者和原博文地址。