js閉包詳解 JavaScript 閉包到底是什麼

1.簡單的例子

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

    <div id="divTest">
        <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,es6

每一個span的onclick方法這時候爲內部函數,因此i被閉包引用,內存不能被銷燬,i的值會一直保持4,閉包

直到程序改變它或者全部的onclick函數銷燬(主動把函數賦爲null或者頁面卸載)時纔會被回收。這樣每次咱們點擊span的時候,onclick函數會查找i的值(做用域鏈是引用方式),一查等於4,而後就alert給咱們了。函數

簡單的修改就好使了(暫且先不談es6 let)post

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當參數傳入,函數當即執行,url

將i做爲實參傳給了行參num,而行參就至關於在函數裏定義了變量num保存每次i的值,num保存每次i的值。spa

2.內部函數

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

function outerFn () {
    function innerFn () {}
innerFn(); //成功
}
innerFn();     //報錯,innerFn is not defined

innerFn就是一個被包在outerFn做用域中的內部函數。這意味着,在outerFn內部調用innerFn是有效的,而在outerFn外部調用innerFn則是無效的。htm

2.1偉大的逃脫

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

例如能夠將內部函數指定給一個全局變量

        var globalVar;
        function outerFn() {
            console.log('out');        
            function innerFn() {
                console.log('inner')
            }
            globalVar = innerFn;
        }
        outerFn();     //out
        globalVar();   //inner

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

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

function outerFn() {
            console.log('out')
            function innerFn() {
               console.log('inner')
            }
            return innerFn;
        }
        var fnRef = outerFn();   //out
        fnRef();                 //inner

從outerFn中返回了一個對innerFn的引用。經過調用outerFn可以得到這個引用,並且這個引用能夠能夠保存在變量中。

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

閉包:能夠訪問外部函數做用域中變量的函數

被內部函數訪問的外部函數的變量能夠保存在外部函數做用域內而不被回收

建立閉包的常見方式就是在一個函數內部建立另外一個函數,就是咱們上面說的內部函數

 

1.2變量的做用域

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

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

function outerFn() {
            console('out')
            function innerFn() {
                var innerVar = 0;
                innerVar++;
                console.log("inner "+innerVar);
            }
            return innerFn;
        }
        var fnRef = outerFn();  //out
        fnRef();                //inner 1
        fnRef();                //inner 1
        var fnRef2 = outerFn(); //out
        fnRef2();               //inner 1
        fnRef2();               //inner1

 

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

內部函數都會持續地遞增這個全局變量的值

        var globalVar = 0;
        function outerFn() {
            console.log('out');
            function innerFn() {
                globalVar++;
                console.log('inner '+globalVar);
            }
            return innerFn;
        }
        var fnRef = outerFn();   //out
        fnRef();                 //inner 1
        fnRef();                 //inner 2
        var fnRef2 = outerFn();  //out
        fnRef2();                //inner 3
        fnRef2();                //inner 4

 

內部函數會引用到父函數的做用域的變量

        function outerFn() {
var outVar=0; console.log('out'); function innerFn() { outVar++; console.log('inner '+outVar); } return innerFn; } var fnRef = outerFn(); //out fnRef(); //inner 1 fnRef(); //inner 2 var fnRef2 = outerFn(); //out fnRef2(); //inner 1 fnRef2(); //inner 2

當外部函數調用完成後,這些變量的內存不會被釋放(最後的值會保存),閉包仍然須要使用它們

 

3.閉包之間的交互

當存在多個內部函數時,極可能出現意料以外的閉包。

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

function outerFn() {
            var outerVar = 0;
            console.log('out');
            function innerFn1() {
                outerVar++;
                console.log('inner '+outVar);
            }

            function innerFn2() {
                outerVar += 2;
                console.log('inner2 '+outVar)
            }
            return { "fn1": innerFn1, "fn2": innerFn2 };
        }
        var fnRef = outerFn();     //out
        fnRef.fn1();               //inner 1
        fnRef.fn2();               //inner2 3
        fnRef.fn1();               //inner 4
        var fnRef2 = outerFn();    //out
        fnRef2.fn1();              //inner 1
        fnRef2.fn2();              //inner2 3
        fnRef2.fn1();              //inner 4

innerFn1和innerFn2引用了同一個局部變量,所以他們共享一個封閉環境。當innerFn1爲outerVar遞增一時,就位innerFn2設置了outerVar的新的起點值。

對outerFn的後續調用還會建立這些閉包的新實例,同時也會建立新的封閉環境,本質上是建立了一個新對象,自由變量就是這個對象的實例變量,而閉包就是這個對象的實例方法

並且這些變量也是私有的,由於不能在封裝它們的做用域外部直接引用這些變量,從而確保了了面向對象數據的專有性。

 

優勢:

  • 可讓一個變量常駐內存 (若是用的多了就成了缺點
  • 避免全局變量的污染
  • 私有化變量

缺點

  • 由於閉包會攜帶包含它的函數的做用域,所以會比其餘函數佔用更多的內存
  • 引發內存泄露 (解決方法:賦null,將閉包引用的外部函數中活動對象清除)

轉自:JavaScript 閉包到底是什麼

相關文章
相關標籤/搜索