前端開發必須知道的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,而不是想要的 一、二、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.htmjavascript

相關文章
相關標籤/搜索