前端 知識點 閉包

閉包的概念

各種專業文獻上的“閉包”(closure)定義非常抽象,很難看懂。我的理解是,閉包就是能夠讀取其他函數內部變量的函數。

由於在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成“定義在一個函數內部的函數”。

所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋樑。

分析下面的代碼:

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

這段代碼有兩個特點:

  • 函數b嵌套在函數a內部;
  • 函數a返回函數b。

這樣在執行完var c=a()後,變量c實際上是指向了函數b,再執行c()後就會彈出一個窗口顯示i的值(第一次爲1)。這段代碼其實就創建了一個閉包,爲什麼?因爲函數a外的變量c引用了函數a內的函數b,就是說:

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

讓我們說的更透徹一些。所謂“閉包”,就是在構造函數體內定義另外的函數作爲目標對象的方法函數,而這個對象的方法函數反過來引用外層函數體中的臨時變量。這使得只要目標對象在生存期內始終能保持其方法,就能間接保持原構造函數體當時用到的臨時變量值。儘管最開始的構造函數調用已經結束,臨時變量的名稱也都消失了,但在目 標對象的方法內卻始終能引用到該變量的值,而且該值只能通這種方法來訪問。即使再次調用相同的構造函數,但只會生成新對象和方法,新的臨時變量只是對應新 的值,和上次那次調用的是各自獨立的。

簡單的例子

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

<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);
            }
        });

上面代碼在頁面加載後就會執行,當i的值爲4的時候,判斷條件不成立,for循環執行完畢,但是因爲每個span的onclick方法這時候爲內部函數,所以i被閉包引用,內存不能被銷燬,i的值會一直保持4,直到程序改變它或者所有的onclick函數銷燬(主動把函數賦爲null或者頁面卸載)時纔會被回收。

這樣每次我們點擊span的時候,onclick函數會查找i的值(作用域鏈是引用方式),一查等於4,然後就alert給我們了。而第二種方式是使用了一個立即執行的函數又創建了一層閉包,函數聲明放在括號內就變成了表達式,後面再加上括號括號就是調用了,這時候把i當參數傳入,函數立即執行,num保存每次i的值。

內部函數

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();

變量的作用域

內部函數也可以有自己的變量,這些變量都被限制在內部函數的作用域中:

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實例,兩個計數器完全無關。

當內部函數在定義它的作用域的外部被引用時,就創建了該內部函數的一個閉包。這種情況下我們稱既不是內部函數局部變量,也不是其參數的變量爲自由變量,稱外部函數的調用環境爲封閉閉包的環境。從本質上講,如果內部函數引用了位於外部函數中的變量,相當於授權該變量能夠被延遲使用。因此,當外部函數調用完成後,這些變量的內存不會被釋放(最後的值會保存),閉包仍然需要使用它們。

閉包之間的交互

當存在多個內部函數時,很可能出現意料之外的閉包。我們定義一個遞增函數,這個函數的增量爲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的後續調用還會創建這些閉包的新實例,同時也會創建新的封閉環境,本質上是創建了一個新對象,自由變量就是這個對象的實例變量,而閉包就是這個對象的實例方法,而且這些變量也是私有的,因爲不能在封裝它們的作用域外部直接引用這些變量,從而確保了了面向對象數據的專有性。

閉包的用途

  • 可以讀取函數內部的變量
  • 讓變量的值始終保持在內存中

使用閉包的注意點

  • 由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁中的性能問題,在IE中可能導致內存泄漏。
    **解決方法:**在退出函數之前,將不使用的局部變量全部刪除。
  • 閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當做對象使用,把閉包當做他的公用方法,把內部變量當做他的私有屬性,這時一定要小心,不要隨便改變父函數內部變量的值。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章