1. 程式人生 > >談一談對JS閉包的理解

談一談對JS閉包的理解

   個人覺得理解閉包,首先要理解以下幾個概念。

1、函式的作用域和作用域鏈

js不像java等其他類語言,它並不存在塊級作用域,取而代之的是函式作用域,另一個變數作用域是全域性作用域。

函式的作用域:變數在宣告它們的函式體以及這個函式體巢狀的任意函式體內都是有定義的。(摘自犀牛書P57)

在這裡應該注意兩點:一是函式作用域中宣告提前的特性,見程式碼:

var scope="global"
function f(){
  console.log(scope);//輸出結果"undefined"</span>
  var scope="local";
  console.log(scope);//輸出結果"local"</span>
}

其實以上程式碼就相當於:
function f(){
  var scope; //變數宣告提前
  console.log(scope);//輸出結果"undefined"
  scope="local";//賦值留在原地
  console.log(scope);//輸出結果"local"
}

這就是函式的宣告提前。

需要注意的第二點就是:在函式作用域內不用var宣告變數,而是直接賦值給一個變數,那麼該變數就會成為全域性變數。但還不僅僅是這樣,千萬不要把該全域性變數完全等同於在全域性環境中用var宣告的全域性變數,兩者還是有一定區別的。話不多說,上程式碼:

var turevar=1//生命同一個不可刪除的全域性變數
function fun(){
  fakevar=2//在函式作用域內宣告一個全域性變數
}
this.fakevar1=3//同上
delete truevar//=>false 變數並沒有被刪除
delete fakevar//=>true 變數被刪除
delete fakevar1//=>true 變數被刪除

簡單來說:var宣告的全域性變數,實際上定義了一個全域性變數的屬性,而該屬性是不可配置的,也就是這個變數無法通過delete運算子刪除。而未宣告直接賦值的全域性變數是可配置的屬性,會被delete運算子刪除。但是在嚴格模式下,第二種會報ReferenceError的錯。

2、js中的執行環境和ECMAscript的執行流機制

執行環境:

定義了變數和函式有權訪問的其他資料,決定了他們各自的行為。PS:每個執行環境都有一個與之相連的變數物件,如果這個環境是函式,則將其活動物件作為變數物件。

ECMAscript的執行流機制:

每個函式都有自己的執行環境,當執行流進入一個函式時,函式的環境就會被推入一個環境棧中。而在函式執行之後,棧將其環境彈出(該函式的執行環境銷燬,儲存在器重的所有變數核函式定義也隨之銷燬)——摘自:《JavaScript高階程式設計》P73


為了理解這兩個概念看如下的程式碼:

<span style="font-size:18px;">var color="blue";
function changeColor(){
  var anotherColor="red";
    function swapColor(){
      var tempColor=anotherColor;
      anotherColor=color;
      color=tempColor;
    }
}</span>
以上程式碼涉及三個執行環境:全域性環境、changeColor()的區域性環境和swapColors()的區域性環境,如圖所示:


該函式的作用域鏈圖如上圖所示,其中的矩形(臥槽好難看!)表示特定的執行環境。內部環境可以通過作用鏈訪問所有的外部環境,但外部環境不能訪問內部環境中的任何變數和函式。這些環境直接的聯絡是線性,有次序的,每個環境都可以向上搜尋作用域鏈,以查詢變數和函式名;但任何環境都不能通過向下搜尋作用域鏈而進入另一個執行環境。

最後要補充的一點是:JavaScript採用詞法作用域,也就是說,函式的執行依賴於變數作用域,這個作用域是在函式定義時決定的,而不是函式呼叫時決定的。

在充分理解了以上幾個概念之後,那麼對於js中閉包的理解也就沒那麼困難了。

首先,閉包的概念:

函式物件可以通過作用域鏈相互關聯起來,函式體內的變數可以儲存在函式作用域內,這種特性被稱為閉包。

為了更直觀的瞭解閉包,先上一段程式碼:

var scope='global scope';//全域性變數
function checkscope(){
  var scope="local scope";//區域性變數
  function f(){return scope;}//在作用域中返回這個值
  return f();
}
checkscope()//=>輸出結果local scope
理解以上程式碼並不難,現在對這段程式碼做一點的改動
var scope='global scope';
function checkscope(){
  var scope="local scope";
  function f(){return scope;}
  return f;
}
checkscope()();
我們知道呼叫函式checkscope(),我們得到的不再是一個scope值,而是一個函式f;再接著呼叫函式f(),這裡就出現了一個問題,呼叫函式f是在全域性環境中呼叫,其中的scope值從何而來?上面提到過一個概念:js是採用的詞法作用域,該scope值還是來自於該函式即f定義時的作用域,而不是呼叫時的作用域。所以結果還是local scope。

此外,在犀牛書上還遇見一個例子比較經典。見程式碼:

function constfuncs(){
  var funcs=[];
  for(var i=0;i<10;i++){
    funcs[i]=function{ return i ;}
  }
    return funcs;
}
var funcs=constfuncs();
funcs[5]()

此時,結果的返回值是10;

結合上面的概念一步一步分析:

首先,constfuncs()函式執行以後得到的是funcs陣列,陣列中是十個未執行的函式function(){return i}。當呼叫funcs[5]()的時候,變數i的值應該是函式在定義的時候i的取值,又因為直到i=10的時候迴圈才停止,所以,最後i的取值是10.函式呼叫返回值也是10。

從這裡不能看出:關聯到閉包的作用域鏈是“活動的”。巢狀函式不會將作用域內的私有成員複製一份,也不會對所繫結的變數生成靜態快照。