JavaScript 閉包到底是什麼

用JavaScript一年多了,閉包老是讓人二丈和尚摸不着頭腦。陸陸續續接觸了一些閉包的知識,也犯過幾回由於不理解閉包致使的錯誤,一年多了資料也看了一些,但仍是不是很是明白,最近偶然看了一下 jQuery基礎教程 的附錄,發現附錄A對JavaScript的閉包的介紹簡單易懂,因而借花獻佛總結一下。閉包

 

1.簡單的例子函數

首先從一個經典錯誤談起,頁面上有若干個div, 咱們想給它們綁定一個onclick方法,因而有了下面的代碼spa

複製代碼

<div id="divTest">
        <span>0</span> <span>1</span> <span>2</span> <span>3</span>
    </div>
    <div id="divTest2">
        <span>0</span> <span>1</span> <span>2</span> <span>3</span>
    </div>

複製代碼

複製代碼

$(document).ready(function() {
            var spans = $("#divTest span");
            for (var i = 0; i < spans.length; i++) {
                spans[i].onclick = function() {
                    alert(i);
                }
            }
        });

複製代碼

很簡單的功能但是卻恰恰出錯了,每次alert出的值都是4,簡單的修改就好使了對象

複製代碼

var spans2 = $("#divTest2 span");
        $(document).ready(function() {
            for (var i = 0; i < spans2.length; i++) {
                (function(num) {
                    spans2[i].onclick = function() {
                        alert(num);
                    }
                })(i);
            }
        });

複製代碼

2.內部函數教程

讓咱們從一些基礎的知識談起,首先了解一下內部函數。內部函數就是定義在另外一個函數中的函數。例如:ip

function outerFn () {
    functioninnerFn () {}
}

innerFn就是一個被包在outerFn做用域中的內部函數。這意味着,在outerFn內部調用innerFn是有效的,而在outerFn外部調用innerFn則是無效的。下面代碼會致使一個JavaScript錯誤:內存

 

複製代碼

function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                document.write("Inner function<br/>");
            }
        }
        innerFn();

複製代碼

不過在outerFn內部調用innerFn,則能夠成功運行:作用域

複製代碼

function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                document.write("Inner function<br/>");
            }
            innerFn();
        }
        outerFn();

複製代碼

 

 

2.1偉大的逃脫開發

JavaScript容許開發人員像傳遞任何類型的數據同樣傳遞函數,也就是說,JavaScript中的內部函數可以逃脫定義他們的外部函數。it

逃脫的方式有不少種,例如能夠將內部函數指定給一個全局變量:

複製代碼

var globalVar;
        function outerFn() {
            document.write("Outer function<br/>");          
            function innerFn() {
                document.write("Inner function<br/>");
            }
            globalVar = innerFn;
        }
        outerFn();
        globalVar();

複製代碼

 

調用outerFn時會修改全局變量globalVar,這時候它的引用變爲innerFn,此後調用globalVar和調用innerFn同樣。這時在outerFn外部直接調用innerFn仍然會致使錯誤,這是由於內部函數雖然經過把引用保存在全局變量中實現了逃脫,但這個函數的名字依然只存在於outerFn的做用域中。

也能夠經過在父函數的返回值來得到內部函數引用

複製代碼

function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                document.write("Inner function<br/>");
            }
            return innerFn;
        }
        var fnRef = outerFn();
        fnRef();

複製代碼

這裏並無在outerFn內部修改全局變量,而是從outerFn中返回了一個對innerFn的引用。經過調用outerFn可以得到這個引用,並且這個引用能夠能夠保存在變量中。

 

這種即便離開函數做用域的狀況下仍然可以經過引用調用內部函數的事實,意味着只要存在調用內部函數的可能,JavaScript就須要保留被引用的函數。並且JavaScript運行時須要跟蹤引用這個內部函數的全部變量,直到最後一個變量廢棄,JavaScript的垃圾收集器才能釋放相應的內存空間(紅色部分是理解閉包的關鍵)。

 

說了半天總算和閉包有關係了,閉包是指有權限訪問另外一個函數做用域的變量的函數,建立閉包的常見方式就是在一個函數內部建立另外一個函數,就是咱們上面說的內部函數,因此剛纔說的不是廢話,也是閉包相關的 ^_^

 

1.2變量的做用域

內部函數也能夠有本身的變量,這些變量都被限制在內部函數的做用域中:

複製代碼

function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                var innerVar = 0;
                innerVar++;
                document.write("Inner function\t");
                document.write("innerVar = "+innerVar+"<br/>");
            }
            return innerFn;
        }
        var fnRef = outerFn();
        fnRef();
        fnRef();
        var fnRef2 = outerFn();
        fnRef2();
        fnRef2();

複製代碼

 

每當經過引用或其它方式調用這個內部函數時,就會建立一個新的innerVar變量,而後加1,最後顯示

複製代碼

Outer function
Inner function    innerVar = 1
Inner function    innerVar = 1
Outer function
Inner function    innerVar = 1
Inner function    innerVar = 1

複製代碼

 

內部函數也能夠像其餘函數同樣引用全局變量:

複製代碼

var globalVar = 0;
        function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                globalVar++;
                document.write("Inner function\t");
                document.write("globalVar = " + globalVar + "<br/>");
            }
            return innerFn;
        }
        var fnRef = outerFn();
        fnRef();
        fnRef();
        var fnRef2 = outerFn();
        fnRef2();
        fnRef2();

複製代碼

 

如今每次調用內部函數都會持續地遞增這個全局變量的值:

複製代碼

Outer function
Inner function    globalVar = 1
Inner function    globalVar = 2
Outer function
Inner function    globalVar = 3
Inner function    globalVar = 4

複製代碼

 

 

可是若是這個變量是父函數的局部變量又會怎樣呢?由於內部函數會引用到父函數的做用域(有興趣能夠了解一下做用域鏈和活動對象的知識),內部函數也能夠引用到這些變量

複製代碼

function outerFn() {
            var outerVar = 0;
            document.write("Outer function<br/>");
            function innerFn() {
                outerVar++;
                document.write("Inner function\t");
                document.write("outerVar = " + outerVar + "<br/>");
            }
            return innerFn;
        }
        var fnRef = outerFn();
        fnRef();
        fnRef();
        var fnRef2 = outerFn();
        fnRef2();
        fnRef2();

複製代碼

 

這一次結果很是有意思,也許或出乎咱們的意料

複製代碼

Outer function
Inner function    outerVar = 1
Inner function    outerVar = 2
Outer function
Inner function    outerVar = 1
Inner function    outerVar = 2

複製代碼

咱們看到的是前面兩種狀況合成的效果,經過每一個引用調用innerFn都會獨立的遞增outerVar。也就是說第二次調用outerFn沒有繼續沿用outerVar的值,而是在第二次函數調用的做用域建立並綁定了一個一個新的outerVar實例,兩個計數器徹底無關。

當內部函數在定義它的做用域的外部被引用時,就建立了該內部函數的一個閉包。這種狀況下咱們稱既不是內部函數局部變量,也不是其參數的變量爲自由變量,稱外部函數的調用環境爲封閉閉包的環境。從本質上講,若是內部函數引用了位於外部函數中的變量,至關於受權該變量可以被延遲使用。所以,當外部函數調用完成後,這些變量的內存不會被釋放(最後的值會保存),閉包仍然須要使用它們。

 

3.閉包之間的交互

當存在多個內部函數時,極可能出現意料以外的閉包。咱們定義一個遞增函數,這個函數的增量爲2

複製代碼

function outerFn() {
            var outerVar = 0;
            document.write("Outer function<br/>");
            function innerFn1() {
                outerVar++;
                document.write("Inner function 1\t");
                document.write("outerVar = " + outerVar + "<br/>");
            }

            function innerFn2() {
                outerVar += 2;
                document.write("Inner function 2\t");
                document.write("outerVar = " + outerVar + "<br/>");
            }
            return { "fn1": innerFn1, "fn2": innerFn2 };
        }
        var fnRef = outerFn();
        fnRef.fn1();
        fnRef.fn2();
        fnRef.fn1();
        var fnRef2 = outerFn();
        fnRef2.fn1();
        fnRef2.fn2();
        fnRef2.fn1();

複製代碼

咱們映射返回兩個內部函數的引用,能夠經過返回的引用調用任一個內部函數,結果:

複製代碼

Outer function
Inner function 1    outerVar = 1
Inner function 2    outerVar = 3
Inner function 1    outerVar = 4
Outer function
Inner function 1    outerVar = 1
Inner function 2    outerVar = 3
Inner function 1    outerVar = 4

複製代碼

 

innerFn1和innerFn2引用了同一個局部變量,所以他們共享一個封閉環境。當innerFn1爲outerVar遞增一時,久違innerFn2設置了outerVar的新的起點值,反之亦然。咱們也看到對outerFn的後續調用還會建立這些閉包的新實例,同時也會建立新的封閉環境,本質上是建立了一個新對象,自由變量就是這個對象的實例變量,而閉包就是這個對象的實例方法,並且這些變量也是私有的,由於不能在封裝它們的做用域外部直接引用這些變量,從而確保了了面向對象數據的專有性。

 

3.解惑

如今咱們能夠回頭看看開頭寫的例子就很容易明白爲何第一種寫法每次都會alert 4了。

for (var i = 0; i < spans.length; i++) {
           spans[i].onclick = function() {
               alert(i);
           }
       }

 

上面代碼在頁面加載後就會執行,當i的值爲4的時候,判斷條件不成立,for循環執行完畢,可是由於每一個span的onclick方法這時候爲內部函數,因此i被閉包引用,內存不能被銷燬,i的值會一直保持4,直到程序改變它或者全部的onclick函數銷燬(主動把函數賦爲null或者頁面卸載)時纔會被回收。這樣每次咱們點擊span的時候,onclick函數會查找i的值(做用域鏈是引用方式),一查等於4,而後就alert給咱們了。而第二種方式是使用了一個當即執行的函數又建立了一層閉包,函數聲明放在括號內就變成了表達式,後面再加上括號括號就是調用了,這時候把i當參數傳入,函數當即執行,num保存每次i的值。

這一通下來想必你們也和我同樣,對閉包有所瞭解了吧,固然徹底瞭解的話須要把函數的執行環境和做用域鏈搞清楚 ^_^

相關文章
相關標籤/搜索