1. 程式人生 > >js的closures(閉包)

js的closures(閉包)

JS中的閉包(closure)

閉包(closure)是Javascript語言的一個難點,也是它的特色,很多高階應用都要依靠閉包實現。
下面就是我的學習筆記,對於Javascript初學者應該是很有用的。

一.什麼是閉包

JS中,在函式內部可以讀取函式外部的變數

function outer(){
     var localVal = 30;
     return localVal;
}
outer();//30

但,在函式外部自然無法讀取函式內的區域性變數

function outer(){
     var localVal = 30;
}
alert(localVal);//error

這裡有個需要注意的地方,函式內部宣告變數的時候,一定要使用var命令。如果不用的話,實際上是聲明瞭一個全域性變數。

複製程式碼
function outer(){
     localVal = 30;
     return localVal;
}
outer();
alert(localVal);//30
複製程式碼

以上的表述,是JS變數的作用域的知識,它包括全域性變數和區域性變數。

Javascript語言的特殊之處,就在於函式內部可以直接讀取全域性變數。

複製程式碼
function outer(){
     var localVal = 30;
function inner(){
          alert(localVal);
     }
 
 
     return inner; 
}
var func = outer();
func();//30
複製程式碼

我們看到在上面的程式碼中,outer函式內又定義一個函式inner,outer函式的返回值是inner函式,inner函式把localVal alert出來。

我們可以看出以上程式碼的特點:函式巢狀函式,內部函式可以引用外部函式的引數和變數,引數和變數不會被垃圾回收機制收回。

程式碼中的inner函式,就是閉包。簡單的說,閉包(closure)就是能夠讀取其他函式內部變數的函式。

由於在Javascript語言中,只有函式內部的子函式才能讀取區域性變數,因此可以把閉包簡單理解成“定義在一個函式內部的函式”。所以,在本質上,閉包就是將函式內部和函式外部連線起來的一座橋樑。

在上面的程式碼中,函式inner被包含在函式outer內部,這時outer內部的所有區域性變數,對inner都是可見的。但是inner內部的區域性變數,對oute 是不可見的。這是Javascript語言特有的“鏈式作用域”結構(chain scope),子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。

補充--JS中的函式定義

JS中定義一個函式,最常用的就是函式宣告和函式表示式

Js中的函式宣告是指下面的形式:

function functionName(){  
  
}

函式表示式則是類似表示式那樣來宣告一個函式:

var functionName = function(){  

}

我們可以使用函式表示式建立一個函式並馬上執行它,如:

(function() {
  var a, b    // local variables
  // ...      // and the code
})()

()();第一個括號裡放一個無名的函式。

二者區別:js的解析器對函式宣告與函式表示式並不是一視同仁地對待的。對於函式宣告,js解析器會優先讀取,確保在所有程式碼執行之前宣告已經被解析,而函式表示式,如同定義其它基本型別的變數一樣,只在執行到某一句時也會對其進行解析,所以在實際中,它們還是會有差異的,具體表現在,當使用函式宣告的形式來定義函式時,可將呼叫語句寫在函式宣告之前,而後者,這樣做的話會報錯。

二.閉包的應用

使用閉包的好處:

-希望一個變數長期駐紮在記憶體當中;

-避免全域性變數的汙染;

-私有成員的存在

1.模組化程式碼

使用自執行的匿名函式來模擬塊級作用域

(function(){
        // 這裡為塊級作用域
 })();

該方法經常在全域性作用域中被用在函式外部,從而限制向全域性作用域中新增過多的變數和函式影響全域性作用域。也可以減少如閉包這樣的對記憶體的佔用,由於匿名函式沒有變數指向,執行完畢就可以立即銷燬其作用域鏈。

示例:

複製程式碼
var test = (function(){
    var a= 1;
    return function(){
        a++;
        alert(a);
    }
})();

test();//2
test();//3
複製程式碼

實現a的自加,不汙染全域性。

2.迴圈閉包

迴圈給每個li註冊一個click事件,點選alert序號。程式碼如下:

複製程式碼
var aLi = document.getElementByClassName("test");
function showAllNum( aLi ){
    for( var i =0,len = aLi.length ;i<len;i++ ){
            aLi[i].onclick = function(){
            alert( i );//all are aLi.length!
        }
    }
}
複製程式碼

點選後會一直彈出同一個值 aLi.length 而不是123。當點選之前,迴圈已經結束,i值為aLi.length。

利用閉包,建一個匿名函式,將每個i存在記憶體中,onclick函式用的時候提取出外部匿名函式的i值。程式碼如下:

複製程式碼
var aLi = document.getElementByClassName("test");
function showAllNum( aLi ){
    for( var i =0,len = aLi.length ;i<len;i++ ){
        (function(i){
            aLi[i].onclick = function(){
            alert( i );
        }
        })(i);
    }
}
複製程式碼

或者:

複製程式碼
function showAllNum( aLi ){
    for( var i =0,len = aLi.length ;i<len;i++ ){
            aLi[i].onclick = (function(i){
                return function(){
                    alert( i );
                }
            })(i);
    }
}
複製程式碼

實現解釋:

1.作用域鏈

2.閉包函式的賦值與執行

實際上只是通過函式的賦值表式方式付給了標籤點選事件,並沒有執行;當遍歷完後,i變成標籤組的長度,根據作用域的原理,向上找到for函式裡的i,所以點選執行的時候都會彈出標籤組的長度。閉包可以使變數長期駐紮在記憶體當中,我們在繫結事件的時候讓它自執行一次,把每一次的變數存到記憶體中;點選執行的時候就會彈出對應本作用域i的序號。

3.封裝

外部無法直接獲取函式內的變數,可通過暴露的方法獲取

複製程式碼
var info = function(){
    var _userId = 23492;
    var _typeId = 'item';

    function getUserId(){
        alert(_userId);
    }

    function getTypeId(){
        alert(_typeId);
    }
};

info.getUserId();//23492
info.getTypeId();//item

info._userId//undefined
info._typeId//undefined
複製程式碼

但是這種方式會使我們在每一次建立新物件的時候都會建立一個這種方法。使用原型來建立一個這種方法,避免每個例項都建立不同的方法。在這裡不做深究(一般建構函式加屬性,原型加方法)。

4.關於 this 物件

this 物件是在執行時基於函式的執行環境繫結的(匿名函式中具有全域性性)(this:當前發生事件的元素),有時候在一些閉包的情況下就有點不那麼明顯了。

程式碼1:

複製程式碼
var name = "The Window";
var obj = {
    name : "The object",
    getNameFunc : function(){
        return function(){
            return this.name;
        }
    }
}
alert( obj. getNameFunc()() )//The Window
複製程式碼

程式碼2:

複製程式碼
var name="The Window"
var obj = {
    name : "The object",
        
    getNameFunc : function(){
        var _this = this;
        return function(){
            return _this.name;
        }
    }
}
alert(object.getNameFunc()());//The object
複製程式碼

javascript是動態(或者動態型別)語言,this關鍵字在執行的時候才能確定是誰。所以this永遠指向呼叫者,即對‘呼叫物件‘者的引用。第一部分通過程式碼:執行程式碼object.getNameFunc()之後,它返回了一個新的函式,注意這個函式物件跟object不是一個了,可以理解為全域性函式;它不在是object的屬性或者方法,此時呼叫者是window,因此輸出是 The Window。

第二部分,當執行函式object.getNameFunc()後返回的是:

function( )
{
         return _this.name;
}

此時的_this=this。而this指向object,所以that指向object。他是對object的引用,所以輸出My Object。

總結:關於js中的this,記住誰呼叫,this就指向誰;要訪問閉包的this,要定義個變數快取下來。一般喜歡var _this = this。

5.閉包在IE下記憶體洩露問題

IE9之前,JScript物件和COM物件使用不同的垃圾收集例程,那麼閉包會引起一些問題。

建立一個閉包,而後閉包有建立一個迴圈引用,那麼該元素將無法銷燬。常見的就是dom獲取的元素或陣列的屬性(或方法)再去呼叫自己屬性等。例如:

複製程式碼
function handler(){
    var ele = document.getElementById("ele");
    ele.onclick = function(){
        alert(ele.id);
    }
}
複製程式碼

閉包會引用包含函式的整個活動物件,即是閉包不直接引用ele,活動物件依然會對其儲存一個引用,那麼設定null就可以斷開儲存的引用,釋放記憶體。程式碼如下:

複製程式碼
function handler(){
    var ele = document.getElementById("ele");
    var id = ele.id;
    ele.onclick = function(){
        alert(id);
    }
    ele = null;
}
複製程式碼

當然還有其他方法,推薦此法。

三.閉包的原理

當某個函式第一次被呼叫時,會建立一個執行環境(execution context)及相應的作用域鏈,並把作用域鏈賦值給一個特殊的內部屬性(即[[Scope]])。然後,使用this、arguncmts 和其他命名引數的值來初始化函式的活動物件(activation object)。但在作用域鏈中,外部函式的活動物件始終處於第二位,外部函式的外部函式的活動物件處於第三位,……直至作為作用域鏈終點的全域性執行環境。

在函式執行過程中,為讀取和寫入變數的值,就需要在作用域鏈中查詢變數。來看下面的例子:

複製程式碼
function compare(valael, value2){ 
    if (valuel < value2){ 
       return -1;
    } else if (vaiuel > value2){ 
       return 1;
     } else {
       return 0;
    }
}

var result = compare(5, 10);
複製程式碼

以上程式碼先定義了compare()函式,然後又在全域性作用域中呼叫了它。當第一次呼叫compare()時,會建立一個包含this、arguments、valuel和value2的活動物件。全域性執行環境的變數物件 (包含this、result和compare)在compare()執行環境的作用域鏈中則處於第二位。圖展示了包含上述關係的compare()函式執行時的作用域鏈。

image

後臺的每個執行環境都有一個表示變數的物件——變數物件。全域性環境的變數物件始終存在,而像compare()函式這樣的區域性環境的變數物件,則只在函式執行的過程中存在。在建立compare()函式時,會建立一個預先包含全域性變童物件的作用域鏈,這個作用域鏈被儲存在內部的[[Scope]]屬性中。當呼叫compare()函式時,會為函式建立一個執行環境,然後通過複製函式的[[Scope]]屬性中的物件構建起執行環境的作用域鏈。此後,又有一個活動物件(在此作為變數物件使用)被建立並被推入執行環境作用域鏈的前端。對於這個例子中compare()函式的執行環境而言,其作用域鏈中包含兩個變數物件:本地活動物件和全域性變數物件。顯然,作用域鏈本質上是一個指向變數物件的指標列表,它只引用但不實際包含變數物件。

無論什麼時候在函式中訪問一個變數時,就會從作用域鏈中搜索具有相應名字的變數。一般來講,當函式執行完畢後,區域性活動物件就會被銷燬,記憶體中僅儲存全域性作用域(全域性執行環境的變數物件)。 但是,閉包的情況又有所不同。

複製程式碼
function createComparisonFunction(propertyName) {
  return function(object1, object2){
    var valuel = objectl[propertyName]; 
    var value2 = object2[propertyName]; 
    if (valuel < value2){ 
       return -1;
    } else if (valuel > value2){ 
       return 1;
    } else {
       return 0;
     }
  };
}
複製程式碼

在另一個函式內部定義的函式會將包含函式(即外部函式)的活動物件新增到它的作用域鏈中。因此,在createComparisonFunction()涵數內部定義的匿名函式的作用域鏈中,實際上將會包含外部函式createComparisonFunction()的活動物件。圖展示了當下列程式碼執行時,包含函式與內部匿名函式的作用域鏈。

var compare = createComparisonFunction("name");

var result = compare({ name: "Nicholas" }, { naine: BGreg" });

在匿名函式從createComparisonFunction()中被返冋後,它的作用域鏈被初始化為包含createComparisonFunction()函式的活動物件和全域性變數物件。這樣,匿名函式就可以訪問在createComparisonFunction()中定義的所有變數。更重要的是,createCoir.parisonFunction() 函式在執行完畢後,其活動物件也不會被銷燬,因為匿名函式的作用域鏈仍然在引用這個活動物件。換句話說,當createComparisonFunction()函式返回後,其執行環境的作用域鏈會被銷燬,但它的活動物件仍然會留在記憶體中;直到匿名函式被銷燬後,createComparisonFunction()的活動物件才會被銷燬,例如:

複製程式碼
var compareNames = createComparisonFunction("name");

//呼叫函式
var result = compareNames({ name: "Nicholas" ), { name:"Greg" });

//解除對匿名函式的引用(以便釋放記憶體)
compareNanies = null;
複製程式碼

首先,建立的比較函式被儲存在變數coinpareNames中。而通過將compareNames設定為等於null解除該函式的引用,就等於通知垃圾問收例程將其清除。隨著匿名函式的作用域鏈被銷燬,其他作用域 (除r全域性作用域)也都可以安全地銷燬了。圖 展示了呼叫conpareNamesO的過程中產生的作用域鏈之間的關係。

image

-------------------------------------------------------------------------------------------------------------------------------------

閉包無處不在,弄懂它很重要。

轉載自博主:jingwhale

原文:https://www.cnblogs.com/jingwhale/p/4574792.html