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

233發表於2020-08-29

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讓這些變數的值始終保持在記憶體中

相關文章