Javascript閉包(Javascript Closures譯文)

原文: http://jibbering.com/faq/notes/closures/ , 強烈推薦有能力的同窗讀原文.javascript

本文不會對原文逐字逐句翻譯, 但文章的結構和大致意思會與做者保持一致.文中穿插了一些我我的的理解,以藍色字體標出.html

Javascript閉包java

簡介node

    「閉包」就是一個表達式(一般是函數表達式),該表達式能夠自由訪問一些變量和變量所處的環境(這個環境將這些變量‘關’在了裏面)。web

     閉包是JS最強大的特性之一,但在沒有深入理解它以前咱們很難充分、高效地利用它。閉包一般很容易被建立,有時甚至是無心識地,但這種建立一般是有害的,尤爲是在web瀏覽器環境中。爲了不在無心識建立閉包帶來的危害而充分利用它的優勢,咱們須要瞭解閉包底層的機制。該機制主要是做用域鏈(scope chain)在解析標識符(identifier)及對象屬性時所扮演的角色。面試

     關於閉包最簡單的解釋就是 ECMAScript(即Javascript)容許內部函數(inner function),內部函數是指函數聲明或函數表達式是在另一個函數(稱爲outer function或外部函數)的函數體中定義的。內部函數能夠自由訪問其outer function的函數參數、局部變量及其它內部函數。當內部函數在其外部函數以外被訪問時,閉包就造成了。這意味着內部函數在其外部函數返回以後依然能夠被執行。當內部函數被執行時, 它所能訪問的外部函數的參數、局部變量及其它inner function的值都處於外部函數返回以前的狀態(至關因而保留了一份當時的現場)。編程

對象的命名屬性解析數組

ECMAScript共有兩種類型的對象,分別是原生對象(Native Object)和宿主對象(Host Object)其中原生對象是指js語言自己的對象(Array, Date等內置對象,以及經過js代碼建立的對象如var obj = new Object();),宿主對象是指JS執行環境(客戶端環境一般是瀏覽器,服務器端多是NodeJs等)所內置的對象,例如瀏覽器環境中的window對象,dom對象等。瀏覽器

原生對象是鬆散動態的命名屬性的集合(Native objects are loose and dynamic bags of named properties),這些命名屬性的值多是其它對象的引用,也多是一個原始值: string, numbe,boolean, null 或undefined. 其中undefined這個原始值有一點怪異,當咱們把它賦值給對象的某個屬時,並不會將該屬性從對象中移除,而只是該屬性的值是undefined而已。緩存

下面的這個例子展現瞭如何給對象的屬性賦值以及如何從對象中讀取這個屬性的值。

賦值

//建立一個普通的js對象
var objectRef = new Object(); 
//給該對象添加一個名爲'testNumber'的屬性:
objectRef.testNumber = 5;
/* - 或:- */
objectRef["testNumber"] = 5;
//在上面這句賦值語句執行以前,objectRef所引用的對象上並無testNumber這個屬性,但在賦值以後它有了。以後再給這個屬性賦值就不會再建立新屬性了,而只是重置這個屬性的值:
objectRef.testNumber = 8;
/* - 或:- */
objectRef["testNumber"] = 8;

每一個js對象都有一個原型(prototype),這個原型自己也是一個對象(或null,只有Object.prototype能夠爲null,並且這也是js語言自己的設置,任何將其它對象的原型設爲null的語句都會被忽略)。該原型對象也可能有命名的屬性,可是它們和賦值過程無關。賦值時,若是被賦值的對象沒有對應的命名屬性,則該命名屬性將在該對象上被建立,並把值賦給這個新建立的屬性。在此以後的賦值操做只會重置(reset)這個屬性的值。

讀值

在讀值的過程當中,原型才真正開始發揮做用。若是對象上有對應的命名屬性,則該屬性的值將被返回:

 
/* 給對象的一個命名屬性賦值,若是在此以前對象上沒有這個屬性,則該屬性將被建立*/
objectRef.testNumber = 8;

/*讀取這個屬性的值:- */ 
var val = objectRef.testNumber; /* val的值如今是8了 */
 

因爲全部的js對象都有原型,又原型也是對象,因此原型也有原型,原型的原型也有原型。。這樣就造成了原型鏈(prototype chain).這個原型鏈會一直繼續直到原型鏈中的某個對象的原型爲null. Object對象的原型默認有一個null的原型(即Object.prototype.prototype = null), 所以:

var objectRef = new Object();

objectRef的原型鏈中將只有一個對象,即Object.prototype. 然而:

/* 
   MyObject1的構造函數
*/
function MyObject1(formalParameter){
    /* 該類的對象有一個名爲‘testNumber’的命名屬性  */
    this.testNumber = formalParameter;
}

/* 
   MyObject2的構造函數
*/
function MyObject2(formalParameter){
   /* 該類的對象有一個名爲‘testString’ 的命名屬性   */
    this.testString = formalParameter;
}

/* 下面的操做將全部MyObject2類型實例的原型由默認的對象(??Object.prototype or Function.prototype??)替換爲一個MyObject1類型的對象,且該對象的testNumber屬性的值爲8 */
MyObject2.prototype = new MyObject1( 8 );

/* 
  最後,建立MyOject2類型的一個實例,並傳遞一個值給它的testString屬性
*/ var objectRef = new MyObject2( "String_Value" );

如今,objectRef擁有一個原型鏈,該原型鏈上由近及遠依次是MyOject2.prototype,它是一個testNumber值爲8的MyObject1對象,而後是MyObject1.prototype,這是MyObject類的默認原型(這是一個僅有一個constructor屬性的對象,而且該屬性的值是構造函數MyObejct1()的引用),而後是這個默認原型對象的原型,即Object.prototype,因爲Object.prototype的原型爲null, 所以原型鏈終止。即objectRef的原型鏈爲:

new MyObject1( 8 ) ---> MyOject1.prototype({constructor:MyObject1(formalParameter)}) ---> Object.prototype

當咱們試圖從objectRef所引用的對象(如下簡稱爲objectRef)中讀值時,整個原型鏈都會進入這個搜索過程,例以下面的例子:

var val = objectRef.testString;

因爲objectRef自己有一個名爲testString的屬性,所以該屬性的值被返回("String_Value")被賦給變量val. 然而:

var val = objectRef.testNumber;

 因爲objectRef中並無一個名叫testNumber的屬性,所以js引擎開始檢查objectRef的原型鏈。而後它在原型鏈的第一個對象(new MyObject(8))中找到了一個名爲testNumber的屬性,所以該屬性的值被返回,查找結束。下面:

var val = objectRef.toString;

一樣,objectRef中沒有名爲toString的屬性,所以js引擎轉到原型鏈中查找,因爲原型鏈第一個對象中沒有相應的屬性,因而它接着到第二對象中去找,也沒找到,因而到第三個(Object.prototype)中去找,發現Object.prototype中有這樣一個屬性(是一個函數),因而該屬性的值被返回,查找結束。最後:

var val = objectRef.madeUpProperty;

將返回undefined,由於objectRef自己以及它的原型鏈上的全部對象中都沒有一個名叫madeUpProperty的屬性,所以查找失敗,返回undefined.

從對象中讀取某個命名屬性的值時,將返回從對象自己或它的原型鏈中第一個找到的值;而賦值的過程則只跟這個對象自己有關,若它自己不存在這個命名屬性,則將爲它建立這樣一個屬性。

也就是說,若是咱們試圖進行這樣的賦值操做:

objectRef.testNumber = 3

將會在objectRef自己中建立這樣一個屬性,這以後的任何讀取testNumber值的操做都將直接返回objectRef自己中這個屬性的值,它的原型鏈將再也不被查找。可是它原型鏈中的這個屬性的值不會被影響,依然是8. 只是讀值時objectRef中的testNumber將原型鏈中的同名屬性給遮住了,使它‘不可見’了。

 

執行上下文做用域鏈 和 標識符解析

執行上下文,或運行期上下文(execution context)是ECMAScript規範中的一個抽象的概念,用來定義ECMAScript實現的行爲準則. 可是規範並無明肯定義執行上下文應怎樣被實現,除了指出執行上下文應該包含一些關聯屬性,這些關聯屬性由規範定義。所以執行上下文能夠被看成(甚至是實現)爲一組擁有某些屬性的對象,儘管這些屬性不是public的。

全部js代碼都運行在一個執行上下文中。全局代碼運行在全局執行上下文中;函數(或構造函數)的每一次調用也有相應的執行上下文。經過eval()執行的代碼也有相應的執行上下文,但因爲eval()並不經常使用因此咱們這裏不討論它。關於執行上下文的具體細節請查閱ECMA262規範的10.2小節。

當一個js函數被調用時,js引擎進入一個執行上下文,若是在這期間另外一個函數被調用了,或是該函數遞歸調用了自身,則另外一個執行上下文將被建立,而後js引擎進入這個新的執行上下文,當這個函數執行完畢後,js引擎返回以前的執行上下文,從上次中斷的地方繼續向下執行。這樣的執行機制就造成了一個執行上下文棧

當一個執行上下文被建立時,一系列的事將以固定的順序發生:

  首先,在一個函數的執行上下文中,一個「活動對象」(Activacation Object)將被建立.這個活動對象是ECMAScript規範的又一個機制。它能夠被認爲是一個對象,由於它有一些可訪問的命名屬性。但它又不是一個常規意義上的對象,由於它沒有prototype,而且它不能夠被直接編程訪問。

  其次,一個argument對象被建立,這是一個‘類數組’(array-like)對象,由於它的屬性能夠經過數字索引的方式順序訪問,而且它有一個length屬性和callee屬性(這點已超出咱們的討論範圍,具體請參閱ECMA262規範)。

  而後,第一步建立的活動對象被賦予一個名爲arguments的屬性,該屬性指向第2步中被建立的arguments對象。

  接着,該執行上下文被賦予一個做用域,該做用域是由一系列對象組成的鏈表。每個函數對象都有一個內部的[[scope]]屬性(這個咱們稍後會詳細講解),該[[scope]]屬性的值也是一個對象鏈表。函數被調用時,其執行上下文被賦予的做用域,便是該函數的[[scope]]屬性所指向的對象鏈表。而且,第一步中建立的活動對象將被添加到這個鏈表的頂部。(函數每次執行時都會建立一個新的活動對象,所以活動對象是不一樣,但做用域鏈表的其它部分基本是相同的)

    接下來發生的是'變量實例化'(variable instantiation), 函數的全部參數、內部函數聲明、局部變量和內部函數表達式(這兩個是同級別的,按代碼的前後順序執行)等將依次(注意順序,函數聲明的建立在局部變量以前,所以,同名的變量將覆蓋同名的函數聲明,見下面的test(),這但是一道面試題哦~~)被映射成第一步所建立的活動對象的命名屬性。而後,是爲函數的參數賦值。若是某個參數被傳遞了值,則活動對象的相應命名屬性將被賦予該值,不然該參數對應的命名屬性的值被賦予undefined. 再而後,執行內部函數聲明,活動對象上對應的命名屬性的值將指向這個新建立的內部函數對象。

//這個例子是我加的,爲了幫助你們理解函數的執行過程
function
test() { var a = 1; function a() {} return a; } var t = test(); console.log(typeof t); //number

須要注意的是,局部變量及內部函數表達式的值在'變量實例化'以後都是undefined,變量實例化只是在活動對象中建立了與它們一一對應的命名屬性。這些命名屬性只有在函數執行到對應的賦值語句時纔會真正被賦值。見test2():

function test2() {
console.log(typeof b); //function
var b = 1;
console.log(typeof b); //number
    function b() {
        console.log(123);
    }
  console.log(typeof b); //number
}
test2();

在變量實例化以後, var b =1;執行以前,b被映射成活動對象上的一個同名屬性b, 因爲此時內部函數聲明已經執行完畢,而局部變量b還沒有被賦值,所以此時b的值是一個函數。而在var b = 1;執行以後,b被從新賦值, 又由於函數聲明只在變量實例化時被執行一次,以後不會再被執行,由於在function b()以後和以後,b的類型都是number.  再看下面test3()和test4():

function test3() {
    console.log(typeof c);//undefined
    var c = 1;
    console.log(typeof c);//number
    var c = function() {}
    console.log(typeof c);//function
}
test3();
function test4() {
    console.log(typeof d);//undefined
    var d = function() {}
    console.log(typeof d);//function    
    var d = 1;
    console.log(typeof d);//number
}
test4();

局部變量和函數表達式的執行機制同樣,所以是誰在前誰先被執行,後面的賦值將覆蓋前面的。

  最後,一個叫this的關鍵字被賦值,若是賦予給它的值是一個對象,那麼this.xx將指向該對象中相應的屬性。 若是被賦的值是null(注意這個賦值操做是js引擎內部的機制,咱們是沒法經過編程方式控制的),則this將指向全局對象。

  全局執行上下文有一些特殊,由於它沒有arguments,因此它不須要定義一個活動對象來指向這個arguments. 可是這個全局執行上下文也有做用域,只不過這個做用域鏈中只有一個對象,即全局對象。 另外,全局執行上下文也會執行‘變量實例化’,在這個過程當中,全局對象自己充當了活動對象,這也是爲何,在全局上下文中定義的變量和函數都是全局對象的屬性的緣由了。全局上下文中,this關鍵字指向全局對象。

 

做用域鏈(scope chain)和[[scope]]

 函數調用時的做用域鏈,其實就是經過將活動對象添加到該函數對象的[[scope]]屬性所指向的對象鏈表(下文簡稱爲[[scope]]鏈表)的頂部造成的,所以理解內部的[[scope]]屬性的定義是很重要的。

在JS中,函數也是對象,它們是經過函數聲明在變量實例化期間被建立,或經過函數表達式的在代碼執行期間被建立,或經過調用Function構造函數來建立。

  • 經過Function構造器建立的函數對象, 其[[scope]]鏈表中永遠只有一個對象——全局對象。
  • 經過函數聲明或函數表達式建立的函數對象, 其[[scope]]鏈表指向它們被建立時的執行上下文的做用域鏈。看下面的例子:
function foo(formalParameter){
    ...   // function body code
}

上面這個函數在全局執行上下文的‘變量實例化’期間被建立,所以它的[[scope]]鏈表指向全局做用域。因爲全局做用域鏈表中只有一個全局對象,所以foo()的[[scope]]鏈表中也只有一個全局對象。

一個類似的例子,一樣是在全局環境中,以函數表達式的方式:

var foo = function(){
    ...   // function body code
}

在這個例子中,foo()的[[scope]]鏈表中依然只有一個全局對象,只不過名爲foo的命名屬性是在變量實例化期間添加到全局對象中,而該命名屬性所對應的函數對象是在執行期才被建立的。

 

因爲內部函數聲明和內部函數表達式所對應的函數對象是在外部函數的執行上下文中建立的,所以它們擁有更豐滿的做用域鏈。考慮下面這個例子,在外部函數中定義了一個內部函數聲明,而後執行這個外部函數:

function exampleOuterFunction(formalParameter){
    function exampleInnerFuncitonDec(){
        ... // inner function body
    }
    ...  // the rest of the outer function body.
}

exampleOuterFunction( 5 );
exampleOuterFunction()是全局上下文的’變量實例化‘過程當中被建立的,所以它的做用域鏈等於全局執行上下文:只有一個全局對象,exampleOuterFunction.[[scope]] = 全局對象。當全局上下文執行到exampleOuterFunction(5);這句時,一個新的執行上下文將被建立,同時被建立的還有一個活動對象。
這是exampleOuterFunction()的執行上下文,該執行上下文的做用域鏈由這個新建立的活動對象和exampleOuterFunction的[[scope]]鏈組成,即活動對象--->全局對象。而後是新執行上下文的變量實例化,在這個過程當中,一個名爲exampleInnerFuncitonDec的命名屬性被添加到活動對象上,該屬性的值是一個函數對象。
該函數對象的[[scope]]鏈表被初始化爲當前的執行上下文,即exampleInnerFuncitonDec.[[scope]] = 」活動對象--->全局對象「

到目前爲止,一切都是js引擎自動控制的:執行上下文的做用域鏈定義了它內部函數對象的[[scope]]鏈活動對象 + 執行上下文;內部函數對象的[[scope]]鏈又定義了它自身的執行上下文(內部函數的做用域鏈將是內部活動對象+活動對象 + 執行上下文,不要將做用域鏈和執行上下文混淆了前者是在函數調用時存在的,包含活動對象
可是ECMAScript規範提供了with語句用來改變函數的執行上下文。with語句評估一個表達式,若是這個表達式是一個對象,那個這個對象將被添加到執行上下文(是一個對象鏈表)的頂部(在活動對象以前). 在with語句塊中,執行上下文將被暫時改變:在鏈表頂部添加了一個對象。在with語句執行完畢後,該對象也將從鏈表頂部被刪除,
執行上下文恢復成with語句以前的狀態。函數聲明是不會被with語句影響的,由於它們是在變量實例化期間建立的(而with語句是在代碼執行期間才被評估的),而函數表達式卻能夠,由於它是在代碼執行時才被建立的:
/* 建立一個全局變量y,指向一個對象 */
var y = {x:5}; 
function exampleFuncWith(){
    var z;
    /* 將y所指向的對象添加到做用域鏈的頂部 */
    with(y){
        /*經過函數表達式建立一個函數對象,並將該對象賦值給局部變量z */
        z = function(){
            ... 
        }
    }
    ... 
}

/* 執行外部函數 */
exampleFuncWith();
當exampleFuncWith();執行時,exampleFuncWith的執行上下文被建立,即活動對象--->全局對象。當執行到exampleFuncWith中的with語句時,全局變量y指向的對象被添加到執行上下文鏈頂部,而後z指向的函數對象被建立,該函數對象的[[scope]]鏈被初始化爲當前的執行上下文,即:
y--->活動對象--->全局對象。當with語句結束後,y從執行上下文鏈中移除,但這不會改變z()的[[scope]]鏈,它只會記住它建立時的上下文環境。

標識符解析
標識符是沿着做用域鏈被解析的。ECMA262規範將this做爲一個關鍵字而非標識符,這是有道理的,由於this的值只與它所處的執行上下文有關,而與做用域鏈無關。

標識符解析始於做用域鏈中的第一個對象,js引擎檢查該對象中是否有命名屬性與這個標識符相同。因爲做用域鏈是一個對象鏈,所以這個檢查也包括檢查該對象的原型鏈。也就是說,檢查會按這樣的順序進行:
做用域鏈中的第一個對象--->第一個對象的原型鏈--
->
第二個對象--->第二個對象的原型鏈--->...--->最後一個對象--->最後一個對象的原型鏈。直到在某個對象或它的原型鏈中找到對應的命名屬性,則查找成功,查找終止;或一直到最後一個對象的原型鏈依然沒有找到,則查找失敗,查找終止。

對象屬性的解析與標識符的解析過程一致,此時對象的屬性名至關於標識符。全局對象老是處於做用域鏈的最末端。

因爲函數被調用時,一個活動對象將被放入它的執行期上下文的做用域鏈的頂部,函數的全部參數、局部變量和內部函數都被映射成該活動對象的一個命名屬性。所以,在函數體內,對該函數的參數、局部變量和內部函數的訪問速度是最快的,它們將做爲活動對象的命名屬性被解析。

閉包
自動垃圾回收機制
ECMAScript 使用自動垃圾回收機制規範並沒有定義該機制的細節,所以不一樣的實現間可能會有一些差異,而且已知某些實現給了垃圾回收器一個很是低的優先級想法若是一個對象再也不可訪問(再也不有外界引用指向它),那麼將成爲垃圾回收器的回收目標從此的某個時刻銷燬它並釋放它所佔用的系統資源
一般來說是這個樣子的,當js引擎退出一個執行上下文時,與該執行上下文相關的做用域鏈、活動對象、內部函數對象及任何其它的對象等都再也不可訪問,所以也都成爲垃圾回收的目標。

造成閉包
閉包的造成是經過將一個內部函數的引用賦值給一個外部變量或外部對象的屬性。例如:
//閉包的例子
function
exampleClosureForm(arg1, arg2){ var localVar = 8; function exampleReturned(innerArg){
    return ((arg1 + arg2)/(innerArg + localVar));
}
/* 返回內部函數的引用 */
    return exampleReturned;
}

var globalVar = exampleClosureForm(2, 4);

 如今,經過調用exampleClosureForm()建立的函數對象(exampleReturned)將不能被垃圾回收器回收了。由於有一個全局變量引用了它,如今咱們甚至還能經過globalVar(n)執行它呢。

exampleClosureForm執行時的活動對象(後面簡稱外部活動對象1)---->全局對象。所以,外部活動對象1如今也不能被垃圾回收,由於有外界引用指向它。
閉包就這樣造成了。建立它的執行上下文已經銷燬了。
 事情變得有點複雜了,由於globalVar所引用的函數對象,即exampleReturned(),它的[[scope]]鏈是它被建立時的執行上下文,也就是:以後調用globalVar()時,它的執行上下文做用域鏈的第二個對象就是這個外部活動對象(第一個是globalVar自身相關的活動對象)。這個外部活動對象上的值仍然能夠被讀取和設置(見下面的例子),儘管
//設置活動對象的屬性值
function
outer(){ var a = 1; function inner(b){    a += b; //a的值依然能夠被設置,儘管建立它的執行上下文已經銷燬了      console.log(a); } return inner; } var innerRef = outer(); innerRef(2); //打印出3,是a的新值

在上面閉包的例子中, 第一次調用exampleClosureForm時建立的外部活動對象1將保持exampleClosureForm返回時的狀態,即arg1=2,arg2=4,localVar=8,exampleReturned-->(此符號意爲‘指向’)func obj. 若是exampleClosureForm再次被調用,例如:

var secondGlobalVar = exampleClosureForm(12, 3);

一個新的執行上下文和新的活動對象(如下簡稱外部活動對象2)將被建立,同時一個新的函數對象將被返回, 該函數對象擁有它獨立的[[scope]]鏈:外部活動對象2---->全局對象. 外部活動對象2的狀態爲: arg1=12, arg2=3, localVar=8, 

exampleReturned--->another func obj.

也就是說,exampleClosureForm的第二次調用,造成了一個全新的閉包。這兩次調用中造成的這兩個獨立的函數對象,分別被 globalVar 和 secondGlobalVar 引用,如下簡稱globalVar() 和 secondGlobalVar()。globalVar() 和 secondGlobalVar()均返回一個表達式:((arg1 + arg2)/(innerArg + localVar)). 該表達式中的這幾個標識符是如何被解析的,對理解閉包相當重要。

如今,假設咱們執行globalVar()

globalVar(2);

那麼,一個新的執行上下文和一個新的活動對象將被建立(如下稱爲內部活動對象1),它只有一個命名屬性:innerArg,值爲2。該執行上下文的做用域爲內部活動對象1---->外部活動對象1---->全局對象

因爲標識符沿着做用域鏈被解析,所以,((arg1 + arg2)/(innerArg + localVar))表達式中的幾個標識符將沿着上面的做用域鏈解析。做用域鏈中的第一個對象是內部活動對象1它只有一個innerArg屬性,返回2,其它的幾個標識符都是在外部活動對象1中找到的,分別是arg1=2,arg2=4,localVar=8, 所以globalVar(2)調用返回((2+4)/(2+8)).

接下來,執行secondGlobalVar():

secondGlobalVar(5);

另外一個活動對象(如下稱爲內部活動對象2),innerArg=5, 和另外一個執行上下文,做用域爲:內部活動對象2---->外部活動對象2---->全局對象,被建立與上面一樣的解析方式,所以secondGlobalVar(5)返回((12+3)/(5+8)).

再次執行secondGlobalVar():

secondGlobalVar(100);

新的活動對象(內部活動對象3)和新的執行上下文被建立:內部活動對象3---->外部活動對象2---->全局對象. 注意這和secondGlobalVar(5)調用的區別:做用域鏈頂部的活動對象是不一樣的,但做用域鏈中第二個對象即外部活動對象2是相同的,所以不管secondGlobalVar()被調用多少次,函數的返回表達式中arg1,arg2,localVar的值永遠是12,3和8.

這就是ECMAScript中內部函數在它的生存期內如何引用、訪問外部函數的參數、局部變量、內部函數的機制。該內部函數被建立時的外部活動對象始終處於它的做用域鏈上,直到再也不有外部引用指向該內部函數,這時,該內部函數對象將成爲垃圾回收器的回收目標, 同時它做用域鏈上全部失去外界引用的對象(包括外部活動對象)也將被回收。

內部函數自己也可能有內部函數,所以內部函數中也可能再返回函數進而造成更深層的閉包。每深一層,函數的做用域鏈中就多一個活動對象。ECMAScript規範要求做用域鏈的長度是有限的,可是沒說這個限度具體是多少。不一樣的JS實現(JS引擎)可能設定了不一樣的長度限制但目前爲止沒有具體的值被公開。但這個值遠遠超過了你在代碼中真正想要嵌套的層次。

 

閉包能作什麼?

奇怪的這個問題答案彷佛任何東西一切。閉包使得JavaScript能夠模擬任何東西,由於使用閉包的惟一限制是你想像的力和執確實有點深奧,所以以一些比較實際的東西開始可能比較好

例1:給setTimeout傳遞函數引用

使用閉包的一個常見情景就是在調用一個函數以前給它傳遞參數

例如,將一個函數做爲參數傳遞給setTimeout,使setTimeout在指定時間(由第二個參數指定)以後調用它,這個js中是很常見的。可是,這樣沒法給該函數傳遞參數(固然setTimeout還有另一種用法,即第一個參數是字符串的狀況,這種狀況下能夠將參數拼接在字符串,但這不是咱們要討論的話題)。

爲了在將函數做爲第一個參數傳遞給setTimeout的同時,也能給該函數傳遞參數,咱們能夠調用另外一個函數,這個函數返回內部函數的一個引用,而後將該內部函數的引用做爲第一個參數傳遞給setTimeout. 內部函數執行時所需的參數在調用外部函數時傳入。這樣,setTimeout在執行該內部函數時無需傳遞任何參數,但該內部函數依然能夠訪問外界提供的參數---這些參數是在外部函數被調用時傳入的:

function callLater(paramA, paramB, paramC){
    /* 返回一個匿名的內部函數的引用 */
    return (function(){
        /* 這個內部函數將經過setTimeout被執行,當它執行時,它能夠讀取、設置傳遞給外部函數的參數 */
        paramA[paramB] = paramC;
    });
}

...

/* 調用callLater,返回內部函數的一個引用給局部變量funcRef。傳遞給外部函數callLater的參數將在該內部函數最終被執行時使用 */
var functRef = callLater(elStyle, "display", "none");
/* 將內部函數的引用funcRef做爲第一個參數傳遞setTimeout */ hideMenu=setTimeout(functRef, 500);

 

例2: 將函數與對象的實例方法關聯

(

 There are many other circumstances when a reference to a function object is assigned so that it would be executed at some future time where it is useful to provide parameters for the execution of that function that would not be easily available at the time of execution but cannot be known until the moment of assignment.

 

One example might be a javascript object that is designed to encapsulate the interactions with a particular DOM element. It has doOnClick, doMouseOver and doMouseOut methods and wants to execute those methods when the corresponding events are triggered on the DOM element, but there may be any number of instances of the javascript object created associated with different DOM elements and the individual object instances do not know how they will be employed by the code that instantiated them. The object instances do not know how to reference themselves globally because they do not know which global variables (if any) will be assigned references to their instances.

 

So the problem is to execute an event handling function that has an association with a particular instance of the javascript object, and knows which method of that object to call.

 

The following example uses a small generalised closure based function that associates object instances with element event handlers. Arranging that the execution of the event handler calls the specified method of the object instance, passing the event object and a reference to the associated element on to the object method and returning the method's return value. 

)

這個例子大意明白,但使用閉包的精妙之處沒領會。上面這段話(尤爲第一段最後一句)也沒領會精神,有哪位朋友看懂的,請留言告訴我一下,謝謝。

/* 爲指定的dom元素綁定事件處理程序
  @param Object obj 要綁定事件處理程序的dom元素
@param String methodName 要做爲事件處理程序的方法名稱
@return Function 事件處理程序
*/ function associateObjWithEvent(obj, methodName){   /* 注意: 下面返回的這個方法只有在特定事件發生時,才真正被執行 */
return (function(e){ /* 兼容低版本IE和標準瀏覽器 */ e = e||window.event; /* 下面的this指向事件發生源的dom元素, 由於這個內部函數是做爲該dom元素的事件處理程序被執行的 */ return obj[methodName](e, this); }); } function DhtmlObject(elementId){ var el = getElementWithId(elementId); if(el){ /* 綁定事件處理程序: 將DhtmlObject的指定名稱的方法做爲el的某個事件處理程序 */ el.onclick = associateObjWithEvent(this, "doOnClick"); //這裏的this指向當前的DhtmlObject對象 el.onmouseover = associateObjWithEvent(this, "doMouseOver");//this同上
     el.onmouseout = associateObjWithEvent(this, "doMouseOut"); //this同上
     ...
  }
}
DhtmlObject.prototype.doOnClick
= function(event, element){
  ...
// doOnClick 方法體
}
DhtmlObject.prototype.doMouseOver
= function(event, element){
  ...
// doMouseOver 方法體
}
DhtmlObject.prototype.doMouseOut
= function(event, element){
  ...
// doMouseOut 方法體
}

這樣一來,任何DhtmlObject對象均可以將本身與它們所感興趣的dom元素(經過elementId)結合在一塊兒了,而且不須要知道是否有其它代碼與這個dom元素有關聯,也不須要擔憂會污染全局環境或與其它DhtmlObject對象有衝突。

 

例3: 將有關聯關係的函數封裝在一塊兒

 閉包能夠用來建立做用域以便將相互關聯或相互依賴的代碼組織在一塊兒,同時最小化和其它代碼意外交叉的風險。假設有這樣一個函數,它的做用是構建一個字符串,但要避免重複的字符串鏈接操做以及沒必要要的中間字符串的建立(例如 var a = 'aa'; var b = 'bb' + 'cc' +a; 就會創造'bbcc'這個中間字符串.). 所以咱們能夠將這些字符串片斷按順序放入一個數組,最後調用Array.prototype.join(經過傳遞一個空字符串做爲參數)輸出結果字符串. 這個數組在這裏充當了緩存的做用, 若是咱們在函數內部定義它, 則在函數的每次調用中都會從新建立這個數組, 而這是沒必要要的,由於每次調用中這個數組只有一小部份內容會變化.

另外一個方法就是在全局上下文中定義它, 而後在函數中引用它,這樣這個數組只會被建立一次. 但這樣作的缺點就是不利於維護和代碼複用. 假設咱們在別的工程中也要用到這個方法, 那麼當咱們拷貝函數的代碼時,也同時要記得拷貝函數外的這個數組的代碼, 而且,在新的環境中,除了考慮函數名不能和新環境中有衝突外,還要注意數組名不能衝突.

而利用閉包就能夠優雅地解決這一問題. 閉包能夠優雅地將數組定義和上面這個依賴它的函數封裝在一塊兒,同時沒必要擔憂會污染全局做用域或與其它代碼的衝突.

解決方案就是利用自執行的函數表達式建立一個新的執行上下文,在這個上下文中定義這個數組,並返回一個內部函數, 該內部函數的做用與上面說到的那個函數相同. 代碼以下:

/*
定義一個全局變量, 它指向內部函數的引用, 所以,下面這個函數表達式返回的內部函數能夠在全局做用域中執行.
這個內部函數返回一個HTML字符串, 表明了一個絕對定位的div, 包裹着一個img元素, 全部的變量都做爲參數提供給函數調用
*/ var getImgInPositionedDivHtml = (function(){ /* 將buffAr定義爲外部函數的局部變量, 它只會被建立一次(由於它是外部活動對象的一個命名屬性,具體參見上文中'造成閉包'一節) */ var buffAr = [ '<div id="', '', //index 1, DIV ID attribute '" style="position:absolute;top:', '', //index 3, DIV top position 'px;left:', '', //index 5, DIV left position 'px;width:', '', //index 7, DIV width 'px;height:', '', //index 9, DIV height 'px;overflow:hidden;\"><img src=\"', '', //index 11, IMG URL '\" width=\"', '', //index 13, IMG width '\" height=\"', '', //index 15, IMG height '\" alt=\"', '', //index 17, IMG alt text '\"><\/div>' ]; /* 返回一個內部函數對象, 該函數對象會在每次調用 getImgInPositionedDivHtml( ... )時被執行 */ return (function(url, id, width, height, top, left, altText){ /* 爲數組中對應位置的元素賦值 */ buffAr[1] = id; buffAr[3] = top; buffAr[5] = left; buffAr[13] = (buffAr[7] = width); buffAr[15] = (buffAr[9] = height); buffAr[11] = url; buffAr[17] = altText; /* 返回合併後的字符串 */ return buffAr.join(''); }); //End of 內部函數 })(); //自執行

 若是一個函數依賴於一個(或多個)其它函數, 但這些被這依賴的函數又不想被其它代碼訪問的話,那麼能夠用和上面相同的技巧來處理。即利用閉包將這些函數封裝在一塊兒,只暴露一個入口函數給外部調用。 這樣便優雅地將一堆多函數的面向過程的代碼轉變成了一個封裝良好的、易移植的程序單元。

其它例子

可能閉包最著名的應用之一是Douglas Crockford的 在JS對象中模擬私有實例成員。這篇文章中講述的技巧能夠擴展到各類各樣的數據結構中,包括在JS對象中模擬靜態成員。 (Probably one of the best known applications of closures is Douglas Crockford's technique for the emulation of private instance variables in ECMAScript objects. Which can be extended to all sorts of structures of scope contained nested accessibility/visibility, including the emulation of private static members for ECMAScript objects.)

閉包能夠實現的應用是無止境的,理解它的工做原理是瞭解如何使用它的最佳指導。

 

意外的閉包

任何使內部函數呈如今建立它的函數以外的操做都將造成一個閉包。這使得閉包很是容易被建立,js做者甚至能夠在根本不瞭解閉包的狀況下利用內部函數完成各類各樣的任務,在這種狀況下,因爲沒有明顯跡象,他並不知道本身建立了閉包以及這樣作將有什麼影響。

意外地建立閉包可能產生有害的反作用,例如咱們下一個章節要講的IE內存泄漏問題。此外,意外閉包還可能影響代碼的性能。這並非說閉包自己會對性能產生影響,實際上,若是正確地使用,閉包能夠建立至關高效的代碼。真正對效率產生影響的是內部函數。

一個常見的狀況就是使用內部函數做爲dom元素的事件處理程序。例以下面代碼將用來處理a標籤上的點擊事件:

/* 定義一個全局變量,它的值將被添加到a標籤的href屬性中 */
var quantaty = 5;
/* 爲指定的連接元素添加點擊事件監聽, 同時將全局變量添加到它的href屬性中*/
function addGlobalQueryOnClick(linkRef){ 
    if(linkRef){
        /* 將一個內部函數對象賦值爲linkRef的點擊事件處理程序
        */
        linkRef.onclick = function(){           
            this.href += ('?quantaty='+escape(quantaty));
            return true;
        };
    }
}

每一次addGlobalQueryOnClick被調用時,都將建立一個新的函數對象,而且造成一個閉包(由於linkRef.onclick能夠在addGlobalQueryOnClick以外訪問,而它指向addGlobalQueryOnClick中的建立的那個內部函數對象)。從性能的觀點來看,若是addGlobalQueryOnClick只被調用一兩次,那麼這並非一個大問題; 但若是這個函數被執行N屢次,那麼將會建立N多個徹底獨立但卻功能相同的函數對象(每一個連接元素分別對應一個獨立的函數對象)。

上面的代碼並無也不須要用到閉包的特性,所以生成閉包是沒必要要的。一個和上面的例子效果徹底相同的但卻高效的多的作法是,將做爲事件處理程序的函數在addGlobalQueryOnClick以外獨立定義, 而後將它的引用賦值給各個a元素的onclick屬性。這樣只會建立一個函數對象而後在全部的a元素之間共享它的引用:

var quantaty = 5;

function addGlobalQueryOnClick(linkRef){
   if(linkRef){      
        linkRef.onclick = forAddQueryOnClick;
    }
}

function forAddQueryOnClick(){
    this.href += ('?quantaty='+escape(quantaty));
    return true;
}

鑑於上面第一個例子的內部函數並無利用它自身所造成的閉包的優點,所以這種狀況下,最好的作法就是不使用內部函數, 這樣就不會重複建立多個徹底同樣的函數對象。

一個相似的例子是對象的構造函數,下面的代碼並很多見:

function ExampleConst(param){  
//對象的方法
this.method1 = function(){//這個賦值操做將造成一個閉包,由於this.method1能夠在構造函數以外訪問 ... // method body. }; this.method2 = function(){//同上,這個賦值也將造成一個閉包 ... // method body. }; this.method3 = function(){//同上,這個賦值也將造成一個閉包 ... // method body. }; /* 將參數賦值給對象的屬性 */ this.publicProp = param; }

每次調用new ExampleConst(x)建立對象時,一組新的函數對象將被建立並分別賦值給對象的成員方法,這樣,有越多的ExampleConst實例被建立,就有越多的函數對象被建立。

Douglas Crockford的在js對象中模擬私有成員的技巧利用了閉包的特性,而若是對象的方法並無利用到閉包的優點,那麼在構造函內部給對象的方法賦值的操做將影響代碼的執行效率而且會消耗更多的資源(由於會有多餘的函數對象被建立).

此時,更高效的作法是隻建立這些函數對象一次,而後把它們的引用分別賦值給構造函數的原型的相應屬性, 這樣它們就能夠在構造函數所建立出的全部實例中被共享了:

function ExampleConst(param){
    /* 將參數賦值給對象的屬性,這個屬性是每一個對象獨有的,即每次調用構造函數都會建立一個新的屬性 */
    this.publicProp = param;
}
/* 經過構造函數的原型給對象添加方法 */
ExampleConst.prototype.method1 = function(){
    ... // method body.
};
ExampleConst.prototype.method2 = function(){
    ... // method body.
};
ExampleConst.prototype.method3 = function(){
    ... // method body.
};

 

IE內存泄漏問題

在IE4~6中,若是某些宿主對象間存在循環引用的話,則垃圾回收器不會回收這個循環當中的任何一個對象,從而會致使內存泄漏問題(此問題在IE7中已解決)。這裏的宿主對象指任意的Dom對象(包括document對象及它的全部後代)和ActiveX對象。循環鏈中的對象不會被回收,從而它們所佔用的內存和其它系統資源也不會被釋放,直到瀏覽器關閉。

循環引用是指兩個或多個對象間的引用造成一條鏈,最後又指回開始的那個對象。例如對象1有一個屬性指向對象2,對象2有一個屬性指向對象3,而對象3又有一個屬性指向對象1。 若是這個循環鏈中都是純的js對象(即不包含DOM對象或ActiveX對象),那麼當連接以外沒有引用指向它們時,這條鏈是被會釋放的; 但若是這個鏈中存在任何的DOM對象或ActiveX對象,那麼IE4~6的垃圾回收器將識別不出來這是一個自引用的循環鏈,所以也不會釋放它們,這條鏈中的全部對象所佔的內存和資源直到瀏覽器關閉才能被釋放.

閉包尤爲容易造成循環引用。當一個函數造成閉包時,例如,被看成右值賦值給一個Dom對象的事件處理程序, 而且這個Dom對象的引用存在於該閉包函數的scope鏈的一個活動對象的命名屬性中時,一個閉包就造成了:DOM_Node.onevent -> function_object.[[scope]] -> scope_chain -> Activation_object.nodeRef -> DOM_Node.這是很容易的發生的(見下面的例子),而且在一個大的網站中,當每一個頁面中都存在不少段相似這樣的代碼時,系統的大部分(甚至是所有)內存將被消耗掉。

咱們應足夠謹慎以免造成循環引用,當確實沒法避免時,咱們能夠採起必定的措施來彌補,例如在IE的unload事件中將全部事件監聽程序設爲null. 認識到這個問題並理解閉包和閉包的機制是避免在IE中引起這類問題的關鍵。

//例:由閉包造成的循環引用    
(function(){ var b=document.body; // ← 建立docement.body的引用 b.onclick=function() { // ← b.onclick 指向一個函數對象 // 這個函數對象的[[scope]]鏈爲: 活動對象(有一個名爲b的命名屬性)--->全局對象. 這就造成了循環引用鏈,由於: document.body.onclick--->function.[[scope]]-->活動對象.b--->document.body // do something... }; })();

關於IE內存泄漏更詳細的講解和例子能夠參考 http://isaacschlueter.com/2006/10/msie-memory-leaks/

相關文章
相關標籤/搜索