javascript-----閉包

閉包:指有權訪問另一個函數作用域中的變量的函數。
最常見的實現閉包的方式是:在一個函數內部創建另一個函數。
當某個函數被調用時,會創建一個執行環境(execution context)及相應的作用域鏈。然後,使用 arguments 和其他命名參數的值來初始化函數的活動對象(activation object)。但在作用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,……直至作爲作用域鏈終點的全局執行環境(這一段話需要好好理解透徹)。

(一)閉包與變量

閉包只能取得包含函數中任何變量的最後一個值。閉包所保存的是整個變量對象,而不是某個特殊的變量。

function createFun(){
 	var result = new Array();
 	for (var i=0; i < 10; i++){
	     result[i] = function(){ return i;};
	 }
 return result;
} 

每個函數都返回 10。因爲每個函數的作用域鏈中都保存着 createFun() 函數的活動對象,所以它們引用的都是同一個變量 i 。 當createFun()函數返回後,變量 i 的值是 10,此時每個函數都引用着保存變量 i 的同一個變量對象,所以在每個函數內部 i 的值都是 10。

實現符合預期的代碼

function createFunctions(){
 	var result = new Array();
     for (var i=0; i < 10; i++){
	     result[i] = function(num){
	     	return function(){ return num; };
     	}(i);
     }
 return result;
} 

在這裏沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將立即執行該匿名函數的結果賦給數組。這裏的匿名函數有一個參數 num,也就是最終的函數要返回的值。在調用每個匿名函數時,傳入了變量 i。由於函數參數是按值傳遞的,所以就會將變量 i 的當前值複製給參數 num。而在這個匿名函數內部,又創建並返回了一個訪問 num 的閉包。這樣一來,result 數組中的每個函數都有自己num 變量的一個副本,因此就可以返回各自不同的數值了。

(二)關於this對象

通常匿名函數的執行環境具有全局性,因此其 this 對象通常指向 window。但有時候由於編寫閉包的方式不同,這一點可能不會那麼明顯。

var name='我是貓咪'
var obj={
    name:'ccc',
    getName(){
        return function(){
       	    console.log(this)  //指向Window
            return this.name
        }
    }
}
console.log(obj.getName()())  //我是貓咪

調用 obj.getName()()就會返回一個字符串。然而,返回的字符串是"我是貓咪",即全局 name 變量的值。爲什麼匿名函數沒有取得其包含作用域(或外部作用域)的 this 對象呢?每個函數在被調用時都會自動取得兩個特殊變量:this 和 arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,因此永遠不可能直接訪問外部函數中的這兩個變量。
如果將外部作用域中的 this 對象保存在一個閉包能夠訪問到的變量裏,就可以讓閉包訪問該對象了。

var name='我是貓咪'
 var obj={
     name:'ccc',
     getName(){
         var me = this
         return function(){
             console.log(me)  //指向當前的obj
             console.log(this)  //指向Window
             return me.name
         }
     }
 }
 console.log(obj.getName()())  //ccc

this 和 arguments 也存在同樣的問題。如果想訪問作用域中的 arguments 對象,必須將對該對象的引用保存到另一個閉包能夠訪問的變量中。

(三)內存泄漏

function assignHandler(){
     var element = document.getElementById("someElement");
     element.onclick = function(){alert(element.id); };
} 

以上代碼創建了一個作爲 element 元素事件處理程序的閉包,而這個閉包則又創建了一個循環引用。由於匿名函數保存了一個對 assignHandler()的活動對象的引用,因此就會導致無法減少 element 的引用數。只要匿名函數存在,element 的引用數至少也是 1,因此它所佔用的內存就永遠不會被回收。

那麼如何解決呢?

function assignHandler(){
     var element = document.getElementById("someElement");
     //將element.id 的一個副本用變量進行存儲
     var id = element.id;
     
     element.onclick = function(){alert(id); };
     element = null;
}

在上面的代碼中,在閉包中引用該變量消除了循環引用。但是這樣還是不能解決內存泄漏的問題。必須要記住:閉包會引用包含函數的整個活動對象,而其中包含着 element。即使閉包不直接引用 element,包含函數的活動對象中也仍然會保存一個引用。因此,有必要把 element 變量設置爲 null。這樣就能夠解除對 DOM 對象的引用,順利地減少其引用數,確保正常回收其佔用的內存。

(四)模仿塊級作用域

IIFE立即執行的匿名函數

(function(){
 //這裏是塊級作用域
})(); 

這種做法可以減少閉包占用的內存問題,因爲沒有指向匿名函數的引用。只要函數執行完畢,就可以立即銷燬其作用域鏈了。

(五)私有變量

嚴格來講,JavaScript 中沒有私有成員的概念;所有對象屬性都是公有的。不過,倒是有一個私有變量的概念。任何在函數中定義的變量,都可以認爲是私有變量,因爲不能在函數的外部訪問這些變量。私有變量包括函數的參數、局部變量和在函數內部定義的其他函數。
如果在一個函數內部創建一個閉包,那麼閉包通過自己的作用域鏈也可以訪問這些變量。而利用這一點,就可以創建用於訪問私有變量的公有方法。
將有權訪問私有變量和私有函數的公有方法稱爲特權方法(privileged method)
創建特權方法的兩種方式:(1)構造函數中定義特權方法
實現:

function Person(name){
     this.getName = function(){
	     return name;
     };
     this.setName = function (value) {
	     name = value;
     };
} 

不過,在構造函數中定義特權方法也有一個缺點。那就是必須使用構造函數模式來達到這個目的。構造函數模式的缺點是針對每個實例都會創建同樣一組新方法,而使用靜態私有變量來實現特權方法就可以避免這個問題。

(六)靜態私有變量

通過在私有作用域中定義私有變量或函數,同樣也可以創建特權方法。

(function(){
     //私有變量和私有函數
     var privateVariable = 10;
     function privateFunction(){
     	return false;
     }
     //構造函數
     MyObject = function(){};
     //公有/特權方法
     MyObject.prototype.publicMethod = function(){
	     privateVariable++;
	     return privateFunction();
     };
})(); 

這個模式創建了一個私有作用域,並在其中封裝了一個構造函數及相應的方法。在私有作用域中,首先定義了私有變量和私有函數,然後又定義了構造函數及其公有方法。公有方法是在原型上定義的。需要注意的是:函數聲明只能創建局部函數,所以在聲明 MyObject 時也沒有使用 var 關鍵字。(初始化未經聲明的變量,總是會創建一個全局變量)MyObject 是一個全局變量。能夠在私有作用域之外被訪問到。但也要知道,在嚴格模式下給未經聲明的變量賦值會導致錯誤。

當在函數內部定義了其他函數時,就創建了閉包。閉包有權訪問包含函數內部的所有變量,原理如下。
   在後臺執行環境中,閉包的作用域鏈包含着它自己的作用域、包含函數的作用域和全局作用域。
   通常,函數的作用域及其所有變量都會在函數執行結束後被銷燬。
   但是,當函數返回了一個閉包時,這個函數的作用域將會一直在內存中保存到閉包不存在爲止。

使用閉包可以在 JavaScript 中模仿塊級作用域(JavaScript 本身沒有塊級作用域的概念),要點如下。
    創建並立即調用一個函數,這樣既可以執行其中的代碼,又不會在內存中留下對該函數的引用。
    結果就是函數內部的所有變量都會被立即銷燬——除非將某些變量賦值給了包含作用域(即外
   部作用域)中的變量。

閉包還可以用於在對象中創建私有變量,相關概念和要點如下。
    即使 JavaScript 中沒有正式的私有對象屬性的概念,但可以使用閉包來實現公有方法,而通過公
   有方法可以訪問在包含作用域中定義的變量。
    有權訪問私有變量的公有方法叫做特權方法。
    可以使用構造函數模式、原型模式來實現自定義類型的特權方法,也可以使用模塊模式、增強
   的模塊模式來實現單例的特權方法。

JavaScript 中的函數表達式和閉包都是極其有用的特性,利用它們可以實現很多功能。不過,因爲創建閉包必須維護額外的作用域,所以過度使用它們可能會佔用大量內存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章