jQuery閉包之淺見,從面向對象角度來理解


本篇的標題雖然是"jQuery閉包之淺見...",但實際上本篇涉及的多半是javascript"閉包"相關內容,之所以寫這個標題,完全是因為自己平常用jQuery寫前端習慣了。還有一個原因是:javascript"閉包"很容易造成"內存泄漏", 而jQuery已經自動為我們規避、處理了由"閉包"引發的"內存泄漏"。

 

javascript的"閉包"是這樣定義的:一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。

 

定義高度提煉,還是從代碼實例來體驗"閉包"的來龍去脈吧。本篇主要包括:

 

  從面向對象的角度來理解"閉包"

在面向對象中,我們是這樣定義類:

public class SomeClass
{
    private string _someVariable;
 
    public void SomeMethod()
    {
        //TODO:
    }
}

在javascript中,有一種情況是,一個函數中包含了另外一個內部函數:

function OutFunction()
{
    var temp = 0;
    function innerFunction(){
    }
}

我們把OutFunction稱為外部函數,把innerFunction稱為內部函數。

 

這里的外部函數OutFunction相當於一個類。
外部函數變量temp相當於類的私有字段。
內部函數innerFunction相當於類的方法。

 

就像我們需要實例化類來調用類的實例方法一樣,在javasript中,當我們在外部函數之外調用內部函數的時候,我們把此時的內部函數叫做"閉包",相當於面向對象的實例方法。

接下來,從"如何調用內部函數"這個角度,來循序漸進地體會何謂"閉包"。

 

  調用內部函數的幾種方式

□ 在外部函數之外直接調用內部函數

    <script type="text/javascript">
        function outerFunction() {
            console.log('外部函數執行了');
            function innerFunction() {
                console.log('內部函數執行了');
            }
        }
 
        $(function() {
            innerFunction();
        });
    </script>

結果報錯:Uncaught ReferenceError: innerFunction is not defined

以上情況,內部函數的作用域是在外部函數之內,當在外部函數之外調用內部函數的時候,自然就報錯。

 

□ 在外部函數之內調用內部函數:

    <script type="text/javascript">
        function outerFunction() {
            console.log('外部函數執行了');
            function innerFunction() {
                console.log('內部函數執行了');
            }
 
            innerFunction();
        }
 
        $(function() {
            outerFunction();
        });
    </script>

結果:
外部函數執行了
內部函數執行了

以上情況,內部函數的作用域是在外部函數之內,當在外部函數之內調用內部函數,自然不會報錯。

 

□ 在外部函數之外調用內部函數,把內部函數賦值給一個全局變量

    <script type="text/javascript">
        
        //全局變量
        var globalInner;
 
        function outerFunction() {
            console.log('外部函數執行了');
            function innerFunction() {
                console.log('內部函數執行了');
            }
 
            globalInner = innerFunction;
        }
 
        $(function() {
            outerFunction();
            console.log('以下是全局變量調用內部方法:');
            globalInner();
        });
    </script>

結果:
外部函數執行了
以下是全局變量調用內部方法:
內部函數執行了

以上情況,全局變量globalInner相當於面向對象的委托,當把內部函數賦值給全局變量,調用委托方法就會調用內部函數。

 

□ 在外部函數之外調用內部函數,把內部函數賦值給一個變量:

    <script type="text/javascript">
 
        function outerFunction() {
            console.log('外部函數執行了');
            function innerFunction() {
                console.log('內部函數執行了');
            }
 
            return innerFunction;
        }
 
        $(function() {
            console.log('先把外部函數賦值給變量');
            var temp = outerFunction();
            console.log('再執行外部函數變量');
            temp();
        });
    </script>

結果:
先把外部函數賦值給變量
外部函數執行了
再執行外部函數變量
內部函數執行了

以上情況,我們可以看到,內部函數不僅可以賦值給全局變量,還可以賦值給局部變量。

就像面向對象的方法會用到類的字段,內部函數也會用到變量,接下來體驗變量的作用域。

 

  變量的作用域

□ 內部函數變量

    <script type="text/javascript">
 
        function outerFunction() {
            function innerFunction() {
                var temp = 0;
                temp++;
                console.log('內部函數的變量temp的值為:' + temp);
            }
            return innerFunction;
        }
 
        $(function() {
            var out1 = outerFunction();
            out1();
            out1();
 
            var out2 = outerFunction();
            out2();
            out2();
        });
    </script>

結果:
內部函數的變量temp的值為:1
內部函數的變量temp的值為:1
內部函數的變量temp的值為:1
內部函數的變量temp的值為:1

 

從中我們可以看出內部函數變量temp的生命周期:
→當第一次執行內部函數,JavaScript運行時創建temp變量
→當第二次執行內部函數,JavaScript垃圾回收機制把先前的temp回收,並釋放與該temp對應的內存,再創建一個新的內部函數變量temp

.....
所以,每次調用內部函數,內部函數的變量是全新的。也就是說,內部函數的變量與內部函數同生共滅。

 

□ 全局變量

   <script type="text/javascript">
        //全局變量
        var temp = 0;
        function outerFunction() {
            function innerFunction() {
                temp++;
                console.log('全局變量temp的值為:' + temp);
            }
            return innerFunction;
        }
 
        $(function() {
            var out1 = outerFunction();
            out1();
            out1();
 
            var out2 = outerFunction();
            out2();
            out2();
        });
    </script>

結果:
全局變量temp的值為:1
全局變量temp的值為:2
全局變量temp的值為:3
全局變量temp的值為:4

可見,全局變量供外部函數和內部函數共享。

 

□ 外部函數變量

    <script type="text/javascript">
        function outerFunction() {
            var temp = 0;
            function innerFunction() {
                temp++;
                console.log('外部函數變量temp的值為:' + temp);
            }
            return innerFunction;
        }
 
        $(function() {
            var out1 = outerFunction();
            out1();
            out1();
 
            var out2 = outerFunction();
            out2();
            out2();
        });
    </script>

結果:
外部函數變量temp的值為:1
外部函數變量temp的值為:2
外部函數變量temp的值為:1
外部函數變量temp的值為:2

可見,外部函數的變量與外部函數同生共滅。

以上情況,更接近與"閉包"的原型。有如下幾個要素:
1、外部函數
2、外部函數變量
3、內部函數


當我們在外部函數之外調用內部函數的時候,這時的內部函數就叫做"閉包",可以理解為面向對象的實例方法。"閉包"與外部函數變量的"外部環境"是外部函數,他倆與外部函數同生共滅。

 

一個外部函數中可以有多個內部函數,當調用"閉包"的時候,多個"閉包"共享外部函數變量:

 

   <script type="text/javascript">
        function outerFunction() {
            var temp = 0;
            function innerFunction1() {
                temp++;
                console.log('經innerFunction1,外部函數變量temp的值為:' + temp);
            }
            
            function innerFunction2() {
                temp += 2;
                console.log('經innerFunction2,外部函數變量temp的值為:' + temp);
            }
 
            return {'fn1' : innerFunction1, 'fn2' : innerFunction2};
        }
 
        $(function() {
            var out1 = outerFunction();
            out1.fn1();
            out1.fn2();
            out1.fn1();
 
            var out2 = outerFunction();
            out2.fn1();
            out2.fn2();
            out2.fn1();
        });
    </script>
 


結果:
經innerFunction1,外部函數變量temp的值為:1
經innerFunction2,外部函數變量temp的值為:3
經innerFunction1,外部函數變量temp的值為:4
經innerFunction1,外部函數變量temp的值為:1
經innerFunction2,外部函數變量temp的值為:3
經innerFunction1,外部函數變量temp的值為:4

 

  jQuery中的"閉包"

"閉包"在jQuery中最常見的應用是,當Document加載完畢再執行jQuery部分:

    <script type="text/javascript">
        $(document).ready(function() {
            var temp = 0;
 
            function innerFunction() {
                temp++;
                console.log(temp);
            }
 
            innerFunction();
            innerFunction();
        });
    </script>

結果:
1
2

可見,$(document).ready()的參數就是一個匿名外部函數,匿名函數內的函數是內部函數。

 

把元素的事件也可看作是內部函數:

<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script src="../Scripts/jquery-2.1.1.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function() {
            var counter = 0;
 
            $('#btn1').click(function(event) {
                counter++;
                console.log(counter);
            });
 
            $('#btn2').click(function(event) {
                counter--;
                console.log(counter);
            });
        });
    </script>
</head>
<body>
    <input id="btn1" type="button" value="遞增"/>
    <input id="btn2" type="button" value="遞減"/>
</body>
 

可見,2個input元素的click事件看作是內部函數,共享同一個外部函數變量counter。

 

在循環體遍歷中,把每次遍歷的元素事件也可看作是內部函數:

<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script src="../Scripts/jquery-2.1.1.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function() {
            for (var i = 0; i < 3; i++) {
                $('<div>Print ' + i + '</div>').click(function() {
                    console.log(i);
                }).insertBefore('#results');
            }
        });
    </script>
</head>
<body>
    <div id="results"></div>
</body>

頁面呈現的結果如預期:
Print 0
Print 1
Print 2

 

可當點擊每個div的時候,原本以為控制器台應該顯示:0, 1, 2,但實際顯示的始終是3,為什么?
--i看作是匿名外部函數的"自由變量",當頁面加載完畢后,i就變成了3。div的每次點擊看作是內部函數的閉環,而所有的閉環都共享了值為3的這個變量。


我們可以使用jQuery的each()方法來解決以上問題,遍歷一個數組,每一次遍歷元素值都不同:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script src="../Scripts/jquery-2.1.1.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function() {
            $.each([0, 1, 2], function(index, value) {
                $('<div>Print ' + value + '</div>').click(function() {
                    console.log(value);
                }).insertBefore('#results');
            });
        });
    </script>
</head>
<body>
    <div id="results"></div>
</body>

 

  內存泄漏

內存泄漏可以狹隘地理解為:應用程序中變量對應一塊內存,由於某些原因(比如代碼上的漏洞),始終沒有對變量及時實施手動或自動垃圾回收,內存沒有得到及時釋放,造成內存泄漏。

 

在JavaScript中,如果對象間的關系比較簡單:

1

以上,A有一個屬性指向B,B有一個屬性指向C,當A被回收的時候,沒有依賴B的對象,B隨之被自動回收,C也被最終回收。

 

當對象間的關系比較復雜,比如存在循環引用的時候,如下:

2

以上,A有一個屬性指向B, B有一個屬性指向C,而C又有一個屬性指向B,B和C之間存在循環引用。當A被回收之后,B和C是無法自動被回收的,在JavaScript中應該手動處理回收。

 

JavaScript閉包有可能會造成內存泄漏:

→內部函數閉包引發的內存泄漏

        $(function() {
            var outerVal = {};
 
            function innerFunction() {
                console.log(outerVal);
            }
 
            outerVal.fn = innerFunction;
            return innerFunction;
        });

以上,outVal是在內存中的一個對象,而內部函數innerFunction同樣是內存中的一個對象。對象outVal的屬性fn指向內部函數,而內部函數通過console.log(outerVal)引用outVal對象,這樣outVal和內部函數存在循環引用的情況,如果不手動處理,就會發生"內存泄漏"。

 

如果,我們在內部函數中不顯式引用outerVal對象變量,會造成"內存泄漏"嗎?

        $(function() {
            var outerVal = {};
 
            function innerFunction() {
                console.log('hello');
            }
 
            outerVal.fn = innerFunction;
            return innerFunction;
        });

答案是:會的。因為,當內部函數被引用、調用的時候,即使內部函數沒有顯式引用外部函數的變量,也會隱式引用外部函數變量。

→元素事件引發的內存泄漏

在IE中,如下寫法會造成內存泄漏:

$(document).ready(function(){
    var button = document.getElementById("btn");
    button.onclick = function(){
        console.log('hello');
        return false;
    }
});

 

而如下JavaScript寫法不會造成內存泄漏:

function hello(){
    console.log('hello');
    return false;
}
 
$(document).ready(function(){
    var button = docuemtn.getElementById('btn');
    button.onclick = hello;
});

 

而在jQuery中,類似的寫法就不用擔心內存泄漏了,因為jQuery為我們做了自動處理來規避內存泄漏。

$(document).ready(function(){
    var $button = $('#btn');
    $button.click(function(event){
        event.preventDefault();
        console.log('hello');
    });
});

 

  總結

 

與"閉包"相關的包括:變量的作用域、javascript垃圾回收、內存泄漏,需在實踐多體會。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM