文章出處

JS閉包的理解

 

一、變量的作用域

二、如何從外部讀取局部變量

三、什么是閉包

四、深入理解閉包

五、閉包的用途

六、使用閉包注意情況

七、JavaScript的垃圾回收機制

八、一些思考題

 

一、變量作用域

1、要理解閉包,我們先來看一下JavaScript的特殊的變量作用域。

變量的作用域無非是兩種,全局變量和局部變量。

JavaScript語言的獨特之處是:函數內部可以讀取所有的全局變量。

 var n=999;
  function f1(){
    alert(n);
  }
  f1(); // 999

 

函數外部無法讀取函數內部的局部變量。

function f1(){
    var n=999;
  }
  alert(n); // error

 

這里有一個地方需要注意,函數內部聲明變量的時候,一定要使用var命令。如果不用的話,你實際上聲明了一個全局變量!

function f1(){
    n=999;
  }

  f1();

  alert(n); // 999

 

再來看一個例子,理解一下變量作用域

function a(){
  var n = 0;
  function inc() {
    n++;
    console.log(n);
  }
  inc(); 
  inc(); 
}
a(); //控制臺輸出1,再輸出2

 

function a(){
  var n = 0;
  this.inc = function () {
    n++; 
    console.log(n);
  };
}
var c = new a();
c.inc();  //控制臺輸出1
c.inc();  //控制臺輸出2

 

function a(){
  var n = 0;
  function inc(){
    n++; 
    console.log(n);
  }
  return inc;
}
var c = a();
c();  //控制臺輸出1
c();  //控制臺輸出2

注意:函數名只是一個標識(指向函數的指針),而()才是執行函數

 

二、如何從外部讀取局部變量

從局部變量讀取外部全局變量是JavaScript語言的特點,但是有時我們需要從外部讀取函數內部的局部變量,那怎么辦呢?解決辦法:在函數內部定義一個函數。

function f1(){
    n=999;
    function f2(){
      alert(n); // 999
    }
  }

      在函數f1()中定義一個f2()函數。上述代碼中,函數f2倍包括早函數f1內部,這時候,f1的內部局部變量f2都可見,反過來,f2的布局變量,f1不可見。這個就是JavaScript語言特有的“鏈式作用域”的結構---子對象會一級級的向上尋找所有的父對象的變量(即,所有的父對象的變量,子對象都是可見的)。

既然f2可以讀取f1中的局部變量,那么只要把f2作為返回值,我們就可以在f1的外部就可以讀取f1內部變量了。

function f1(){
    n=999;
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999

 

三、什么是閉包

閉包就是可以讀取其他函數內部的變量。

由于,JavaScript語言中,只有函數內部的子函數才能讀取局部變量,也可以說,閉包就是“定義在一個函數內部的函數”。

所以,本質上說,閉包就是講函數內部很函數外部連接起來的一座橋梁。

JavaScript中所有的function都是一個閉包。不過一般來說,嵌套的function所產生的閉包更為強大,也是大部分時候我們所謂的“閉包”。看下面這段代碼:

function a() { 
 var i = 0; 
 function b() { alert(++i); } 
 return b;
}
var c = a();
c();

這段代碼有個特點:

1、函數b嵌套在函數a內部

2、函數a返回函數b

執行代碼c=a();實際c指向函數b

再執行c();就會彈出一個窗口顯示 i 的值(第一次為1)。

這段代碼其實就是創建了一個閉包,因為函數a外的變量c引用了函數a內的函數b,

當函數a 的 內部函數b  被  函數a  外的  一個變量引用的時候,就創建了一個閉包。

 

四、深入理解閉包  

如果要深入理解閉包和嵌套函數b的關系,我們需要引入:函數的執行環境(excution context)、活動對象c(call object)、作用域(scope)、作用域鏈(scope chain),以函數a為例,從定義到執行過程

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和3),用一段代碼來說明這個:

function f(x) { 
  var g = function () { return x; }
  return g;
}
var h = f(1);
alert(h()); 

這段代碼中,變量h指向了 f 中的那個匿名函數(由g返回)。

1、假設函數h的作用域是在執行alert(h())確定的,那么此時h的作用域鏈是:h的活動對象-》alert活動對象-》window對象。

2、假設函數h的作用域是在定義時候確定的就是說h指向的那個匿名函數在定義的時候就已經確定確定了作用域。那么在執行的時候,h的作用域鏈為:h的活動對象-》f的活動對象-》window活動對象。

如果第一種假設成立,anemia輸出值就是undefined;如果第二種假設成立,則輸出值為1.

運行結果證明了第二個假設是正確的,說明函數的作用域確實在定義這個函數的時候就已經確定了

 

五、閉包的用途

接上個例子,

閉包的作用就是在a執行完并返回后,閉包使得Javascript的垃圾回收機制GC不會收回a所占用的資源,因為a的內部函數b的執行需要依賴a中的變量。由于閉包的存在使得函數a返回后,a中的i始終存在,這樣每次執行c(),i都是自加1后alert出i的值。

 閉包有很多用途,最大的兩個好處:

(一)讀取函數內部的變量

(二)讓這些變量始終保存在內存中。

 

 理解“(二)讓這些變量始終保存在內存中”這句話,看一下下面的額代碼。

 function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

這段代碼中,result實際上是閉包f2函數。result一共運行了兩次,第一次是999,第二次是1000.

這說明,函數f1中的局部變量n一直保存在內存中,并沒有在f1調用后進行自動清除。

原因:f1是f2的父函數,而f2被賦給一個全局變量,這導致f2一直在內存中,f2依賴于f1,因此f1始終在內存中,不會再調用結束之后,被垃圾回收機制回收。

注意:nAdd=function(){n+=1}這個函數,首先這個函數沒有用var關鍵字,因此nAdd是一個全局變量,而不是局部變量。其次,nAdd的值是一個匿名函數,這個匿名函數本身就是一個閉包,可以在函數外部對函數的內部今次進行操作。

 

六、使用閉包的注意情況

(1)閉包會是的函數中的變量倍保存在內存中,內存消耗大,所以不能濫用閉包。否則會造成網頁性能問題,在IE中可能存在內存泄漏,解決辦法,在函數退出之前,將不使用的局部變量全部刪除。

(2)閉包會在父函數的外部,改變父函數變量內部的值。所以你把父函數當做對象object使用,把閉包當做她的公用方法,把內部變量當做他的私有屬性,不要隨便改變父函數內部變量的值。

 

七、JavaScript的垃圾回收機制

在JavaScript中,如果一個對象不再被引用,那么這個對象就會被GC回收。如果兩個對象相互引用,而不再被第三者所引用,那么這兩個相互引用的對象就會被回收。因為函數a被b引用,b又被a外的c引用,這就是為什么函數a執行后不會被回收的原因。

 

八、一些思考題

1、使用對象的閉包使用

var name="The Window";
var object={
     name:"My object",
     getNameFunc:function(){
     return function(){
           return this.name;
          };
        }
      };
alert(object.getNameFunc()());  //The Window,修改的地方

2、直接調用函數內部的函數,報錯,需要使用閉包。

function outerFun()
 {
  var a=0;
  function innerFun()
  {
   a++;
   alert(a);
  }    
 }
innerFun()

上面的代碼是錯誤的.innerFun()的作用域在outerFun()內部,所在outerFun()外部調用它是錯誤的.

改成下面,就可行了

function outerFun()
{
 var a=0;
 function innerFun()
 {
  a++;
  alert(a);
 }
 return innerFun;  //注意這里
}
var obj=outerFun();
obj();  //結果為1
obj();  //結果為2
var obj2=outerFun();
obj2();  //結果為1
obj2();  //結果為2

3、

function outerFun()
{
 var a =0;
 alert(a);  
}
var a=4;
outerFun();   //結果為0
alert(a);     //結果為4

因為函數內部使用了關鍵字var維護了a的作用域在outFun()內。

function outerFun()
{
 //沒有var 
 a =0;
 alert(a);  
}
var a=4;
outerFun();
alert(a);

      作用域鏈是描述一種路徑的術語,沿著該路徑可以確定變量的值 .當執行a=0時,因為沒有使用var關鍵字,因此賦值操作會沿著作用域鏈到var a=4;  并改變其值。


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()