javascript筆記整理系列 - 閉包

1 什麼是閉包

1.1 作用域鏈

  1. 函數的執行依賴於變量作用域,這個作用域是在創建函數定義是決定的,而不是在調用時決定的。
     要理解閉包,就必須先了解作用域鏈。看代碼
function f1() {    
    var n = 999;    
    function f2() {
        var m = 1;      
        alert(n);    //999
    }  
    alert(m);        //error:無法訪問變量m。
}

從上面代碼我們可以看出,f2可以訪問f1內局部變量,但反過來不行。

1.2 從外部獲取函數局部變量

 還是上面的函數,如果我們希望從外部獲取f1函數的變量n,就像在f1中獲取f2的局部變量m一樣不可行。但是f2何以獲取n,我們利用這一點來獲取變量n。

function f1() {    
    var n = 999;    
    function f2() {   
        console.log(n);
    }
    return f2;
}
f1()();    //999

1.3 閉包的概念

閉包概念
  閉包是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
其他理解
  通過1.2我們瞭解如何從函數外訪問函數內的變量,例子中的f2就是閉包。閉包連接了函數外與函數的作用域。所以在本質上,閉包就是將函數內部和函數外部連接起來的一座橋樑。有權訪問另一個函數作用域內變量的函數都是閉包
個人理解
  我們也可以通過”閉包”這兩個字面來理解。拿上面的代碼來說,有人會提出質疑:既然在javascript中函數也是對象,那執行f1()返回的f2函數也只是簡單的將f2函數當作對象返回。這種質疑沒錯,f1函數中確實將f2函數當作對象返回,但重要的時,f1的return同時將內部作用域也一起返回出來,而並不是僅僅返回f2對象。如此說來,”包”這個字就好理解了:將作用域及f2”打包”返回。而”閉”則代表封閉的意思,也就是f1函數的作用域封閉。

2 閉包的作用

總結起來,閉包有倆作用:
1. 可在外部讀取函數內部的變量。
2. 讓閉包所佔用的資源始終保存在內存中,不會被垃圾回收機制GC回收。。

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

3 Javascript的垃圾回收機制

  在Javascript中,如果一個對象不再被引用,那麼這個對象就會被GC回收。如果兩個對象互相引用,而不再被第3者所引用,那麼這兩個互相引用的對象也會被回收。

4 詳解閉包

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

  如果要更加深入的瞭解閉包以及函數a和嵌套函數b的關係,我們需要引入另外幾個概念:
函數的執行環境(excution context)、活動對象(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回收。

5 閉包的應用場景

  1. 保護函數內的變量安全。
  2. 在內存中維持一個變量。
  3. 通過保護變量的安全實現JS私有屬性和私有方法(不能被外部訪問)。
var counter = function () {
    var n = 0;
    return {
        count: function () { return n++; },
        reset: function () { n = 0; }
    }
}
var c = counter(), d = counter();
c.count();  //0
d.count();  //0
c.reset();
c.count();  //1
d.count();  //0

每次調用counter()都會創建一個新的作用域鏈和一個新的私有變量,所以c和d的私有變量互不干擾。

6 通過閉包實現getter和setter

function counter(n){
    return {
        get count() { return n++; },
        set count(m){
            n = m;
        }
    };
}
var a = counter(100);
console.log(a.count);   //100
a.count = 200;
console.log(a.count);   //200
  1. get set關鍵字爲ES5添加,支持IE9+。

7 閉包出現的問題

  1. 由於閉包將變量式中保存在內存中,可能會導致內存溢出。
  2. 閉包可以訪問函數內的局部變量,所以可能會錯誤的修改局部變量。
  3. 不希望共享的變量共享了其他的閉包:
function constfuncs(){
    var funcs = [];
    for(var i=0; i<10; i++){
        funcs[i] = function(){ return i; };
    }
    return funcs;
}

var funcs = constfuncs();
funcs[5]();     //結果是?

上面函數利用for循環創建了多個閉包,但這些閉包內返回的是constcuncs函數內的局部變量i。其實只有一個變量i,每一個閉包返回的都是這個變量i。最後i爲10跳出循環,所以funcs5的結果是10。

發佈了79 篇原創文章 · 獲贊 26 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章