衆所周知,JS有全局(Global)變量和局部(Local)變量,其實還有一種,就是閉包(Closure)變量,在Google瀏覽器調試時會發現它(圖1)。那麼這個閉包變量是什麼,又是如何產生的,又發揮怎樣的做用呢?javascript
圖1 css
閉包在實際中運用十分普遍,最重要的是它爲應用內存中存儲變量的引用提供了一套簡便優化的方案。html
咱們能夠經過定義對象,屬性,賦值的方式來存儲對象信息:前端
//定義 var Person = function(){ Person.prototype.setName = function(name){ this.name = name; } } //實例化,賦值 var p1 = new Person(); p1.setName("a")
可是不少時候咱們並沒有這樣的必要,只是但願某些變量引用不一樣的信息並存儲起來,如:java
在這個日曆渲染時,咱們須要2016年9月份中的每一個td元素都指向一個對應的日期,如td(21)指向2016-09-21,那麼這種須要通常都是用閉包來實現的。那是如何實現的呢?看完本文相信你會知道答案。算法
JS沒有塊級做用域,有函數做用域,那麼咱們如何讀取一個函數中的變量呢?答案就是內嵌一個函數,在這個內嵌函數中引用外部函數的變量。如:數組
例2瀏覽器
function a() { var i = 0; function b() { alert(++i); } return b; } var c = a(); c();
這樣執行函數c咱們就能夠訪問函數a的變量了。能夠說,函數b就是一個閉包。緩存
關於閉包,官方的解釋是:閉包是一個擁有許多變量和綁定了這些變量的環境的表達式(一般是一個函數),於是這些變量也是該表達式的一部分。安全
在文章javascript深刻理解js閉包中對上述示例作出了這樣的解釋:
簡單地說,對於函數內的局部變量來講,當該函數被調用完畢,若是局部變量沒有被引用則就會被GC回收。在實際的狀況中,函數內局部變量的值極可能發生變化,那麼要儲存這些變化的值就能夠再嵌套一層函數,並在內嵌函數中引用局部變量,這樣它就不會被GC回收,這就是閉包。下面以一個關於閉包的經典案例來講明:
HTML代碼:
<form name="form1" onsubmit="checkSelecttion();return false;"> <input type="radio" name="radioGroup"> <input type="radio" name="radioGroup"> <input type="radio" name="radioGroup"> <input type="radio" name="radioGroup"> <input type="submit" value="sub"> </form>
如今的要求是使用原生的JavaScript完成checkSelecttion()函數的內容,實現彈出一個對話框提示當前選中的是第幾個單選框。
最直接的想法確定是下面的,但這種寫法並不會知足要求。
function checkSelecttion(){ var radioGroups = document.getElementsByName("radioGroup"); for( var i = 0; i < radioGroups.length; i++ ) { //no. 彈出i的值都是最後一個 radioGroups[i].onclick = function () { alert(i); } } }
變量i是函數checkSelecttion中的局部變量,onclick函數引用了該變量,i就是一個閉包變量(Closure variable),它會被存儲起來,但問題是i因爲循環會指向不一樣的值,循環完畢,i會指向最後一個值:radioGroups.length-1,那這樣的話,每次彈出i的值就是radioGroups.length-1。
解決該問題就是運用閉包,把i變成onclick函數的局部變量,並再嵌套一層函數引用它,這樣就能夠存儲變化的i的值了:
function checkSelecttion(){ var radioGroups = document.getElementsByName("radioGroup"); for( var i = 0; i < radioGroups.length; i++ ) { //按參數傳遞 radioGroups[i].onclick = function (i) {//形參【必須】 return function () { alert(i); } }(i);//實參【必須】 } }
由於JS的參數傳遞是按值傳遞,至關於copy一份參數值(或者說至關於在onclick函數內將i定義成了該函數的局部變量),因此當i是0時,因爲onclick函數的內嵌函數有引用i,因此它會被存儲起來,當i是1...同理都會被存儲起來。這樣就實現了用閉包存儲函數內變化的局部變量值的目的。
固然,下面的2種方式同理均可以達到相同的效果:
1.下面的這種方式與上面的區別是在定義自執行函數沒有傳遞參數,那麼在onclick函數內定義局部變量i_接收i,i和i_都是閉包變量,它們都會被存儲起來,可是被存儲i的值只有一個,就是radioGroups.length-1,而i_的值卻被存儲成不一樣的值。
function checkSelecttion(){ var radioGroups = document.getElementsByName("radioGroup"); for( var i = 0; i < radioGroups.length; i++ ) { radioGroups[i].onclick = function () { var i_ = i;//定義局部變量 return function () {//嵌套一層函數造成閉包存儲局部變量不一樣的值 alert(i_); } }();//自執行函數 } }
2.下面的方式只是自執行函數的另外一種寫法:
function checkSelecttion(){ var radioGroups = document.getElementsByName("radioGroup"); for( var i = 0; i < radioGroups.length; i++ ) { radioGroups[i].onclick = (function (i) { return function () { alert(i+1); } })(i);//自執行函數 (function(param){})(param) } }
要想深刻理解閉包,咱們須要引入另外幾個概念:函數的執行環境(excution context)、活動對象(call object)、做用域(scope)、做用域鏈(scope chain)。如今,咱們以例1爲例闡述這幾個概念:
到此,整個函數a從定義到執行的步驟就完成了。此時a返回函數b的引用給c,又函數b的做用域鏈包含了對函數a的活動對象的引用,也就是說b能夠訪問到a中定義的全部變量和函數。函數b被c引用,函數b又依賴函數a,所以函數a在返回後不會被GC回收。
當函數b執行的時候亦會像以上步驟同樣。所以,執行時b的做用域鏈包含了3個對象:b的活動對象、a的活動對象和window對象,以下圖所示:
如圖所示,當在函數b中訪問一個變量的時候,搜索順序是:
1.在內存中維持一個變量。依然如前例,因爲閉包,函數a中i的一直存在於內存中,所以每次執行c(),都會給i自加1。
2.經過保護變量的安全實現JS私有屬性和私有方法(不能被外部訪問)。
在Javascript中,若是一個對象再也不被引用,那麼這個對象就會被GC回收。若是兩個對象互相引用,而再也不被第3者所引用,那麼這兩個互相引用的對象也會被回收。由於函數a被b引用,b又被a外的c引用,這就是爲何函數a執行後不會被回收的緣由。
在下面的代碼中,爲何結果是「The Window」呢?
var name = "The Window"; var object = { name: "My Object", getNameFunc: function() { return function() { return this.name; }; } }; alert(object.getNameFunc()()); //The Window
在JS中,內嵌函數歸屬於window,也就是說上面代碼中的匿名函數中的this指的是window,故而打印的結果就是全局變量"The Window"了。
那麼如何訪問局部變量"My Object"呢?很簡單,咱們在匿名函數外建立變量that,指向object,這樣打印的結果就是"My Object"了。
var name = "The Window"; var object = { name: "My Object", getNameFunc: function() { var that = this; return function() { return that.name; }; } }; alert(object.getNameFunc()());//My Object
如本文的篇首所說,一般用JS寫的日曆插件在渲染每一個日期(通常是td元素)時會用閉包來實現讓當前的td元素引用對應的日期數據,如:
假設這就是日曆中2016年的9月份的1~9日,如今就須要將這些數字與具體的日期的對應數據在應用中存儲起來,如6對應2016年9月6日:
代碼以下:
//爲元素綁定事件 var addEvent=function(env,fn,obj){ obj.addEventListener(env,fn,false); }; var render = function(h){ var that = this; addEvent('click',function(){ alert(h); },that); } var dc = function(a){ return document.createElement(a); } var table = dc('table'); var tr = dc('tr'); table.appendChild(tr); for(var i = 1;i < 10;i++){ var d = new Date() d.setDate(i); var td = dc('td'); td.innerHTML = i; render.apply(td,[d]); tr.appendChild(td); } table.setAttribute("border","1px"); table.setAttribute("bordercolor","red"); table.setAttribute("cellspacing","0px"); document.body.appendChild(table);
因爲在內嵌函數這有引用render函數的局部變量h,因此每次調用render函數時h的引用數據均被存儲下來。
var render = function(d){ var that = this; var h= d; addEvent('click',function(){ alert(h); },that); }
如下實例來自:前端開發必須知道的JS之閉包及應用
這個是我在用js模擬排序算法過程遇到的問題。我要輸出每一次插入排序後的數組,若是在循環中寫成
setTimeout(function() { $("proc").innerHTML += arr + "<br/>"; }, i * 500);
會發現每次輸出的都是最終排好序的數組,由於arr數組不會爲你保留每次排序的狀態值。爲了保存會不斷髮生變化的數組值,咱們用外面包裹一層函數來實現閉包,用閉包存儲這個動態數據。下面用了2種方式實現閉包,一種是用參數存儲數組的值,一種是用臨時變量存儲,後者必需要深拷貝。全部要經過閉包存儲非持久型變量,都可以用臨時變量或參數兩種方式實現。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript">< !-- var arr = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0]; var $ = function(id) { return document.getElementById(id); } var Sort = { Insert: function() { for (var i = 1; i < arr.length; i++) { for (var j = 0; j < i; j++) { if (arr[i] < arr[j]) { arr[i] = [arr[j], arr[j] = arr[i]][0]; } } setTimeout((function() { var m = []; for (var j = 0; j < arr.length; j++) { m[j] = arr[j]; } return function() { $("proc").innerHTML += m + "<br>"; } })(), i * 500); //or 寫成下面這樣也能夠 /* setTimeout((function(m) { return function() { $("proc").innerHTML += m + "<br>"; } })(arr.join(",")), i * 500); */ } return arr; } } // --> </script> </head> <body> <div>var a = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];</div> <div> <input type="button" value="插入排序" onclick="Sort.Insert();" /></div>Proc: <div id="proc"></div></body> </html>
下面的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>
這個是無憂上月MM的例子(點擊這裏查看原帖),用閉包實現程序的暫停執行功能,還蠻創意的。
< input type = "button"value = "繼續"onclick = 'st();' / > <script type = "text/javascript" > <!-- var st = (function() { alert(1); alert(2); return function() { alert(3); alert(4); } })(); // --></script>
把這個做用延伸下,我想到了用他來實現window.confirm。
<html xmlns="http://www.w3.org/1999/xhtml"> <head> </head> <body> < !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" > <title> </title> <script type="text/javascript "> var $ = function(id) { return "string " == typeof id ? document.getElementById(id) : id; } var doConfirm = function(divId) { $(divId).style.display = ""; function closeDiv() { $(divId).style.display = "none "; } return function(isOk) { if (isOk) { alert("Do deleting..."); } closeDiv(); } } </script> <style type="text / css "> body { font-family: Arial; font-size: 13px; background-color: #FFFFFF;} #confirmDiv { width: 200px; height: 100px; border: dashed 1px black;position: absolute; left: 200px; top: 150px; } </style> <div> <input name="btn2 " type="button " value="刪除" onclick="doConfirm('confirmDiv'); " /> <div id="confirmDiv " style="display: none; "> <div style="position: absolute; left: 50px; top: 15px;"> <p> 你肯定要刪除嗎? </p> <input type="button " value="肯定" onclick="doConfirm('confirmDiv')(true); " /> <input type="button " value="取消" onclick="doConfirm('confirmDiv')(false); " /> </div> </div> </div> </body> </html>
在動態執行環境中,數據實時地發生變化,爲了保持這些非持久型變量的值,咱們用閉包這種載體來存儲這些動態數據。這就是閉包的做用。也就說遇到須要存儲動態變化的數據或將被回收的數據時,咱們能夠經過外面再包裹一層函數造成閉包來解決。