前端開發必須知道的JS之閉包及應用


本文講的是函數閉包,不涉及對象閉包(如用with實現)。如果你覺得我說的有偏差,歡迎拍磚,歡迎指教。
在前端開發必須知道的JS之原型和繼承一文中說過下面寫篇閉包,加之最近越來越發現需要加強我的閉包應用能力,所以此文不能再拖了。本文講的是函數閉包,不涉及對象閉包(如用with實現)。如果你覺得我說的有偏差,歡迎拍磚,歡迎指教。
一. 閉包的理論 
  首先必須了解以下幾個概念: 

  執行環境 
  每調用一個函數時(執行函數時),系統會為該函數創建一個封閉的局部的運行環境,即該函數的執行環境。函數總是在自己的執行環境中執行,如讀寫局部變量、函數參數、運行內部邏輯。創建執行環境的過程包含了創建函數的作用域,函數也是在自己的作用域下執行的。從另一個角度說,每個函數執行環境都有一個作用域鏈,子函數的作用域鏈包括它的父函數的作用域鏈。關於作用域、作用域鏈請看下面。 

  作用域、作用域鏈、調用對象 
  函數作用域分為詞法作用域和動態作用域。 
  詞法作用域是函數定義時的作用域,即靜態作用域。當一個函數定義時,他的詞法作用域就確定了,詞法作用域說明的是在函數結構的嵌套關系下,函數作用的范圍。這個時候也就形成了該函數的作用域鏈。作用域鏈就是把這些具有嵌套層級關系的作用域串聯起來。函數的內部[[scope]]屬性指向了該作用域鏈。 
  動態作用域是函數調用執行時的作用域。當一個函數被調用時,首先將函數內部[[scope]]屬性指向了函數的作用域鏈,然后會創建一個調用對象,並用該調用對象記錄函數參數和函數的局部變量,將其置於作用域鏈頂部。動態作用域就是通過把該調用對象加到作用域鏈的頂部來創建的,此時的[[scope]]除了具有定義時的作用域鏈,還具有了調用時創建的調用對象。換句話說,執行環境下的作用域等於該函數定義時就確定的作用域鏈加上該函數剛剛創建的調用對象,從而也形成了新的作用域鏈。所以說是動態的作用域,並且作用域鏈也隨之發生了變化。再看這里的作用域,其實是一個對象鏈,這些對象就是函數調用時創建的調用對象,以及他上面一層層的調用對象直到最上層的全局對象。  
  譬如全局環境下的函數A內嵌套了一個函數B,則該函數B的作用域鏈就是:函數B的作用域—>函數A的作用域—>全局window的作用域。當函數B調用時,尋找某標識符,會按函數B的作用域—>函數A的作用域—>全局window的作用域去尋找,實際上是按函數B的調用對象—>函數A的調用對象—>全局對象這個順序去尋找的。也就是說當函數調用時,函數的作用域鏈實際上是調用對象鏈。 

  閉包 
  在動態執行環境中,數據實時地發生變化,為了保持這些非持久型變量的值,我們用閉包這種載體來存儲這些動態數據(看完下面的應用就會很好的體會這句話)。閉包的定義:所謂“閉包”,指的是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。 
  閉包就是嵌套在函數里面的內部函數,並且該內部函數可以訪問外部函數中聲明的所有局部變量、參數和其他內部函數。當該內部函數在外部函數外被調用,就生成了閉包。(實際上任何函數都是全局作用域的內部函數,都能訪問全局變量,所以都是window的閉包) 
  譬如下面這個例子: 
復制代碼 代碼如下:

<script type="text/javascript"> 
function f(x) { 
var a = 0; 
a++; 
x++; 
var inner = function() { 
return a + x; 
} 
return inner; 
} 
var test = f(1); 
alert(test()); 
</script> 

垃圾回收機制:如果某個對象不再被引用,該對象將被回收。   
  再結合前面所講的一些概念,在執行var test=f(1)時創建了f的調用對象,這里暫且記作obj,執行完后雖然退出了外部執行環境,但內部函數inner被外部函數f外面的一個變量test引用。由於外部函數創建的調用對象obj有一個屬性指向此內部函數,而現在這個內部函數又被引用,所以調用對象obj會繼續存在,不會被垃圾回收器回收,其函數參數x和局部變量a都會在這個調用對象中得以維持。雖然調用對象不能被直接訪問,但是該調用對象已成為內部函數作用域鏈中的一部分,可以被內部函數訪問並修改,所以執行test()時,可以正確訪問x和a。所以說, 當執行了外部函數時,生成了閉包,被引用的外部函數的變量將繼續存在。 
二. 閉包的應用 
  應用1: 
  這個是我在用js模擬排序算法過程遇到的問題。我要輸出每一次插入排序后的數組,如果在循環中寫成 
  setTimeout(function() { $("proc").innerHTML += arr + "<br/>"; }, i * 500); 
會發現每次輸出的都是最終排好序的數組,因為arr數組不會為你保留每次排序的狀態值。為了保存會不斷發生變化的數組值,我們用外面包裹一層函數來實現閉包,用閉包存儲這個動態數據。下面用了2種方式實現閉包,一種是用參數存儲數組的值,一種是用臨時變量存儲,后者必須要深拷貝。所有要通過閉包存儲非持久型變量,均可以用臨時變量或參數兩種方式實現。 

  [Ctrl+A 全選 注:如需引入外部Js需刷新才能執行]

應用2: 
  這個是無憂上的例子(點擊這里查看原帖),為每個<li>結點綁定click事件彈出循環的索引值。起初寫成 
id.onclick = function(){ alert(i); }  id.onclick = function(){alert(i);} 
發現最終彈出的都是4,而不是想要的 1、2、3,因為循環完畢后i值變成了4。為了保存i的值,同樣我們用閉包實現: 

  [Ctrl+A 全選 注:如需引入外部Js需刷新才能執行]

(ps:var a = (function(){})(); 與 var a =new function(){}效果是一樣的,均表示自執行函數。) 
  應用3: 
  下面的code是緩存的應用,catchNameArr。在匿名函數的調用對象中保存catch的值,返回的對象由於被CachedBox變量引用導致匿名函數的調用對象不會被回收,從而保持了catch的值。可以通過CachedBox.getCatch("regionId");來操作,若找不到regionId則從后台取,catchNameArr 主要是為了防止緩存過大。 
復制代碼 代碼如下:

<script type="text/javascript"> 
var CachedBox = (function() { 
var cache = {}, catchNameArr = [], catchMax = 10000; 
return { 
getCatch: function(name) { 
if (name in cache) { 
return cache[name]; 
} 
var value = GetDataFromBackend(); 
cache[name] = value; 
catchNameArr.push(name); 
this.clearOldCatch(); 
return value; 
}, 
clearOldCatch: function() { 
if (catchNameArr.length > catchMax) { 
delete cache[catchNameArr.shift()]; 
} 
} 
}; 
})(); 
</script> 

同理,也可以用這種思想實現自增長的ID。   
復制代碼 代碼如下:

<script type="text/javascript"> 
var GetId = (function() { 
var id = 0; 
return function() { 
return id++; 
} 
})(); 
var newId1 = GetId(); 
var newId2 = GetId(); 
</script> 

應用4: 
  這個是無憂上月MM的例子(點擊這里查看原帖),用閉包實現程序的暫停執行功能,還蠻創意的。 

  [Ctrl+A 全選 注:如需引入外部Js需刷新才能執行]

把這個作用延伸下,我想到了用他來實現window.confirm。 

  [Ctrl+A 全選 注:如需引入外部Js需刷新才能執行]

看了上面的這些應用,再回到前面的一句話:在動態執行環境中,數據實時地發生變化,為了保持這些非持久型變量的值,我們用閉包這種載體來存儲這些動態數據。這就是閉包的作用。也就說遇到需要存儲動態變化的數據或將被回收的數據時,我們可以通過外面再包裹一層函數形成閉包來解決。 
  當然,閉包會導致很多外部函數的調用對象不能釋放,濫用閉包會使得內存泄露,所以在頻繁生成閉包的情景下我們要估計下他帶來的副作用。 
  畢了。希望能對大家有所幫助。 

http://www.jb51.net/article/24156.htm


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM