理解閉包與內存泄漏

1、閉包的定義
閉包,是指有權訪問另外一個函數做用域中變量的函數。從定義上咱們能夠知道,閉包是函數,而且是被另外一個函數包裹的函數。因此須要用一個函數去包裹另外一個函數,即在函數內部定義函數。被包裹的函數則稱爲閉包函數,包裹的函數(外部的函數)則爲閉包函數提供了一個閉包做用域,因此造成的閉包做用域的名稱爲外部函數的名稱。npm

咱們先來看一個常見的閉包例子,如:瀏覽器

let foo;
function outer() { // outer函數內部爲閉包函數提供一個閉包做用域(outer)閉包

let bar = "bar";
let inner = function() {
    console.log(bar);
    debugger; // 打一個debuuger斷點,以便查看閉包做用域
    console.log("inner function run.");
}
return inner;

}
foo = outer(); // 執行外部函數返回內部函數
foo(); // 執行內部函數
咱們在瀏覽器上執行該段代碼後,會停在斷點位置,此時咱們能夠看到造成的閉包做用域如圖所示,
image函數

從圖中咱們能夠看到,造成的閉包做用域名稱爲外部的outer函數提供的做用域,閉包做用域內有一個變量bar能夠被閉包函數訪問到。工具

2、造成閉包的條件
從上面的閉包例子在,看起來造成的閉包的條件就是,一個函數被另外一個函數包裹,而且返回這個被包裹的函數供外部持有。其實,閉包函數是否被外部變量持有並不重要,造成閉包的必要條件就是,閉包函數(被包裹的函數)中必需要使用到外部函數中的變量。測試

function outer() { // outer函數內部爲閉包函數提供一個閉包做用域(outer)ui

let bar = "bar";
let inner = function() {
    console.log(bar);
    debugger;
    console.log("inner function run.");
}
inner(); // 直接在外部函數中執行閉包函數inner

}
outer();
咱們稍微修改一下上面的例子,外部函數outer不將內部函數inner返回,而是直接在outer內執行。
imagethis

從執行結果能夠看到,仍然造成了閉包,因此說這個被包裹的閉包函數是否被外部持有並非造成閉包的條件。debug

function outer() { // outer函數內部爲閉包函數提供一個閉包做用域(outer)調試

let bar = "bar";
let inner = function() {
    // console.log(bar); // 註釋該行,內部inner函數再也不使用外部outer函數中的變量
    debugger;
    console.log("inner function run.");
}
inner(); // 直接在外部函數中執行閉包函數inner

}
outer();
咱們再修改一下上面的例子,將console.log(bar)這行代碼註釋掉,這樣inner函數中將再也不使用外部outer函數中的變量。
image

從執行結果上能夠看到,沒有造成閉包。因此造成閉包的必要條件就是,被包裹的閉包函數必須使用外部函數中的變量。

固然上面的結論也太過絕對了些,由於外部函數能夠同時包裹多個閉包函數,也就是說,(外部)函數內部定義了多個函數,這種狀況下,就不須要每一個閉包函數都使用到外部函數中的變量,由於閉包做用域是內部全部閉包函數共享的,只要有一個內部函數使用到了外部函數中的變量便可造成閉包。

function outer() { // outer函數內部爲閉包函數提供一個閉包做用域(outer)

let bar = "bar";
let unused = function() {
    console.log(bar); // 再建立一個閉包函數,並在其中使用外部函數中的變量
}
let inner = function() {
    // console.log(bar); // 註釋該行,內部inner函數再也不使用外部outer函數中的變量
    debugger;
    console.log("inner function run.");
}
inner(); // 直接在外部函數中執行閉包函數inner

}
outer();
咱們繼續修改一下上面的例子,在outer函數內部再建立一個unused函數,這個函數只是定義但不會執行,同時unused函數內部使用了外部outer函數中的變量,inner函數仍然不使用外部outer函數中的變量。
image

從執行結果能夠看到,又造成了閉包。因此造成的閉包條件就是,存在內部函數中使用外部函數中定義的變量。

3、內存泄漏
內存泄漏經常與閉包牢牢聯繫在一塊兒,很容易讓人誤覺得閉包就會致使內存泄漏。其實閉包只是讓內存常駐,而濫用閉包纔會致使內存泄漏。
內存泄漏,從廣義上說就是,內存在使用完畢以後,對於再也不要的內存沒有及時釋放或者沒法釋放。再也不須要的內存使用完畢以後確定須要釋放掉,不然這個塊內存就浪費掉了,至關於內存泄漏了。可是在實際中,每每不會經過判斷該內存或變量是否再也不須要使用來判斷。由於內存測試工具很難判斷該內存是否再也不須要。因此咱們一般會重複屢次執行某段邏輯鏈路,而後每隔一段時間進行一次內存dump,而後判斷內存是否存在不斷增加的趨勢,若是存在,則可用懷疑存在內存泄漏的可能。

4、內存dump
瀏覽器中抓取內存的dump相對來講簡單些,直接經過谷歌瀏覽器的調試工具找到memory對應的tab頁面,而後點擊Load便可開始抓取內存dump,如:
image

在NodeJS中,咱們也能夠經過引入heapdump來抓取內存dump,直接經過npm安裝heapdump模塊便可

npm install heapdump
安裝完成以後,便可直接在應用程序中使用了,用法很是簡單,如:

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump

// 應用code部分

heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump
應用程序執行完成後,會在應用根目錄中生成start.heapsnapshot和end.heapsnapshot兩個內存dump文件,咱們能夠經過判斷兩個文件的大小變化來判斷是否存在內存泄漏。

固然並非說內存dump文件的大小不斷增大就存在內存泄漏,若是應用的訪問量確實在一直增大,那麼內存曲線只增不減也屬於正常狀況,咱們只能根據具體狀況判斷是否存在內存泄漏的可能。

5、常見的內存泄漏
① 閉包循環引用

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump
let foo = null;
function outer() {

let bar = foo;
function unused() { // 未使用到的函數
    console.log(`bar is ${bar}`);
}

foo = { // 給foo變量從新賦值
    bigData: new Array(100000).join("this_is_a_big_data"), // 若是這個對象攜帶的數據很是大,將會形成很是大的內存泄漏
    inner: function() {
        console.log(`inner method run`);
    }
}

}
for(let i = 0; i < 1000; i++) {

outer();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump
在這個例子中,執行了1000次outer函數,start.heapsnapshot文件的大小爲2.4M,而end.heapsnapshot文件的大小爲4.1M,因此可能存在內存泄漏。
前面講解閉包的過程當中,咱們已經能夠知道outer函數內部是存在閉包的,由於outer函數內部定義了unused和inner兩個函數,雖然inner函數中沒有使用到outer函數中的變量,可是unused函數內部使用到了outer函數中的bar變量,故造成閉包,inner函數也會共享outer函數提供的閉包做用域。
因爲閉包的存在,bar變量不能釋放,即至關於inner函數隱式持有了bar變量,因此存在...-->foo-->inner-->bar-->foo(賦值給bar的foo,即上一次的foo)...。
這裏inner隱式持有bar變量怎麼理解呢?由於inner是一個閉包函數,可使用outer提供的閉包做用域中的bar變量,因爲閉包的關係,bar變量不能釋放,因此bar變量一直在內存中,而bar變量又指向了上一次賦值給bar的foo對象,因此會存在這樣一個引用關係。

那怎麼解決呢?因爲bar變量常駐內存不能釋放,因此咱們能夠在outer函數執行完畢的時候手動釋放,即將bar變量置爲null,這樣以前賦值給bar的foo對象就沒有被其餘變量引用了,就會被回收了。

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump
let foo = null;
function outer() {

let bar = foo;
function unused() { // 未使用到的函數
    console.log(`bar is ${bar}`);
}

foo = { // 給foo變量從新賦值
    bigData: new Array(100000).join("this_is_a_big_data"), // 若是這個對象攜帶的數據很是大,將會形成很是大的內存泄漏
    inner: function() {
        console.log(`inner method run`);
    }
}
bar = null; // 手動釋放bar變量,解除bar變量對上一次foo對象的引用

}
for(let i = 0; i < 1000; i++) {

outer();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump
手動釋放bar變量是一種相對比較好的解決方式。關鍵在於要解除閉包解除bar變量對上一次foo變量的引用。因此咱們可讓unused方法內不使用bar變量,或者將bar變量的定義放在一個塊級做用域中,如:

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump
let foo = null;
function outer() {

{ // 將bar變量定義在一個塊級做用域內,這樣outer函數中就沒有定義變量了,天然inner也不會造成閉包
    let bar = foo;
    function unused() { // 未使用到的函數
        console.log(`bar is ${bar}`);
    }
}

foo = { // 給foo變量從新賦值
    bigData: new Array(100000).join("this_is_a_big_data"), // 若是這個對象攜帶的數據很是大,將會形成很是大的內存泄漏
    inner: function() {
        console.log(`inner method run`);
    }
}

}
for(let i = 0; i < 1000; i++) {

outer();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump
② 重複註冊事件,好比頁面一進入就重複註冊1000個同名事件(一次模擬每次進入頁面都註冊一次事件)

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump
const events = require('events');
class Page extends events.EventEmitter {

onShow() {
    for (let i = 0; i < 1000; i++) {
        this.on("ok", () => {
            console.log("on ok signal.");
        });
    }
}
onDestory() {
    
}

}
let page = new Page();
page.setMaxListeners(0); // 設置能夠註冊多個同名事件
page.onShow();
page.onDestory();
heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump
這個例子中Page頁面一進入就會同時註冊1000個同名的ok事件,start.heapsnapshot文件的大小爲2.4M,而end.heapsnapshot文件的大小爲2.5M,因此可能存在內存泄漏。
解決方式就是,在頁面離開的時候移除全部事件,或者在頁面建立的時候僅註冊一次事件,如:

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump
const events = require('events');
class Page extends events.EventEmitter {

onCreate() {
    this.on("ok", () => { // 僅在頁面建立的時候註冊一次事件,避免重複註冊事件
        console.log("on ok signal.");
    });
}
onShow() {
    // for (let i = 0; i < 1000; i++) {
    //     this.on("ok", () => {
    //         console.log("on ok signal.");
    //     });
    // }
}
onLeave() {
    this.removeAllListeners("ok"); // 或者在離開頁面的時候移除全部ok事件
}

}
let page = new Page();
page.setMaxListeners(0); // 設置能夠註冊多個同名事件
page.onCreate();
page.onShow();
page.onLeave();
heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump
③ 意外的全局變量,這是咱們經常簡單的內存泄漏例子,實際上內存工具很難判斷意外的全局變量是否存在內存泄漏,除非應用程序不斷的往這個全局變量中加入數據,不然對於一個恆定不變的意外全局變量內存測試工具是沒法判斷出是否存在內存泄漏的,因此咱們儘可能不要隨意使用全局變量來保存數據。

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump

function createBigData() {

const bigData = [];
for(let j = 0; j < 100; j++) {
    bigData.push(new Array(10000).join("this_is_a_big_data"));
}
return bigData;

}

function fn() {

foo = createBigData(); // 意外的全局變量致使內存泄漏

}
for (let j = 0; j < 100; j++) {

fn();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump
該例子執行後,end.heapsnapshot文件的大小爲2.5M也變成了2.5M,執行fn函數的時候意外產生了一個全局變量foo,並賦值爲了一個很大的數據,若是foo變量用完後咱們再也不須要,那麼咱們就要主動釋放,不然常駐內存形成內存泄漏,若是這個全局變量咱們後續還須要使用到,那麼就不算內存泄漏。
解決方法就是,將foo定義成局部變量,如:

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump

function createBigData() {

const bigData = [];
for(let j = 0; j < 100; j++) {
    bigData.push(new Array(10000).join("this_is_a_big_data"));
}
return bigData;

}

function fn() {

// foo = createBigData(); // 意外的全局變量致使內存泄漏
const foo = createBigData(); // 將foo定義爲局部變量,避免內存泄漏

}
for (let j = 0; j < 100; j++) {

fn();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump
④ 事件未及時銷燬

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump
const events = require('events');
function createBigData() {

const bigData = [];
for(let j = 0; j < 100; j++) {
    bigData.push(new Array(100000).join("this_is_a_big_data"));
}
return bigData;

}

class Page extends events.EventEmitter {

onCreate() {
    const data = createBigData();
    this.handler = () => {
        this.update(data);
    }
    this.on("ok", this.handler);
}

update(data) {
    console.log("開始更新數據了"); // 接收到ok信號,能夠開始更新數據了
}

onDestory() {
   
}

}
let page = new Page();
page.onCreate();
page.onDestory();
heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump
此例中頁面onCreate的時候會註冊一個ok事件,事件處理函數爲this.handler,this.handler的定義會造成一個閉包,致使data沒法釋放,從而內存溢出。
解決辦法就是移除事件並清空this.handler,由於this.handler這個閉包函數被兩個變量持有,一個是page對象的handler屬性持有,另外一個是事件處理器因爲註冊事件後被事件處理器所持有。因此須要釋放this.handler而且移除事件監聽。

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 記錄應用開始時的內存dump
const events = require('events');
function createBigData() {

const bigData = [];
for(let j = 0; j < 100; j++) {
    bigData.push(new Array(100000).join("this_is_a_big_data"));
}
return bigData;

}

class Page extends events.EventEmitter {

onCreate() {
    const data = createBigData();
    this.handler = () => {
        this.update(data);
    }
    this.on("ok", this.handler);
}

update(data) {
    console.log("開始更新數據了"); // 接收到ok信號,能夠開始更新數據了
}

onDestory() {
    this.removeListener("ok", this.handler); // 移除ok事件,解決事件處理器對this.handler閉包函數的引用
    this.handler = null; //解除page對象對this.handler閉包函數的引用
}

}let page = new Page();page.onCreate();page.onDestory();heapdump.writeSnapshot('end.heapsnapshot'); // 記錄應用結束時的內存dump解除page對象和事件處理器對象對this.handler閉包函數的引用後,this.handler閉包函數就會被釋放,從而解除閉包,data也會獲得釋放。

相關文章
相關標籤/搜索