JavaScript閉包(記憶體泄漏、溢出以及記憶體回收),超直白解析

1 引言

變數作用域

首先我們先鋪墊一個知識點——變數作用域:

變數根據作用域的不同分為兩種:全局變數和局部變數。

  1. 函數內部可以使用全局變數。
  2. 函數外部不可以使用局部變數。
  3. 當函數執行完畢,本作用域內的局部變數會銷毀。

如果我想在函數外部引用這個函數的局部變數呢?

2 閉包

閉包是什麼?

閉包(closure)指有權訪問另一個函數作用域中變數的函數。 —– JavaScript 高級程式設計

閉包有什麼用?

1延伸變數作用域範圍,讀取函數內部的變數
2讓這些變數的值始終保持在記憶體中

簡單理解就是 ,一個作用域可以訪問另外一個函數內部的局部變數。

閉包案例一

我們來看一個簡單的閉包案例。

        function fn1() {
            var num = 10;
            function fn2() {
                console.log(num);
            }
            fn2();
        }
        fn1(); //輸出結果10

這樣的一個函數寫法我們已經見過或者用過很多次了,但其實這就是一個閉包的運用。
我們可以用Chrome的調試工具驗證一下。

如圖(看不清圖片的夥伴們可以把圖片放大)
在 Scope 選項(Scope 作用域的意思)中,有兩個參數(global 全局作用域、local 局部作用域)。我在fn1函數調用的前面(21行)中設置了一個斷點,進行單步調試,當執行到 fn2() 時,Scope 裡面會多一個 Closure (閉包)參數 ,這就表明產生了閉包。被訪問的變數是num,包含num的函數為fn1。

fn2的作用域當中訪問到了fn1函數中的num這個局部變數 ,所以此時fn1 就是一個閉包函數(被訪問的變數所在的函數就是一個閉包函數)

也有人說,閉包是一種現象,一個作用域訪問了另外一個函數中的局部變數,如果有這種現象的產生,就有了閉包的發生。 我覺的這樣理解也是沒有什麼問題的。

閉包案例二

接下來我們來看一個稍微複雜一點點的閉包

        function fn() {
            var num = 10;
            return function() {
                console.log(num);
            }
        }
        var f = fn();
        // 上面這步類似於
        // var f = function() {
        //         console.log(num);
        //     }
        f();//輸出結果10

在f=fn()這步操作中,執行了num =10 的賦值,並且給f賦值了一個匿名函數,這個函數是fn中return 返回的那個匿名函數,注意此時只是賦值了,並沒有調用。
然後在f()中調用了那個匿名函數,此時我們便做到了在 fn() 函數外面訪問 fn() 中的局部變數 num 。

$閉包延伸了變數作用域範圍,讀取了函數內部的變數$

閉包案例三

首先我們看一下這個閉包案例

        var fn  =function(){
            var sum = 0
            return function(){
                sum++
                console.log(sum);
            }
        }
        fn()() //1
        fn()() //1
        //fn()進行sum變數申明並且返回一個匿名函數,第二個()意思是執行這個匿名函數

這裡出現了一個小問題,sum為什麼沒有自增?如果想要實現自增怎麼操作?
回答這個問題需要先了解一下js中記憶體回收機制。(詳細內容可以看文章後面的3 Js記憶體回收機制

我這裡直接簡單解釋一下,執行fn()() 後,fn()()已經執行完畢,沒有其他資源在引用fn,此時記憶體回收機制會認為fn不需要了,就會在記憶體中釋放它。

那如何不被回收呢?

        var fn  =function(){
            var sum = 0
            return function(){
                sum++
                console.log(sum);
            }
        }
        fn1=fn() 
        fn1()   //1
        fn1()   //2
        fn1()   //3

這種情況下,fn1一直在引用fn(),此時記憶體就不會被釋放,就能實現值的累加。那麼問題又來了,這樣的函數如果太多,就會造成記憶體泄漏(記憶體泄漏、記憶體溢出的知識點在文章後面4 記憶體溢出、記憶體泄漏

記憶體泄漏了怎麼辦呢?我們可以手動釋放一下

        var fn  =function(){
            var sum = 0
            return function(){
                sum++
                console.log(sum);
            }
        }
        fn1=fn() 
        fn1()   //1
        fn1()   //2
        fn1()   //3
        fn1 = null // fn1的引用fn被手動釋放了
        fn1=fn()  //num再次歸零
        fn1() //1

3 Js記憶體回收機制

由於字元串、對象和數組沒有固定大小,當他們的大小已知時,才能對他們進行動態的存儲分配。JavaScript程式每次創建字元串、數組或對象時,解釋器都必須分配記憶體來存儲那個實體。只要像這樣動態地分配了記憶體,最終都要釋放這些記憶體以便他們能夠被再用,否則,JavaScript的解釋器將會消耗完系統中所有可用的記憶體,造成系統崩潰。

現在各大瀏覽器通常用採用的垃圾回收有兩種方法:標記清除、引用計數。

1標記清除

這是javascript中最常用的垃圾回收方式。當變數進入執行環境是,就標記這個變數為「進入環境」。從邏輯上講,永遠不能釋放進入環境的變數所佔用的記憶體,因為只要執行流進入相應的環境,就可能會用到他們。當變數離開環境時,則將其標記為「離開環境」。

2引用計數

引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明了一個變數並將一個引用類型賦值給該變數時,則這個值的引用次數就是1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其所佔的記憶體空間給收回來。這樣,垃圾收集器下次再運行時,它就會釋放那些引用次數為0的值所佔的記憶體。

4 記憶體溢出、記憶體泄漏

記憶體溢出

記憶體溢出一般是指執行程式時,程式會向系統申請一定大小的記憶體,當系統現在的實際記憶體少於需要的記憶體時,就會造成記憶體溢出

記憶體溢出造成的結果是先前保存的數據會被覆蓋或者後來的數據會沒地方存

記憶體泄漏

記憶體泄漏是指程式執行時,一些變數沒有及時釋放,一直佔用著記憶體
而這種佔用記憶體的行為就叫做記憶體泄漏。

作為一般的用戶,根本感覺不到記憶體泄漏的存在。真正有危害的是記憶體泄漏的堆積,這會最終消耗盡系統所有的記憶體。從這個角度來說,一次性記憶體泄漏並沒有什麼危害,因為它不會堆積。

記憶體泄漏如果一直堆積,最終會導致記憶體溢出問題

5 總結

  1. 閉包是什麼?

閉包是一個函數 (一個作用域可以訪問另外一個函數的局部變數)

  1. 閉包的作用是什麼?

1延伸變數作用域範圍,讀取函數內部的變數
2讓這些變數的值始終保持在記憶體中