乐趣区

关于javascript:理解闭包与内存泄漏

一、闭包的定义
闭包,是指有权拜访另一个函数作用域中变量的函数。从定义上咱们能够晓得,闭包是函数,并且是被另一个函数包裹的函数。所以须要用一个函数去包裹另一个函数,即在函数外部定义函数。被包裹的函数则称为闭包函数,包裹的函数 (内部的函数) 则为闭包函数提供了一个闭包作用域,所以造成的闭包作用域的名称为内部函数的名称。

咱们先来看一个常见的闭包例子,如:

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 能够被闭包函数拜访到。

二、造成闭包的条件
从下面的闭包例子在,看起来造成的闭包的条件就是,一个函数被另一个函数包裹,并且返回这个被包裹的函数供内部持有。其实,闭包函数是否被内部变量持有并不重要,造成闭包的必要条件就是,闭包函数 (被包裹的函数) 中必须要应用到内部函数中的变量。

function outer() { // outer 函数外部为闭包函数提供一个闭包作用域(outer)

let bar = "bar";
let inner = function() {console.log(bar);
    debugger;
    console.log("inner function run.");
}
inner(); // 间接在内部函数中执行闭包函数 inner

}
outer();
咱们略微批改一下下面的例子,内部函数 outer 不将外部函数 inner 返回,而是间接在 outer 内执行。
image

从执行后果能够看到,依然造成了闭包,所以说这个被包裹的闭包函数是否被内部持有并不是造成闭包的条件。

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

从执行后果能够看到,又造成了闭包。所以造成的闭包条件就是,存在外部函数中应用内部函数中定义的变量。

三、内存透露
内存透露经常与闭包紧紧分割在一起,很容易让人误以为闭包就会导致内存透露。其实闭包只是让内存常驻,而滥用闭包才会导致内存透露。
内存透露,从狭义上说就是,内存在应用结束之后,对于不再要的内存没有及时开释或者无奈开释。不再须要的内存应用结束之后必定须要开释掉,否则这个块内存就节约掉了,相当于内存透露了。然而在理论中,往往不会通过判断该内存或变量是否不再须要应用来判断。因为内存测试工具很难判断该内存是否不再须要。所以咱们通常会反复屡次执行某段逻辑链路,而后每隔一段时间进行一次内存 dump,而后判断内存是否存在一直增长的趋势,如果存在,则可用狐疑存在内存透露的可能。

四、内存 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 文件的大小一直增大就存在内存透露,如果利用的访问量的确在始终增大,那么内存曲线只增不减也属于失常状况,咱们只能依据具体情况判断是否存在内存透露的可能。

五、常见的内存透露
① 闭包循环援用

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 也会失去开释。

退出移动版