javascript閉包

    衆所周知,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()函數的內容,實現彈出一個對話框提示當前選中的是第幾個單選框。

checkSelecttion函數後的return false;必不可少,不然的話就沒法在checkSelecttion函數中爲radio添加事件,由於當sub按鈕提交後就會執行表單的submit事件,return false;是取消執行默認的事件。

    最直接的想法確定是下面的,但這種寫法並不會知足要求。

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)
        }
    }
1. (function(param){})(param); 與 function(param){}(param)效果是同樣的,均表示自執行函數。
2.如何使用閉包存儲函數中變化的局部變量?——在該函數中再嵌套一層函數,並引用它。

深刻理解閉包

    要想深刻理解閉包,咱們須要引入另外幾個概念:函數的執行環境(excution context)、活動對象(call object)、做用域(scope)、做用域鏈(scope chain)。如今,咱們以例1爲例闡述這幾個概念:

  1. 定義函數a的時候,js解釋器會將函數a的做用域鏈(scope chain)設置爲定義a時a所在的「環境」,若是a是一個全局函數,則scope chain中只有window對象。
  2. 執行函數a的時候,a會進入相應的執行環境(excution context)
  3. 在建立執行環境的過程當中,首先會爲a添加一個scope屬性,即a的做用域,其值就爲第1步中的scope chain。即a.scope=a的做用域鏈。
  4. 而後執行環境會建立一個活動對象(call object)。活動對象也是一個擁有屬性的對象,但它不具備原型並且不能經過JavaScript代碼直接訪問。建立完活動對象後,把活動對象添加到a的做用域鏈的最頂端。此時a的做用域鏈包含了兩個對象:a的活動對象和window對象。
  5. 下一步是在活動對象上添加一個arguments屬性,它保存着調用函數a時所傳遞的參數。
  6. 最後把全部函數a的形參和內部的函數b的引用也添加到a的活動對象上。在這一步中,完成了函數b的的定義,所以如同第3步,函數b的做用域鏈被設置爲b所被定義的環境,即a的做用域。

    到此,整個函數a從定義到執行的步驟就完成了。此時a返回函數b的引用給c,又函數b的做用域鏈包含了對函數a的活動對象的引用,也就是說b能夠訪問到a中定義的全部變量和函數。函數b被c引用,函數b又依賴函數a,所以函數a在返回後不會被GC回收。

    當函數b執行的時候亦會像以上步驟同樣。所以,執行時b的做用域鏈包含了3個對象:b的活動對象、a的活動對象和window對象,以下圖所示:

    

    如圖所示,當在函數b中訪問一個變量的時候,搜索順序是:

  1. 先搜索自身的活動對象,若是存在則返回,若是不存在將繼續搜索函數a的活動對象,依次查找,直到找到爲止。
  2. 若是函數b存在prototype原型對象,則在查找完自身的活動對象後先查找自身的原型對象,再繼續查找。這就是Javascript中的變量查找機制。
  3. 若是整個做用域鏈上都沒法找到,則返回undefined。

閉包的應用場景

    1.在內存中維持一個變量。依然如前例,因爲閉包,函數a中i的一直存在於內存中,所以每次執行c(),都會給i自加1。

    2.經過保護變量的安全實現JS私有屬性和私有方法(不能被外部訪問)。

Javascript的垃圾回收機制

    在Javascript中,若是一個對象再也不被引用,那麼這個對象就會被GC回收。若是兩個對象互相引用,而再也不被第3者所引用,那麼這兩個互相引用的對象也會被回收。由於函數a被b引用,b又被a外的c引用,這就是爲何函數a執行後不會被回收的緣由。

閉包中的this

    在下面的代碼中,爲何結果是「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);

這裏個人腳本的位置在<body>前面,因此這個腳本前加個<div id="bixu"></div>,不然執行document.body.appendChild(table);由於document的body還未解析會報錯,最好是把該腳本寫到<body>標籤後面。

    因爲在內嵌函數這有引用render函數的局部變量h,因此每次調用render函數時h的引用數據均被存儲下來。

	var render = function(d){
		var that = this;
		var h= d;
		addEvent('click',function(){
			alert(h);
		},that);
	}

其餘實例

    如下實例來自:前端開發必須知道的JS之閉包及應用

1.保存會不斷髮生變化的數組值

     這個是我在用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>

2.緩存的應用

    下面的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> 

3.暫停執行功能

    這個是無憂上月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>

 

小結

在動態執行環境中,數據實時地發生變化,爲了保持這些非持久型變量的值,咱們用閉包這種載體來存儲這些動態數據。這就是閉包的做用。也就說遇到須要存儲動態變化的數據或將被回收的數據時,咱們能夠經過外面再包裹一層函數造成閉包來解決。

其餘參考文章

JavaScript變量做用域(Variable Scope)和閉包(closure)的基礎知識

相關文章
相關標籤/搜索