閉包和垃圾回收機制


 

閉包就是有權限訪問 其他函數作用域的局部變量的 一個函數

在JS中,變量的作用域屬於函數作用域,在函數執行后作用域就會被清理、內存也隨之被收回,但是由於閉包時建立在一個函數內部的子函數,由於其可訪問上級作用域的原因,即使上級函數執行完,作用域也不會隨之銷毀,這時的子函數---也就是閉包,便擁有了訪問上級作用域中的變量的權限,即使上級函數執行完后,作用域內的值也不會被銷毀。

 

function a(){
  var i=0; function b(){ alert(++i); } return b; } var c=a(); c(); // 1 c(); // 2 c(); // 3
這段代碼有兩個特點:
1、 函數b嵌套在函數a內部
2、 函數a返回函數b。
這樣在執行完var c=a( )后,變量c實際上是指向了函數b,再執行c( )后就會彈出一個窗口顯示i的值(第一次為1)。
這段代碼其實就創建了一個閉包,這是因為 函數a外的變量c引用了函數a內的函數b。
也就是說, 當函數a的內部函數b被函數a外的一個變量引用的時候,就創建了一個閉包。
 
【對比】:
function fn3(){
    var a = 10; return function(){ a--; console.log(a); } } fn3()();//9 fn3()();//9
當fn3()()第一次執行完后,整個fn3()被銷毀,第二次fn3()相當於重新開辟了一塊新的空間,所以第二次fn3()()和第一次打印的結果無關。
function fn3(){
    var a = 10; return function(){ a--; console.log(a); } } var val = fn3(); val(); //9 val(); //8 
外部的函數連續調用了內部的兩次,當fn3第一次執行完后,val=9並沒有被銷毀,第二次是在第一次基礎之上的

 val指向的對象會永遠存在堆內存中,即使是fn3已經執行完畢
 val = null;   //如果val不再使用,將其指向的對象釋放
 
function Person(){
    var a = 10; return { age:function(){ a += 1; console.log(a); } } } var result = Person(); result.age(); // 11 result.age(); // 12
 
通常, 函數的作用域及其所有變量都會在函數執行結束后被銷毀。
但是, 在創建了一個閉包以后, 這個函數的作用域就會一直保存到閉包不存在為止。
    function add(x) {
        return function(y) { return x + y } } var add5 = add(5); var add10 = add(10); console.log(add5(2)); console.log(add10(2));
相當於在上一次的基礎上繼續加2,因為上一次結果被存在了內存當中,沒有被釋放
 
釋放對閉包的引用
    add5 = null;
    add10 = null;

從上述代碼可以看到add5 和 add10 都是閉包。它們共享相同的函數定義,但是保存了不同的環境。
在 add5 的環境中,x 為 5。而在 add10 中,x 則為 10。最后通過 null 釋放了 add5 和 add10 對閉包的引用。
 
閉包解決了什么
阮一峰在他的博客中寫到:在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
 
務實一點的說法應該是下面這樣:
由於閉包可以緩存上級作用域,那么就使得函數外部打破了“函數作用域”的束縛,可以訪問函數內部的變量。
以平時使用的 AJAX 成功回調為例,這里其實就是個閉包,由於上述的特性,回調就擁有了整個上級作用域的訪問和操作能力,提高了極大的便利。開發者不用去學鈎子函數來操作上級函數作用域內部的變量了
 
閉包最大的用處: 1) 一個是 可以讀取函數內部的變量;
                              2) 另一個就是 讓這些變量的值始終保存在內存中。
 
 
閉包的應用場景
閉包隨處可見,一個 Ajax 請求的成功回調,一個事件綁定的回調方法,一個 setTimeout 的延時回調,或者一個函數內部返回另一個匿名函數,這些都是閉包。
簡而言之,無論使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時,都有閉包的身影

 

垃圾回收機制
 
如果一個對象不再被引用, 那么這個對象就會被垃圾回收機制回收;
如果兩個對象互相引用, 且不再被第3者所引用, 那么這兩個互相引用的對象也會被回收。
(在閉包中,父函數被子函數引用,子函數又被外部的一個變量引用,這就是父函數不被回收的原因)
 
我們知道,程序的運行需要內存,如果一個程序持續不斷地運行,而內存沒有及時釋放的話,那么它的內存占用會iu越來越高,輕則影響系統性能,重則導致進程崩潰。
如果不再用到的內存,沒有及時釋放,我們就稱之為內存泄漏。顯然,這對於我們程序員來講簡直是個噩夢。
所以,大多數語言都有它自身的垃圾回收機制,這樣的好處是自動幫我們清理不必要的內存占用,但是我們的可控性卻比較差,而C語言就無法自動清理垃圾,但它的可控性較強。

在javascript中,我們的垃圾回收機制會定期(周期性)找出那些不再用到的內存(變量),然后釋放其內存,以此來解決內存泄漏的問題。 現在各大瀏覽器通常采用的垃圾回收機制有兩種方法:標記清除,引用計數。js中最常用的垃圾回收方式就是標記清除。
 
1.標記清除
        在一個函數中聲明一個變量,就將這個變量標記為"進入環境",從邏輯上講,永遠不能釋放進入環境變量所占用的內存,因為只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記為"離開環境"。
function foo(){
    var name = 'edward';    //被標記"進入環境"
    var age = "25";         //被標記"進入環境"
} foo(); //執行完畢后之后,name和age又被標記"離開環境",被回收

垃圾回收機制在運行的時候會給存儲在內存中的所有變量都加上標記(可以是任何標記方式),然后,它會去掉處在環境中的變量及被環境中的變量引用的變量標記(閉包)。而在此之后剩下的帶有標記的變量被視為准備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最后垃圾回收機制到下一個周期運行時,將釋放這些變量的內存,回收它們所占用的空間。

 
2.引用計數

        語言引擎有一張"引用表",保存了內存里面所有資源(通常是各種值)的引用次數。如果一個值的引用次數是0,就表示這個值不再用到了,因此可以將這塊內存釋放。

const arr = [1,2,3,4];
console.log("welcome to my blog");

上面的代碼中,數組[1,2,3,4]是一個值,會占用內存。變量arr是僅有的對這個值的引用,因此引用次數為1。盡管后面的代碼沒有用到arr,它是會持續占用內存。

如果增加一行代碼,解除arr對[1,2,3,4]引用,這塊內存就可以被垃圾回收機制釋放了。

const arr = [1,2,3,4];
console.log("welcome to my blog"); arr = null;

因此,並不是說有了垃圾回收機制,程序員就輕松了。你還是需要關注內存占用:那些很占空間的值,一旦不再用到,你必須檢查是否還存在對它們的引用。如果是的話,就必須手動解除引用。

 

【思考】:

什么情況會引起內存泄漏?
雖然有垃圾回收機制但是我們編寫代碼操作不當還是會造成內存泄漏。

1.    意外的全局變量引起的內存泄漏。

原因:全局變量,不會被回收。

解決:使用嚴格模式避免。

2.    閉包引起的內存泄漏

原因:閉包可以維持函數內局部變量,使其得不到釋放。

解決:將事件處理函數定義在外部,解除閉包,或者在定義事件處理函數的外部函數中,刪除對dom的引用。

3.    沒有清理的DOM元素引用

原因:雖然別的地方刪除了,但是對象中還存在對dom的引用

解決:手動刪除。

4.    被遺忘的定時器或者回調

原因:定時器中有dom的引用,即使dom刪除了,但是定時器還在,所以內存中還是有這個dom。

解決:手動刪除定時器和dom。

5.    子元素存在引用引起的內存泄漏

原因:div中的ul li  得到這個div,會間接引用某個得到的li,那么此時因為div間接引用li,即使li被清空,也還是在內存中,並且只要li不被刪除,他的父元素都不會被刪除。

解決:手動刪除清空。

 

什么放在內存中?什么不放在內存中?
基本類型是:Undefined/Null/Boolean/Number/String

基本類型的值存在內存中,被保存在棧內存中。從一個變量向另一個變量復制基本類型的值,會創建這個值的一個副本。

引用類型:object

引用類型的值是對象,保存在堆內存中。

1.    包含引用類型值的變量實際上包含的並不是對象本身,而是一個指向該對象的指針。從一個變量向另一個變量復制引用類型的值,復制的其實是指針,因此兩個變量最終都指向同一個對象。

2.    js不允許直接訪問內存中的位置,也就是不能直接訪問操作對象的內存空間。在操作對象時,實際上是在操作對象的引用而不是實際的對象。

 

棧和堆的區別
  一、堆棧空間分配區別:
  1、棧(操作系統):由操作系統自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧;
  2、堆(操作系統): 一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收,分配方式倒是類似於鏈表。
  二、堆棧緩存方式區別:
  1、棧使用的是一級緩存, 他們通常都是被調用時處於存儲空間中,調用完畢立即釋放;
  2、堆是存放在二級緩存中,生命周期由虛擬機的垃圾回收算法來決定(並不是一旦成為孤兒對象就能被回收)。所以調用這些對象的速度要相對來得低一些。
  三、堆棧數據結構區別:
  堆(數據結構):堆可以被看成是一棵樹,如:堆排序;
  棧(數據結構):一種先進后出的數據結構。

 

【小練習】:請說出下列的打印結果

1. 

    function foo() {
        var a = 1

        function fn() { a++; console.log(a) } return fn() } console.log(foo); foo(); foo();

2.

    function fn1() {
        var a = 1; var b = 2; function fn() { a++; console.log(a) } return fn; } var x = fn1(); x(); x(); var x = fn1(); x();

 

 


免責聲明!

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



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